UNPKG

@accounter/server

Version:

The test suite is split into three Vitest projects for efficiency and isolation:

154 lines (130 loc) 3.95 kB
import auth, { type BasicAuthResult } from 'basic-auth'; import bcrypt from 'bcrypt'; import { GraphQLError } from 'graphql'; import pg from 'pg'; import { ResolveUserFn, useGenericAuth, ValidateUserFn } from '@envelop/generic-auth'; import { sql } from '@pgtyped/runtime'; import type { Role } from '../__generated__/types.js'; import { env } from '../environment.js'; import { getCacheInstance } from '../shared/helpers/index.js'; import { AccounterContext } from '../shared/types/index.js'; import type { IGetUserByNameQuery, IGetUserByNameResult, } from './__generated__/auth-plugin.types.js'; const getUserByName = sql<IGetUserByNameQuery>` SELECT * FROM accounter_schema.legacy_business_users WHERE name = $userName`; const cache = getCacheInstance({ stdTTL: 60, }); export type UserType = { username: string; userId: string; role?: Role; }; function getAuthorizedUsers(): Record<string, string> { try { return JSON.parse(env.authorization.users ?? '{}'); } catch (e) { console.error('Failed to read authorized users from env file.', e); return {}; } } const authorizedUsers = getAuthorizedUsers(); function getUserFromRequest(request: Request) { const authorization = request.headers?.get('authorization') ?? undefined; return auth({ headers: { authorization } }); } function validateRequestUser(user: BasicAuthResult) { const { name, pass } = user; const storedPass = authorizedUsers[name] ?? ''; return bcrypt.compareSync(pass, storedPass); } async function getUserInfo( user: BasicAuthResult, pool: pg.Pool, ): Promise<{ role: Role; adminBusinessId: string } | undefined> { const validate = validateRequestUser(user); if (!validate) { return undefined; } const userName = user.name; try { let user = cache.get<IGetUserByNameResult>(userName); if (!user) { const userRes = await getUserByName.run({ userName }, pool); if (userRes.length === 1) { cache.set(userName, userRes[0]); user = userRes[0]; } } if (!user) { throw new Error('User not found'); } if (!user.role || !user.id) { return undefined; } return { role: user.role as Role, adminBusinessId: user.id, }; } catch (error) { console.error('Error fetching user:', error); throw new Error('Error fetching user info'); } } const getAcceptableRoles = (role?: string) => { switch (role) { case 'ADMIN': return ['ADMIN']; case 'ACCOUNTANT': return ['ACCOUNTANT', 'ADMIN']; default: return []; } }; const validateUser: ValidateUserFn<UserType> = ({ user, fieldDirectives, parentType }) => { if (!user) { return new GraphQLError(`Unauthenticated!`); } // case sub-field with no auth directive if (!['Query', 'Mutation'].includes(parentType.name) && !fieldDirectives?.auth) { return; } const role = fieldDirectives?.auth?.find(arg => 'role' in arg)?.role; const acceptableRoles = getAcceptableRoles(role); if (user.role && acceptableRoles.includes(user.role)) { return; } return new GraphQLError(`No permissions!`); }; export const authPlugin = () => { const resolveUserFnWithPool: ResolveUserFn<UserType, AccounterContext> = async context => { try { const user = getUserFromRequest(context.request); if (!user) { throw new Error('User not valid'); } const userInfo = await getUserInfo(user, context.pool); if (!userInfo) { throw new Error('User role not valid'); } return { username: user.name, userId: userInfo.adminBusinessId, role: userInfo.role, }; } catch (e) { console.error('Failed to validate token', e); return null; } }; return useGenericAuth({ resolveUserFn: resolveUserFnWithPool, validateUser, mode: 'protect-granular', extractScopes: user => getAcceptableRoles(user?.role), }); };