Files
portfolio/api/src/lib/auth.ts
Ahmed Al-Taiar 0283c293ef
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 39s
An attempt to fix the PDF iframe not loading properly when the API domain is third-partyBasic printer CRUD
2025-04-06 18:08:05 -04:00

144 lines
5.2 KiB
TypeScript
Executable File

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
}