mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
feat: migrate nextjs to rr7
This commit is contained in:
113
packages/auth/server/lib/session/session-cookies.ts
Normal file
113
packages/auth/server/lib/session/session-cookies.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import type { Context } from 'hono';
|
||||
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { useSecureCookies } from '@documenso/lib/constants/auth';
|
||||
import { appLog } from '@documenso/lib/utils/debugger';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
import { generateSessionToken } from './session';
|
||||
|
||||
export const sessionCookieName = 'sessionId';
|
||||
|
||||
const getAuthSecret = () => {
|
||||
const authSecret = env('NEXTAUTH_SECRET');
|
||||
|
||||
if (!authSecret) {
|
||||
throw new Error('NEXTAUTH_SECRET is not set');
|
||||
}
|
||||
|
||||
return authSecret;
|
||||
};
|
||||
|
||||
const getAuthDomain = () => {
|
||||
const url = new URL(NEXT_PUBLIC_WEBAPP_URL());
|
||||
|
||||
return url.hostname;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic auth session cookie options.
|
||||
*/
|
||||
export const sessionCookieOptions = {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: useSecureCookies ? 'none' : 'lax', // Todo: This feels wrong?
|
||||
secure: useSecureCookies,
|
||||
domain: getAuthDomain(),
|
||||
// Todo: Max age for specific auth cookies.
|
||||
} as const;
|
||||
|
||||
export const extractSessionCookieFromHeaders = (headers: Headers): string | null => {
|
||||
const cookieHeader = headers.get('cookie') || '';
|
||||
const cookiePairs = cookieHeader.split(';');
|
||||
const sessionCookie = cookiePairs.find((pair) => pair.trim().startsWith(sessionCookieName));
|
||||
|
||||
if (!sessionCookie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sessionCookie.split('=')[1].trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the session cookie attached to the request headers.
|
||||
*
|
||||
* @param c - The Hono context.
|
||||
* @returns The session ID or null if no session cookie is found.
|
||||
*/
|
||||
export const getSessionCookie = async (c: Context): Promise<string | null> => {
|
||||
const sessionId = await getSignedCookie(c, getAuthSecret(), sessionCookieName);
|
||||
|
||||
return sessionId || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the session cookie into the Hono context.
|
||||
*
|
||||
* @param c - The Hono context.
|
||||
* @param sessionToken - The session token to set.
|
||||
*/
|
||||
export const setSessionCookie = async (c: Context, sessionToken: string) => {
|
||||
await setSignedCookie(
|
||||
c,
|
||||
sessionCookieName,
|
||||
sessionToken,
|
||||
getAuthSecret(),
|
||||
sessionCookieOptions,
|
||||
).catch((err) => {
|
||||
appLog('SetSessionCookie', `Error setting signed cookie: ${err}`);
|
||||
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the session cookie into the Hono context.
|
||||
*
|
||||
* @param c - The Hono context.
|
||||
* @param sessionToken - The session token to set.
|
||||
*/
|
||||
export const deleteSessionCookie = (c: Context) => {
|
||||
deleteCookie(c, sessionCookieName, sessionCookieOptions);
|
||||
};
|
||||
|
||||
export const getCsrfCookie = async (c: Context) => {
|
||||
const csrfToken = await getSignedCookie(c, getAuthSecret(), 'csrfToken');
|
||||
|
||||
return csrfToken || null;
|
||||
};
|
||||
|
||||
export const setCsrfCookie = async (c: Context) => {
|
||||
const csrfToken = generateSessionToken();
|
||||
|
||||
await setSignedCookie(c, 'csrfToken', csrfToken, getAuthSecret(), {
|
||||
...sessionCookieOptions,
|
||||
|
||||
// Explicity set to undefined for session lived cookie.
|
||||
expires: undefined,
|
||||
maxAge: undefined,
|
||||
});
|
||||
|
||||
return csrfToken;
|
||||
};
|
||||
146
packages/auth/server/lib/session/session.ts
Normal file
146
packages/auth/server/lib/session/session.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
/**
|
||||
* The user object to pass around the app.
|
||||
*
|
||||
* Do not put anything sensitive in here since it will be public.
|
||||
*/
|
||||
export type SessionUser = Pick<
|
||||
User,
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'email'
|
||||
| 'emailVerified'
|
||||
| 'avatarImageId'
|
||||
| 'twoFactorEnabled'
|
||||
| 'roles'
|
||||
| 'signature'
|
||||
| 'url'
|
||||
>;
|
||||
|
||||
export type SessionValidationResult =
|
||||
| {
|
||||
session: Session;
|
||||
user: SessionUser;
|
||||
isAuthenticated: true;
|
||||
}
|
||||
| { session: null; user: null; isAuthenticated: false };
|
||||
|
||||
export const generateSessionToken = (): string => {
|
||||
const bytes = new Uint8Array(20);
|
||||
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
const token = encodeBase32LowerCaseNoPadding(bytes);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export const createSession = async (
|
||||
token: string,
|
||||
userId: number,
|
||||
metadata: RequestMetadata,
|
||||
): Promise<Session> => {
|
||||
const hashedSessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
|
||||
const session: Session = {
|
||||
id: hashedSessionId,
|
||||
sessionToken: hashedSessionId,
|
||||
userId,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
|
||||
ipAddress: metadata.ipAddress ?? null,
|
||||
userAgent: metadata.userAgent ?? null,
|
||||
};
|
||||
|
||||
await prisma.session.create({
|
||||
data: session,
|
||||
});
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
export const validateSessionToken = async (token: string): Promise<SessionValidationResult> => {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
|
||||
const result = await prisma.session.findUnique({
|
||||
where: {
|
||||
id: sessionId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
/**
|
||||
* Do not expose anything sensitive here.
|
||||
*/
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
avatarImageId: true,
|
||||
twoFactorEnabled: true,
|
||||
roles: true,
|
||||
signature: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!result?.user) {
|
||||
return { session: null, user: null, isAuthenticated: false };
|
||||
}
|
||||
|
||||
const { user, ...session } = result;
|
||||
|
||||
if (Date.now() >= session.expiresAt.getTime()) {
|
||||
await prisma.session.delete({ where: { id: sessionId } });
|
||||
return { session: null, user: null, isAuthenticated: false };
|
||||
}
|
||||
|
||||
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
|
||||
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
|
||||
|
||||
await prisma.session.update({
|
||||
where: {
|
||||
id: session.id,
|
||||
},
|
||||
data: {
|
||||
expiresAt: session.expiresAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { session, user, isAuthenticated: true };
|
||||
};
|
||||
|
||||
export const invalidateSession = async (
|
||||
sessionId: string,
|
||||
metadata: RequestMetadata,
|
||||
): Promise<void> => {
|
||||
const session = await prisma.session.delete({ where: { id: sessionId } });
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: session.userId,
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_OUT,
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user