import type { Decoded } from '@redwoodjs/api' import { decryptSession, getSession } from '@redwoodjs/auth-dbauth-api' import { AuthenticationError, ForbiddenError, ValidationError, } from '@redwoodjs/graphql-server' import { db } from 'src/lib/db' /** * The name of the cookie that dbAuth sets * * %port% will be replaced with the port the api server is running on. * If you have multiple RW apps running on the same host, you'll need to * make sure they all use unique cookie names */ export const cookieName = 'session_%port%' const cookieRegex = /([a-zA-Z0-9+/|=]{110})\w+/ const tokenRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i /** * The session object sent in as the first argument to getCurrentUser() will * have a single key `id` containing the unique ID of the logged in user * (whatever field you set as `authFields.id` in your auth function config). * You'll need to update the call to `db` below if you use a different model * name or unique field name, for example: * * return await db.profile.findUnique({ where: { email: session.id } }) * ───┬─── ──┬── * model accessor ─┘ unique id field name ─┘ * * !! BEWARE !! Anything returned from this function will be available to the * client--it becomes the content of `currentUser` on the web side (as well as * `context.currentUser` on the api side). You should carefully add additional * fields to the `select` object below once you've decided they are safe to be * seen if someone were to open the Web Inspector in their browser. */ export const getCurrentUser = async (session: Decoded) => { if (!session || typeof session.id !== 'number') throw new Error('Invalid session') return await db.user.findUnique({ where: { id: session.id }, select: { id: true }, }) } /** * The user is authenticated if there is a currentUser in the context * * @returns {boolean} - If the currentUser is authenticated */ export const isAuthenticated = (): boolean => { return !!context.currentUser } /** * When checking role membership, roles can be a single value, a list, or none. * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client` */ type AllowedRoles = string | string[] | undefined /** * Checks if the currentUser is authenticated (and assigned one of the given roles) * * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles * * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles, * or when no roles are provided to check against. Otherwise returns false. */ export const hasRole = (roles: AllowedRoles): boolean => { if (!isAuthenticated()) return false const currentUserRoles = context.currentUser?.roles if (typeof roles === 'string') { // roles to check is a string, currentUser.roles is a string if (typeof currentUserRoles === 'string') return currentUserRoles === roles else if (Array.isArray(currentUserRoles)) // roles to check is a string, currentUser.roles is an array return currentUserRoles?.some((allowedRole) => roles === allowedRole) } if (Array.isArray(roles)) { if (Array.isArray(currentUserRoles)) { // roles to check is an array, currentUser.roles is an array return currentUserRoles?.some((allowedRole) => roles.includes(allowedRole) ) } else if (typeof currentUserRoles === 'string') { // roles to check is an array, currentUser.roles is a string return roles.some((allowedRole) => currentUserRoles === allowedRole) } } // roles not found return false } /** * Use requireAuth in your services to check that a user is logged in, * whether or not they are assigned a role, and optionally raise an * error if they're not. * * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access. * * @returns - If the currentUser is authenticated (and assigned one of the given roles) * * @throws {@link AuthenticationError} - If the currentUser is not authenticated * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions * * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples */ export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => { if (!isAuthenticated()) throw new AuthenticationError("You don't have permission to do that.") if (roles && !hasRole(roles)) throw new ForbiddenError("You don't have access to do that.") } export const validateSessionCookie = (sessionCookie: string) => { const sessionCookieContent = sessionCookie.substring( sessionCookie.indexOf('=') + 1 ) if (!cookieRegex.test(sessionCookieContent)) throw new ValidationError('Invalid token format') } export const decryptAndValidateSession = (sessionCookie: string) => { const cookie = cookieName.replace('%port%', '8911') const [session, csrfToken] = decryptSession(getSession(sessionCookie, cookie)) if (!session.id) throw new ValidationError('Invalid session') if (!tokenRegex.test(csrfToken)) throw new ValidationError('Invalid session format') return session.id }