UNPKG

@drop-in/new

Version:

A SvelteKit Svelte 5 PocketBase Starter Kit with a CLI

265 lines (236 loc) 7.32 kB
import { cookie_options } from '$lib/const'; import type { Cookies } from '@sveltejs/kit'; import bcrypt from 'bcryptjs'; import crypto, { randomBytes } from 'crypto'; import { and, eq } from 'drizzle-orm'; import { db } from '$src/hooks.server'; import { user as schema_user, session, type User } from '$src/schema'; import { create_tokens, decode_access_token } from './jwt'; const { compare, genSalt, hash } = bcrypt; /** * Authenticates a user based on their access and refresh tokens stored in cookies. * * @param cookies - The cookies object from the request * @returns An object containing the authenticated user (if any) and tokens */ export async function authenticate_user(cookies: Cookies) { // Start performance measurement const start = performance.now(); // Extract tokens from cookies const access_token = cookies.get('accessToken'); const refresh_token = cookies.get('refreshToken'); let user: Partial<User> | null = null; let new_access_token = null; let new_refresh_token = null; // First, try to authenticate with the access token if (access_token) { try { // Decode the access token const decoded = decode_access_token(access_token); // Fetch user and their active session in one query user = await get_user_with_session(parseInt(decoded.user_id), decoded.token); if (user) { // If user is found, use the existing tokens new_access_token = access_token; new_refresh_token = refresh_token; } } catch (error) { console.error('Access token validation failed:', error); // Token validation failed, will fall through to refresh token } } // If access token failed, try to authenticate with the refresh token if (!user && refresh_token) { try { // Decode the refresh token const decoded = decode_access_token(refresh_token); // Fetch user by the session token stored in the refresh token user = await get_user_by_refresh_token(decoded.token); if (user?.id) { // If user is found, create new tokens const tokens = create_tokens({ token: decoded.token, user_id: user.id, }); new_access_token = tokens.accessToken; new_refresh_token = tokens.refreshToken; } } catch (error) { // console.error('Refresh token validation failed:', error); // Both token validations failed, user will remain null } } // Log performance metrics const end = performance.now(); console.log(`Authentication took ${end - start} milliseconds`); // Return the authentication result return { user, access_token: new_access_token, refresh_token: new_refresh_token, }; } /** * Fetches a user and their active session in a single query. * * @param user_id - The ID of the user to fetch * @param session_token - The session token to validate * @returns The user object if found with a valid session, null otherwise */ async function get_user_with_session( user_id: number, session_token: string, ): Promise<Partial<User> | null> { const result = await db.query.session.findFirst({ where: and(eq(session.user_id, user_id), eq(session.session_token, session_token)), columns: { user_id: true, }, with: { user: { columns: { id: true, email: true, verified: true, updated_at: true, }, }, }, }); return result?.user || null; } /** * Fetches a user by their refresh token's session. * * @param session_token - The session token stored in the refresh token * @returns The user object if found with a valid session, null otherwise */ async function get_user_by_refresh_token(session_token: string): Promise<Partial<User> | null> { const result = await db.query.session.findFirst({ where: eq(session.session_token, session_token), columns: { user_id: true, }, with: { user: { columns: { id: true, email: true, verified: true, updated_at: true, }, }, }, }); return result?.user || null; } // Given a 'password' from the client, extract the string that we should // bcrypt. 'password' can be one of: // - String (the plaintext password) // - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256". export const getPasswordString = (password: string): string => { const passwordHash = crypto.createHash('sha256').update(password).digest('hex'); return passwordHash; }; export const bcryptPassword = async (password: string): Promise<string> => { const salt = await genSalt(10); const hashedPassword = await hash(password, salt); return hashedPassword; }; /** * @deprecated new auth service */ export const normalizeEmail = (email: string): string => { return decodeURIComponent(email).toLowerCase().trim(); }; // Compares password to hash to confirm they are the same export async function verifyPassword(password: string, hash: string): Promise<boolean> { const compared: boolean = await compare(password, hash); return compared; } /** * @deprecated new auth service */ export const generateRandomToken = (length = 43): string => randomBytes(length).toString('hex'); // Creates a session token & a valid Session // Creates access and refresh token export async function log_user_in({ user_id, cookies }: { user_id: number; cookies: Cookies }) { // Create Session Token const session_token = createSessionToken(); // Creates a session in the database const session_data = await db .insert(session) .values({ user_id, session_token, }) .returning(); // Create access and refresh tokens // These tokens contain the session token as well as the // accessToken has the userId const { accessToken, refreshToken } = create_tokens({ token: session_token, user_id, }); // Sets cookies for the response in the browser setAuthCookies({ accessToken, refreshToken, cookies }); return { sessionId: session_data[0].id, tokens: { refreshToken, accessToken, }, user_id, }; } export function setAuthCookies({ accessToken, refreshToken, cookies, }: { accessToken: string; refreshToken: string; cookies: Cookies; }) { cookies.set('accessToken', accessToken, cookie_options); cookies.set('refreshToken', refreshToken, cookie_options); } // Generates a token to be saved to the database to identify a session export function createSessionToken() { return generateRandomToken(); } /** * @deprecated new auth service */ export async function login_with_password(password: string, email: string) { // Finds user by email const user = await db.query.user.findFirst({ where: eq(schema_user.email, email), }); if (user) { // Compares password hash to one in database const compared: boolean = await authenticate_user_password(password, user?.hashed_password); if (compared) { return user; } } } // Takes a user and a pw string and gives you a boolean if // the password matches the one in the database export async function authenticate_user_password( password: string, user_pw: string, ): Promise<boolean> { // Gets password hash from plain text password const formattedPassword = getPasswordString(password); if (!user_pw) return false; if (user_pw) { // Compares password hash to one in database const compared: boolean = await verifyPassword(formattedPassword, user_pw); if (compared) { // Returns boolean if the user password is accurate return compared; } } return false; }