Skip to content

Commit

Permalink
feat(auth): use Lucia-auth
Browse files Browse the repository at this point in the history
  • Loading branch information
ntatoud authored and ivan-dalmet committed Jul 22, 2024
1 parent 27a2a59 commit 01ab506
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 50 deletions.
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ NEXT_PUBLIC_IS_DEMO="false"
# DATABASE
DATABASE_URL="postgres://${DOCKER_DATABASE_USERNAME}:${DOCKER_DATABASE_PASSWORD}@localhost:${DOCKER_DATABASE_PORT}/${DOCKER_DATABASE_NAME}"

# AUTH
AUTH_SECRET="Replace me with `openssl rand -base64 32` generated secret"

# EMAILS
EMAIL_SERVER="smtp://username:[email protected]:1025"
Expand Down
4 changes: 3 additions & 1 deletion prisma/schema/user.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ model User {
authorizations UserRole[] @default([APP])
language String @default("en")
lastLoginAt DateTime?
Session Session[]
// Relations
Session Session[]
}

model Session {
Expand Down
3 changes: 0 additions & 3 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export const env = createEnv({
DATABASE_URL: z.string().url(),
NODE_ENV: zNodeEnv,

AUTH_SECRET: z.string(),

EMAIL_SERVER: z.string().url(),
EMAIL_FROM: z.string(),
LOGGER_LEVEL: z
Expand Down Expand Up @@ -75,7 +73,6 @@ export const env = createEnv({
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
AUTH_SECRET: process.env.AUTH_SECRET,
EMAIL_FROM: process.env.EMAIL_FROM,
EMAIL_SERVER: process.env.EMAIL_SERVER,
LOGGER_LEVEL: process.env.LOGGER_LEVEL,
Expand Down
38 changes: 19 additions & 19 deletions src/server/config/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { cache } from 'react';

import { VerificationToken } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import bcrypt from 'bcrypt';
import dayjs from 'dayjs';
import { cookies } from 'next/headers';
import { cookies, headers } from 'next/headers';
import { randomInt } from 'node:crypto';

import { env } from '@/env.mjs';
Expand All @@ -14,14 +16,13 @@ import { AppContext } from '@/server/config/trpc';

import { lucia } from './lucia';

export const AUTH_COOKIE_NAME = 'auth';

/**
* getServerAuthSession
*/
export const getServerAuthSession = async () => {
export const getServerAuthSession = cache(async () => {
const sessionId =
lucia.readBearerToken('Authorizations') ??
headers().get('Authorization')?.split('Bearer ')[1] ??
// Get Session from cookies
cookies().get(lucia.sessionCookieName)?.value;

if (!sessionId)
Expand Down Expand Up @@ -56,17 +57,7 @@ export const getServerAuthSession = async () => {
user,
session,
};
};

export const setAuthCookie = (token: string) => {
cookies().set({
name: AUTH_COOKIE_NAME,
value: token,
httpOnly: true,
secure: env.NODE_ENV === 'production',
expires: dayjs().add(1, 'year').toDate(),
});
};
});

export async function generateCode() {
const code =
Expand Down Expand Up @@ -174,9 +165,16 @@ export async function deleteUsedCode({
}

export async function createSession(userId: string) {
const session = await lucia.createSession(userId, {
expiresAt: dayjs().add(1, 'year').toDate(),
});
const session = await lucia.createSession(
userId,
{
// Possible to pass custom session attributes defined when declaring the Lucia instance
},
{
// Possible to pass custom sessionId but otherwise it will be generated
// sessionId: CUSTOM_SESSION_ID,
}
);

const sessionCookie = lucia.createSessionCookie(session.id);

Expand All @@ -185,6 +183,8 @@ export async function createSession(userId: string) {
sessionCookie.value,
sessionCookie.attributes
);

return session.id;
}

export async function deleteSession(sessionId: string) {
Expand Down
7 changes: 6 additions & 1 deletion src/server/config/lucia.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PrismaAdapter } from '@lucia-auth/adapter-prisma';
import { Lucia } from 'lucia';
import { Lucia, TimeSpan } from 'lucia';

import { env } from '@/env.mjs';
import { UserAccount } from '@/features/account/schemas';
Expand All @@ -9,12 +9,17 @@ import { db } from './db';

const adapter = new PrismaAdapter(db.session, db.user); // your adapter

export const AUTH_COOKIE_NAME = 'auth';

export const lucia = new Lucia(adapter, {
sessionExpiresIn: new TimeSpan(4, 'w'),
sessionCookie: {
name: AUTH_COOKIE_NAME,
// this sets cookies with super long expiration
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
expires: false,
attributes: {
sameSite: 'strict',
// set to `true` when using HTTPS
secure: env.NODE_ENV === 'production',
},
Expand Down
40 changes: 16 additions & 24 deletions src/server/routers/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TRPCError } from '@trpc/server';
import dayjs from 'dayjs';
import { cookies } from 'next/headers';
import { randomUUID } from 'node:crypto';
import { z } from 'zod';

Expand All @@ -14,21 +13,15 @@ import {
import { zUser, zUserAuthorization } from '@/features/users/schemas';
import i18n from '@/lib/i18n/server';
import {
AUTH_COOKIE_NAME,
createSession,
deleteSession,
deleteUsedCode,
generateCode,
setAuthCookie,
validateCode,
} from '@/server/config/auth';
import { sendEmail } from '@/server/config/email';
import { ExtendedTRPCError } from '@/server/config/errors';
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from '@/server/config/trpc';
import { createTRPCRouter, publicProcedure } from '@/server/config/trpc';

export const authRouter = createTRPCRouter({
checkAuthenticated: publicProcedure()
Expand All @@ -49,13 +42,6 @@ export const authRouter = createTRPCRouter({
.query(async ({ ctx }) => {
ctx.logger.info(`User ${ctx.user ? 'is' : 'is not'} logged`);

if (ctx.user) {
const cookieToken = cookies().get(AUTH_COOKIE_NAME)?.value;
if (cookieToken) {
setAuthCookie(cookieToken);
}
}

return {
isAuthenticated: !!ctx.user,
authorizations: ctx.user?.authorizations,
Expand Down Expand Up @@ -152,7 +138,7 @@ export const authRouter = createTRPCRouter({
},
})
.input(z.object({ code: z.string().length(6), token: z.string().uuid() }))
.output(z.object({ id: z.string() }))
.output(z.object({ token: z.string() }))
.mutation(async ({ ctx, input }) => {
const { verificationToken } = await validateCode({
ctx,
Expand All @@ -177,15 +163,15 @@ export const authRouter = createTRPCRouter({

await deleteUsedCode({ ctx, token: verificationToken.token });

ctx.logger.info('Set auth cookie');
await createSession(verificationToken.userId);
ctx.logger.info('Create the session');
const sessionId = await createSession(verificationToken.userId);

return {
id: verificationToken.userId,
token: sessionId,
};
}),

logout: protectedProcedure()
logout: publicProcedure()
.meta({
openapi: {
method: 'POST',
Expand All @@ -197,6 +183,12 @@ export const authRouter = createTRPCRouter({
.output(z.void())
.mutation(async ({ ctx }) => {
ctx.logger.info('Delete auth cookie');

if (!ctx.session) {
ctx.logger.warn('No session found');
return;
}

deleteSession(ctx.session.id);
}),

Expand Down Expand Up @@ -317,7 +309,7 @@ export const authRouter = createTRPCRouter({
},
})
.input(z.object({ code: z.string().length(6), token: z.string().uuid() }))
.output(z.object({ id: z.string() }))
.output(z.object({ token: z.string() }))
.mutation(async ({ ctx, input }) => {
const { verificationToken } = await validateCode({
ctx,
Expand Down Expand Up @@ -346,11 +338,11 @@ export const authRouter = createTRPCRouter({

await deleteUsedCode({ ctx, token: verificationToken.token });

ctx.logger.info('Set auth cookie');
await createSession(verificationToken.userId);
ctx.logger.info('Create the session');
const sessionId = await createSession(verificationToken.userId);

return {
id: verificationToken.userId,
token: sessionId,
};
}),
});

0 comments on commit 01ab506

Please sign in to comment.