Skip to content

Commit

Permalink
Redirects and tidy up a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
delbaoliveira committed Mar 21, 2024
1 parent af0f238 commit 77e5d2d
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 123 deletions.
90 changes: 44 additions & 46 deletions app/auth/01-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,39 +30,40 @@ export async function signup(
password: formData.get('password'),
});

// 2. If any form fields are invalid, return early and display errors
// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

// 3. Prepare data for insertion into database
// 2. Prepare data for insertion into database
const { name, email, password } = validatedFields.data;
// 3.1 Hash the user's password
// Hash the user's password
const hashedPassword = await bcrypt.hash(password, 10);

// 4. Insert the user into the database
try {
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id });

// 5. Create a session for the user
if (data && data.length > 0) {
const userId = data[0].id.toString();
await createSession(userId);
}
} catch (error) {
// 3. Insert the user into the database or call an Auth Provider's API
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id });

const user = data[0];

if (!user) {
return {
message: 'An error occurred while creating your account.',
};
}

// 4. Create a session for the user
const userId = user.id.toString();
await createSession(userId);
redirect('/dashboard');
}

export async function login(
Expand All @@ -76,40 +77,37 @@ export async function login(
});
const errorMessage = { message: 'Invalid login credentials.' };

// 2. If any form fields are invalid, return early and display errors
// If any form fields are invalid, return early
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

// 3. Query the database for the user with the given email
try {
const user = await db.query.users.findFirst({
where: eq(users.email, validatedFields.data.email),
});

// If user is not found, return early and display an error
if (!user) {
return errorMessage;
}
// 4. Compare the user's password with the hashed password in the database
const passwordMatch = await bcrypt.compare(
validatedFields.data.password,
user.password,
);

// If the password does not match, return early and display an error
if (!passwordMatch) {
return errorMessage;
}

// 5. If login successful, create a session for the user
const userId = user.id.toString();
await createSession(userId);
} catch (error) {
// 2. Query the database for the user with the given email
const user = await db.query.users.findFirst({
where: eq(users.email, validatedFields.data.email),
});

// If user is not found, return early
if (!user) {
return errorMessage;
}
// 3. Compare the user's password with the hashed password in the database
const passwordMatch = await bcrypt.compare(
validatedFields.data.password,
user.password,
);

// If the password does not match, return early
if (!passwordMatch) {
return errorMessage;
}

// 4. If login successful, create a session for the user and redirect
const userId = user.id.toString();
await createSession(userId);
redirect('/dashboard');
}

export async function logout() {
Expand Down
3 changes: 2 additions & 1 deletion app/auth/02-database-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import jwt from 'jsonwebtoken';
import { cookies } from 'next/headers';

// TODO: Replace with secret key from environment variables
// Update to use jose instead of jwt
const secretKey = 'yourSecretKey';

export async function createSession(id: number) {
Expand Down Expand Up @@ -56,7 +57,7 @@ export async function createSession(id: number) {
// - Server Actions or Server Components, use `cookies()`
// - Route handler, can use either headers or cookies

export async function verifyServerSession(token: string | undefined) {
export async function verifySession(token: string | undefined) {
// const token = cookies().get('token')?.value;
if (!token) return null;

Expand Down
20 changes: 2 additions & 18 deletions app/auth/02-stateless-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { cookies } from 'next/headers';
import type { SessionPayload } from '@/app/auth/definitions';

// TODO: Replace with secret key from environment variables

const secretKey = 'yourSecretKey';
const key = new TextEncoder().encode(secretKey);

Expand Down Expand Up @@ -51,24 +52,7 @@ export async function createSession(userId: string) {
});
}

// If invoking this function from:
// - Middleware, pass token from the request header
// - Server Actions or Server Components, use `cookies()`
// - Route handler, can use either headers or cookies

export async function verifyClientSession(session: string | undefined) {
// const session = cookies().get('session')?.value;
if (!session) return null;

try {
const { userId } = await decrypt(session);
return { isAuth: true, userId };
} catch (error) {
console.log(error);
return null;
}
}

// Rabbit hole
export function updateSession() {}

export function deleteSession() {
Expand Down
17 changes: 14 additions & 3 deletions app/auth/03-dal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// authorization begins with middleware.ts, where we check for a local cookie
// Authorization begins with middleware.ts, where we check for a local cookie

import 'server-only';
import { db } from '@/drizzle/db';
Expand All @@ -22,13 +22,15 @@ export async function verifySession() {

// Option 1: Simpler version, individual functions verify the session
// This guarantees items cannot be fetched without a valid session

// In addition to middleware, we should always check for a valid session in any request that requires authentication. This guarantees if the function returns any data, then the user is allowed to access it.
// Further reading, consider using JS classes and DTO.
export const getUser = cache(async () => {
const session = await verifySession();

if (!session) return null;

try {
const user = await db.query.users.findMany({
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),

// explicitly return the columns we need rather than the whole user object. This is a good practice...reason.
Expand All @@ -38,6 +40,9 @@ export const getUser = cache(async () => {
email: true,
},
});

const user = data[0];

return user;
} catch (error) {
console.log('Failed to fetch user');
Expand All @@ -47,6 +52,10 @@ export const getUser = cache(async () => {

// Option 2: A class that holds all the requests that need to be authorized
// You may consider using a class to consolidate all the requests that need to be authorized

// blockers
// - constructors can't be async, so you can't verify the session in the constructor
// - not sure how to use react.cache with classes
export class User {
async getUser() {
const session = await verifySession();
Expand All @@ -66,4 +75,6 @@ export class User {
return null;
}
}

async getItems() {}
}
25 changes: 16 additions & 9 deletions app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ import { PackageIcon } from '@/components/ui/icons';
import { Input } from '@/components/ui/input';
import Image from 'next/image';
import Link from 'next/link';
import { getUser } from '@/app/auth/03-dal';

export default function Layout({
const navLinks = [
{ title: 'Home', href: '/dashboard', badge: 0 },
{ title: 'Orders', href: '#', badge: 3 },
{ title: 'Products', href: '#', badge: 0 },
{ title: 'Customers', href: '#', badge: 0 },
{ title: 'Analytics', href: '#', badge: 0 },
];

export default async function Layout({
children,
}: Readonly<{ children: React.ReactNode }>) {
const navLinks = [
{ title: 'Home', href: '/dashboard', badge: 0 },
{ title: 'Orders', href: '#', badge: 3 },
{ title: 'Products', href: '#', badge: 0 },
{ title: 'Customers', href: '#', badge: 0 },
{ title: 'Analytics', href: '#', badge: 0 },
];
}: {
children: React.ReactNode;
}) {
const user = await getUser();

const activeLink = '/dashboard';

return (
<div className="flex min-h-screen w-full">
<div className="hidden w-80 border-r lg:block dark:border-gray-700">
Expand Down
25 changes: 15 additions & 10 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyClientSession } from '@/app/auth/02-stateless-session';
import { decrypt } from '@/app/auth/02-stateless-session';
import { cookies } from 'next/headers';

// Client Sessions can be verified in Middleware
// Stateless sessions can be verified in Middleware
// as we're only checking for a cookie in the headers
// and there are no data requests to block the stream

Expand All @@ -10,29 +11,33 @@ import { verifyClientSession } from '@/app/auth/02-stateless-session';
// 👆 This is especially important with Next.js Route prefetching
// multiple middleware calls can be triggered on a single route change

// can use cookies() in middleware
// can't use redirect() in middleware

// 1. Specify all protected routes
const protectedRoutes = ['/dashboard'];
// 2. Specify **only** the public paths that should redirect if user is authed
const publicRoutes = ['/login', '/signup', '/'];

export default async function middleware(req: NextRequest) {
// 3. Get the token from the request
const session = req.cookies.get('session')?.value;
const { isAuth } = (await verifyClientSession(session)) || {};

// 4. Check if the current route is protected or public
// 3. Check if the current route is protected or public
const path = req.nextUrl.pathname;
const isProtectedRoute = protectedRoutes.includes(path);
const isPublicRoute = publicRoutes.includes(path);

// 5. Redirect based on the user's auth status
if (isProtectedRoute && !isAuth) {
// 4. Decrypt the session from the cookie
const cookie = cookies().get('session')?.value;
const session = await decrypt(cookie);

// 5. Redirect to login if user is not authed and tries to access a protected route
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl));
}

// 6. Redirect to dashboard if user is authed and tries to access a public route
if (
isPublicRoute &&
isAuth &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl));
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"jose": "^5.2.3",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.356.0",
"next": "14.1.3",
"next": "14.2.0-canary.34",
"prettier-plugin-tailwindcss": "^0.5.12",
"react": "^18",
"react-dom": "^18",
Expand Down
Loading

0 comments on commit 77e5d2d

Please sign in to comment.