All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 39s
144 lines
5.2 KiB
TypeScript
Executable File
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
|
|
}
|