Optimize Authentication Flow, Better Visual Feedback #25

Merged
LAX18 merged 4 commits from npease-authentication-fix-optimization into main 2024-02-23 07:09:52 -10:00
8 changed files with 114 additions and 85 deletions
+29 -6
View File
@@ -4,8 +4,9 @@ import { NextResponse } from "next/server";
import { auth } from "firebase-admin";
import { signInWithEmailAndPassword } from "firebase/auth";
// Lib Imports
import { auth as authConfig } from "../firebase-config";
import { app, auth as authConfig } from "../firebase-config";
import { customInitApp } from "../firebase-admin";
import { getDatabase, ref, get as firebaseGet } from "firebase/database";
// Needs to "init" on each call to the API
customInitApp();
@@ -16,9 +17,32 @@ async function handleEmailAndPassword(email, password) {
var userCredential = await signInWithEmailAndPassword(authConfig,email,password);
if (userCredential.user.accessToken) {
var token = await auth().verifyIdToken(userCredential.user.accessToken);
var expiresIn = 20 * 60 * 1000; // 20 minutes
var sessionCookie = await auth().createSessionCookie(userCredential.user.accessToken, {expiresIn,});
if (token) {
var expiresIn = 20 * 60 * 1000; // 20 minutes
var sessionCookie = await auth().createSessionCookie(userCredential.user.accessToken, {expiresIn,});
var database = getDatabase(app)
var user = await firebaseGet(ref(database, `users/${userCredential.user.uid}`));
if (!user.exists()) {
var userOptions = {
name: "user",
value: JSON.stringify({defined: false, uid: userCredential.user.uid}),
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
} else {
var userData = user.val()
userData.uid = userCredential.user.uid
userData.defined = true
var userOptions = {
name: "user",
value: JSON.stringify(userData),
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
}
cookies().set(userOptions);
var options = {
name: "session",
value: sessionCookie,
@@ -27,14 +51,13 @@ async function handleEmailAndPassword(email, password) {
secure: true,
};
cookies().set(options);
var uid_options = {
cookies().set({
name: "uid",
value: userCredential.user.uid,
maxAge: expiresIn, // 20 mins
httpOnly: true,
secure: true,
};
cookies().set(uid_options);
});
return NextResponse.json({ options }, { status: 200 });
}
}
+3 -10
View File
@@ -3,15 +3,8 @@ import { NextResponse } from "next/server";
export async function GET(req) {
cookies().set({
name: "session",
value: "",
maxAge: -1,
});
cookies().set({
name: "firstName",
value: "",
maxAge: -1,
});
cookies().delete('user')
cookies().delete('session')
cookies().delete('uid')
return NextResponse.json({}, { status: 200 });
}
+2 -32
View File
@@ -1,37 +1,7 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { app } from "../firebase-config";
import { getDatabase, ref, get as firebaseGet } from "firebase/database";
export async function POST(req,res) {
var uid = await req?.json()
var database = getDatabase(app)
var user = await firebaseGet(ref(database, `users/${uid}`));
if (!user.exists()) {
return NextResponse.json({
firstName: "not-found",
lastName: "not-found",
uid: "not-found",
});
} else {
cookies().set("firstName",user.val()?.firstName)
cookies().set("lastName",user.val()?.lastName)
cookies().set("uid",uid)
return NextResponse.json({
firstName: user.val()?.firstName,
lastName: user.val()?.lastName,
uid: uid,
})
}
}
export async function GET(req) {
var uid = cookies().get("uid")?.value
var database = getDatabase(app)
var user = await firebaseGet(ref(database, `users/${uid}`));
return NextResponse.json({
firstName: user.val()?.firstName,
lastName: user.val()?.lastName,
uid: cookies().get("uid")?.value,
})
var userData = cookies().get("user")?.value || false
return userData != false? NextResponse.json(JSON.parse(userData)): NextResponse.json({},{status: 203})
}
+1 -1
View File
@@ -1,7 +1,7 @@
export function Header() {
return (
<div className="m-2 rounded-lg h-[60px] bg-white shadow-2xl">
<a href="/"><img src="/logos/logo_transparent_inverse.png" className="h-[60px]"></img></a>
<a href="/"><img src="/logos/logo_transparent_inverse.png" className="h-[60px]"/></a>
</div>
)
}
+10 -2
View File
@@ -6,7 +6,7 @@ import "../globals.css"
function Login() {
var router = useRouter();
//var { register, handleSubmit } = useForm();
var { register, control, setError, formState: { errors } } = useForm()
var { register, control, setError, formState: { errors, isSubmitting, isSubmitted } } = useForm()
return (
<div>
<div className="grid h-screen place-items-center">
@@ -32,7 +32,15 @@ function Login() {
>
<input type="email" id="email" className={(errors.email && errors.password) && "err"} {...register("email", { required: true })} placeholder="Enter Email Address"/><br/>
<input type="password" id="password" name="password" className={(errors.email && errors.password) && "err"} {...register("password", { required: true })} placeholder="Enter Password"/><br/>
<button className="bg-[#dee0e0] m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Log In</button>
<button className="inline-flex items-center px-4 py-2 transition ease-in-out duration-150 bg-[#dee0e0] m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
{(isSubmitting || isSubmitted) && <span className="inline-block">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span> }
Log In
</button>
<br/>Need an account? <a href="/register">Sign Up</a><br/>
</Form>
</div>
+20 -5
View File
@@ -1,5 +1,16 @@
"use client"
import { useState, useEffect } from 'react'
function Home() {
const [statusCode, setData] = useState(null)
useEffect(() => {
fetch('/api/user')
.then((res) => res.status)
.then((status) => {
setData(status)
})
}, [])
return (
<div>
<div className="grid h-screen place-items-center">
@@ -9,10 +20,14 @@ function Home() {
Chat with friends!
</span>
<div className="m-5">
<a href="/login"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Login</button></a>
<a href="/register"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Signup</button></a>
{statusCode == 203 &&
<div>
<a href="/login"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Login</button></a>
<a href="/register"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Sign Up</button></a>
</div>
}
{statusCode == 200 && <a href="/app"><button className="bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">Continue to App</button></a>}
</div>
</div>
</div>
+10 -3
View File
@@ -4,7 +4,7 @@ import { useForm, Form } from "react-hook-form";
import "../globals.css"
function Register() {
var { register, control, setError, formState: { errors } } = useForm()
var { register, control, setError, formState: { errors, isSubmitting, isSubmitted } } = useForm()
var router = useRouter();
var emailRegex = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
return (
@@ -25,8 +25,15 @@ function Register() {
>
<input type="email" {...register("email", {required: true, pattern: emailRegex})} className={errors.email && "err"} placeholder="Enter Email Address"/><br/>
<input type="password" {...register("password", {required: true})} className={errors.password && errors.password.type == 'required' && "err"} placeholder="Enter Password"/><br/>
<button type="submit" className="bg-[#dee0e0] m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
Register</button><br/>
<button className="inline-flex items-center px-4 py-2 transition ease-in-out duration-150 bg-[#dee0e0] m-5 bg-cyan-500 text-white font-bold py-2 px-4 rounded-full">
{(isSubmitting || isSubmitted) && <span className="inline-block">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</span> }
Register
</button><br/>
Have an account? <a href="/login">Log In</a>
</Form>
</div>
+39 -26
View File
@@ -3,40 +3,53 @@ import { NextResponse } from "next/server";
import { cookies } from "next/headers";
export async function middleware(req, res) {
const session = req.cookies.get("session");
// Login if not logged in
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
//Call the authentication endpoint
const responseAPI = await fetch(new URL("/api/login", req.url), {
headers: {
Cookie: `session=${session?.value}`,
},
});
const session = await req.cookies.get("session");
if (req.nextUrl.pathname !== "/login" && req.nextUrl.pathname != "/register") {
// Login if not logged in
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
//Call the authentication endpoint
const responseAPI = await fetch(new URL("/api/login", req.url), {
headers: {
Cookie: `session=${session?.value}`,
},
});
// Login if unauthorized
if (responseAPI.status !== 200) {
return NextResponse.redirect(new URL("/login", req.url));
}
// Login if unauthorized
if (responseAPI.status !== 200) {
return NextResponse.redirect(new URL("/login", req.url));
}
// If new user, redirect to onboarding
var { uid } = await responseAPI.json()
var user = await fetch(new URL("/api/user", req.url), {
method: "POST",
body: JSON.stringify(uid ? uid : {}),
});
user = await user.json();
if (user.firstName !== "not-found") {
return NextResponse.next();
// If new user, redirect to onboarding
var user = JSON.parse(req.cookies.get("user").value)
if (user.defined) {
return NextResponse.next();
} else {
return NextResponse.redirect(new URL("/onboarding", req.url));
}
} else {
return NextResponse.redirect(new URL("/onboarding", req.url));
// Currently in the /login or /register, if user is authenticated, go ahead and direct them to the app
if (session) {
const responseAPI = await fetch(new URL("/api/login", req.url), {
headers: {
Cookie: `session=${session?.value}`,
},
});
if (responseAPI.status == 200) {
return NextResponse.redirect(new URL("/app", req.url))
} else {
return NextResponse.next() // Unauthenticated, continue
}
} else {
return NextResponse.next() // Not logged in, direct to login
}
}
}
//Protected routes
export const config = {
matcher: ['/((?!login|register|onboarding|api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|logo|$).*)',],
matcher: ['/((?!onboarding|api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|logo|$).*)',],
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },