@accounter/server
Version:
114 lines (96 loc) • 2.86 kB
text/typescript
import auth from 'basic-auth';
import bcrypt from 'bcrypt';
import { GraphQLError } from 'graphql';
import { ResolveUserFn, useGenericAuth, ValidateUserFn } from '@envelop/generic-auth';
import type { Role } from '@shared/gql-types';
import { AccounterContext } from '@shared/types';
import { env } from '../environment.js';
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: ReturnType<typeof auth>) {
if (!user) {
return false;
}
const { name, pass } = user;
const storedPass = authorizedUsers[name] ?? '';
return bcrypt.compareSync(pass, storedPass);
}
function getUserRole(user: ReturnType<typeof auth>): Role | undefined {
const validate = validateRequestUser(user);
if (!validate) {
return undefined;
}
switch (user!.name) {
case 'accountant':
return 'ACCOUNTANT';
case 'admin':
return 'ADMIN';
default:
return undefined;
}
}
const resolveUserFn: ResolveUserFn<UserType, AccounterContext> = async context => {
try {
const user = getUserFromRequest(context.request);
const role = getUserRole(user);
if (!user || !role) {
throw new Error('User not valid');
}
return {
username: user.name,
userId: env.authorization.adminBusinessId, // TODO: replace with actual authentication
role,
};
} catch (e) {
console.error('Failed to validate token', e);
return null;
}
};
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 = () =>
useGenericAuth({
resolveUserFn,
validateUser,
mode: 'protect-granular',
extractScopes: user => getAcceptableRoles(user?.role),
});