@sqrzro/server 2.0.0-bz.8 → 2.0.0-bz.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/auth/AuthService.d.ts +14 -0
  2. package/dist/auth/AuthService.js +135 -0
  3. package/dist/auth/ClientService.d.ts +11 -0
  4. package/dist/auth/ClientService.js +47 -0
  5. package/dist/auth/LoginRequest.d.ts +4 -0
  6. package/dist/auth/LoginRequest.js +8 -0
  7. package/dist/auth/MFARequest.d.ts +4 -0
  8. package/dist/auth/MFARequest.js +9 -0
  9. package/dist/auth/MFAService.d.ts +6 -0
  10. package/dist/auth/MFAService.js +105 -0
  11. package/dist/auth/PasswordRequest.d.ts +4 -0
  12. package/dist/auth/PasswordRequest.js +12 -0
  13. package/dist/auth/PasswordResetRequest.d.ts +4 -0
  14. package/dist/auth/PasswordResetRequest.js +13 -0
  15. package/dist/auth/PasswordService.d.ts +8 -0
  16. package/dist/auth/PasswordService.js +54 -0
  17. package/dist/auth/SessionService.d.ts +42 -0
  18. package/dist/auth/SessionService.js +127 -0
  19. package/dist/auth/index.d.ts +6 -0
  20. package/dist/auth/index.js +6 -0
  21. package/dist/auth/interfaces.d.ts +20 -0
  22. package/dist/auth/interfaces.js +1 -0
  23. package/dist/cache/CacheService.d.ts +2 -0
  24. package/dist/cache/CacheService.js +14 -0
  25. package/dist/cache/index.d.ts +1 -0
  26. package/dist/cache/index.js +1 -0
  27. package/dist/database/DatabaseService.d.ts +7 -0
  28. package/dist/database/DatabaseService.js +12 -0
  29. package/dist/database/schema.d.ts +284 -0
  30. package/dist/database/schema.js +42 -0
  31. package/dist/forms/FormService.d.ts +16 -0
  32. package/dist/forms/FormService.js +78 -0
  33. package/dist/forms/ImageService.d.ts +7 -0
  34. package/dist/forms/ImageService.js +19 -0
  35. package/dist/forms/ValidationError.d.ts +4 -0
  36. package/dist/forms/ValidationError.js +7 -0
  37. package/dist/forms/ValidationService.d.ts +20 -0
  38. package/dist/forms/ValidationService.js +59 -0
  39. package/dist/forms/index.d.ts +3 -0
  40. package/dist/forms/index.js +3 -0
  41. package/dist/forms/lang.d.ts +2 -0
  42. package/dist/forms/lang.js +115 -0
  43. package/dist/lists/ListService.d.ts +16 -0
  44. package/dist/lists/ListService.js +28 -0
  45. package/dist/lists/index.d.ts +1 -0
  46. package/dist/lists/index.js +1 -0
  47. package/dist/mail/MailService.d.ts +12 -0
  48. package/dist/mail/MailService.js +55 -0
  49. package/dist/mail/index.d.ts +1 -0
  50. package/dist/mail/index.js +1 -0
  51. package/dist/middleware.d.ts +3 -0
  52. package/dist/middleware.js +30 -0
  53. package/dist/url/URLService.d.ts +26 -0
  54. package/dist/url/URLService.js +48 -0
  55. package/dist/url/index.d.ts +1 -0
  56. package/dist/url/index.js +1 -0
  57. package/package.json +2 -1
@@ -0,0 +1,14 @@
1
+ import type { Errorable } from '@sqrzro/interfaces';
2
+ import type { LoginFormFields, PasswordFormFields, PasswordResetFormFields, UserObject } from './interfaces';
3
+ export declare function handleLogout(): Promise<void>;
4
+ export declare function getAllowedRoles(): number[];
5
+ export declare function handleLoginForm(formData: LoginFormFields): Promise<Errorable<string>>;
6
+ interface RegisterUserArgs {
7
+ email: string;
8
+ password?: string;
9
+ role?: number;
10
+ }
11
+ export declare function registerUser({ email, password, role, }: RegisterUserArgs): Promise<UserObject | null>;
12
+ export declare function handlePasswordForm(formData: PasswordFormFields, mailFn: (email: string, token: string) => Promise<boolean>): Promise<Errorable<boolean>>;
13
+ export declare function handlePasswordResetForm(formData: PasswordResetFormFields): Promise<Errorable<string | null>>;
14
+ export {};
@@ -0,0 +1,135 @@
1
+ import { and, eq, inArray } from 'drizzle-orm';
2
+ import { db } from '../database/DatabaseService';
3
+ import { authResetTable, authUserTable } from '../database/schema';
4
+ import { submitForm } from '../forms/FormService';
5
+ import ValidationError from '../forms/ValidationError';
6
+ import { checkMFAEnabled } from './MFAService';
7
+ import { hashPassword, verifyPassword } from './PasswordService';
8
+ import { createUserSession, generateID, getScopeByID, getSessionID, invalidateSession, invalidateUserSessions, } from './SessionService';
9
+ import LoginRequest from './LoginRequest';
10
+ import PasswordRequest from './PasswordRequest';
11
+ import PasswordResetRequest from './PasswordResetRequest';
12
+ const RESET_TOKEN_LENGTH = 40;
13
+ // Set expiry to 1 hour (in ms)
14
+ const RESET_TOKEN_EXPIRY = 3600000;
15
+ export async function handleLogout() {
16
+ const id = getSessionID();
17
+ if (id) {
18
+ await invalidateSession(id);
19
+ }
20
+ }
21
+ export function getAllowedRoles() {
22
+ const roles = process.env.AUTH_ALLOWED_ROLES;
23
+ if (!roles) {
24
+ throw new Error('AUTH_ALLOWED_ROLES is not defined. Authentication will not be possible.');
25
+ }
26
+ return roles
27
+ .split(',')
28
+ .map((role) => Number(role))
29
+ .filter((role) => !isNaN(role));
30
+ }
31
+ async function handleUserSession(userID) {
32
+ await createUserSession(userID, checkMFAEnabled() ? 'MFA' : 'AUTHED');
33
+ const scope = await getScopeByID('AUTHED');
34
+ return scope?.redirectOnAuth || null;
35
+ }
36
+ async function getUserByEmail(email) {
37
+ const [user] = await db
38
+ .select()
39
+ .from(authUserTable)
40
+ .where(and(eq(authUserTable.email, email), inArray(authUserTable.role, getAllowedRoles())))
41
+ .limit(1);
42
+ return user;
43
+ }
44
+ async function loginUser({ email, password }) {
45
+ const user = await getUserByEmail(email);
46
+ if (!user?.password || !(await verifyPassword(password, user.password))) {
47
+ throw new ValidationError({ email: '', password: '' });
48
+ }
49
+ const session = await handleUserSession(user.id);
50
+ if (!session) {
51
+ throw new ValidationError({ email: '', password: '' });
52
+ }
53
+ return session;
54
+ }
55
+ export async function handleLoginForm(formData) {
56
+ const response = await submitForm({
57
+ fn: loginUser,
58
+ formData,
59
+ request: LoginRequest,
60
+ });
61
+ return response;
62
+ }
63
+ export async function registerUser({ email, password, role, }) {
64
+ const hash = password ? await hashPassword(password) : null;
65
+ const [user] = await db
66
+ .insert(authUserTable)
67
+ .values({ id: generateID(), email, password: hash, role })
68
+ .returning();
69
+ return user;
70
+ }
71
+ async function createPasswordResetToken(email) {
72
+ const user = await getUserByEmail(email);
73
+ if (!user) {
74
+ return null;
75
+ }
76
+ await db.delete(authResetTable).where(eq(authResetTable.userId, user.id));
77
+ const id = generateID(RESET_TOKEN_LENGTH);
78
+ await db.insert(authResetTable).values({
79
+ id,
80
+ userId: user.id,
81
+ expiresAt: new Date(new Date().getTime() + RESET_TOKEN_EXPIRY),
82
+ });
83
+ return id;
84
+ }
85
+ export async function handlePasswordForm(formData, mailFn) {
86
+ async function mutateFn(data) {
87
+ const token = await createPasswordResetToken(data.email);
88
+ if (!token) {
89
+ // Return true, even though the email doesn't exist (to prevent user enumeration)
90
+ return true;
91
+ }
92
+ return mailFn(data.email, token);
93
+ }
94
+ const response = await submitForm({
95
+ fn: mutateFn,
96
+ formData,
97
+ request: PasswordRequest,
98
+ });
99
+ return response;
100
+ }
101
+ async function validatePasswordResetToken(password, token) {
102
+ const [result] = await db
103
+ .select()
104
+ .from(authResetTable)
105
+ .where(eq(authResetTable.id, token))
106
+ .limit(1);
107
+ if (!result) {
108
+ return null;
109
+ }
110
+ await db.delete(authResetTable).where(eq(authResetTable.id, token));
111
+ if (!result || result.expiresAt < new Date()) {
112
+ return null;
113
+ }
114
+ await invalidateUserSessions(result.userId);
115
+ await db
116
+ .update(authUserTable)
117
+ .set({ password: await hashPassword(password) })
118
+ .where(and(eq(authUserTable.id, result.userId), inArray(authUserTable.role, getAllowedRoles())));
119
+ return result.userId;
120
+ }
121
+ export async function handlePasswordResetForm(formData) {
122
+ async function mutateFn(data) {
123
+ const userID = await validatePasswordResetToken(data.password, data.token);
124
+ if (!userID) {
125
+ return null;
126
+ }
127
+ return handleUserSession(userID);
128
+ }
129
+ const response = await submitForm({
130
+ fn: mutateFn,
131
+ formData,
132
+ request: PasswordResetRequest,
133
+ });
134
+ return response;
135
+ }
@@ -0,0 +1,11 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import type { AuthClient } from '../database/schema';
3
+ export declare function getClientByID(id: string): Promise<AuthClient | null>;
4
+ interface RegisterClientArgs {
5
+ alias: string;
6
+ id?: string;
7
+ secret?: string;
8
+ }
9
+ export declare function registerClient({ alias, id, secret, }: RegisterClientArgs): Promise<AuthClient | null>;
10
+ export declare function handleClientAuth(request: NextRequest): Promise<AuthClient | null>;
11
+ export {};
@@ -0,0 +1,47 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { db } from '../database/DatabaseService';
3
+ import { authClientTable } from '../database/schema';
4
+ import { hashPassword, verifyPassword } from './PasswordService';
5
+ import { generateID } from './SessionService';
6
+ const ID_LENGTH = 16;
7
+ const SECRET_LENGTH = 64;
8
+ export async function getClientByID(id) {
9
+ const [client] = await db
10
+ .select()
11
+ .from(authClientTable)
12
+ .where(eq(authClientTable.id, id))
13
+ .limit(1);
14
+ return client;
15
+ }
16
+ export async function registerClient({ alias, id, secret, }) {
17
+ const [client] = await db
18
+ .insert(authClientTable)
19
+ .values({
20
+ alias,
21
+ id: id || generateID(ID_LENGTH),
22
+ secret: await hashPassword(secret || generateID(SECRET_LENGTH)),
23
+ })
24
+ .returning();
25
+ return client;
26
+ }
27
+ export async function handleClientAuth(request) {
28
+ const { headers } = request;
29
+ const header = headers.get('authorization');
30
+ if (!header) {
31
+ return null;
32
+ }
33
+ const auth = Buffer.from(header.replace('Basic ', ''), 'base64')
34
+ .toString('utf-8')
35
+ .replace(/:$/u, '');
36
+ const [id, ...secret] = auth.split('-');
37
+ const [client] = await db
38
+ .select()
39
+ .from(authClientTable)
40
+ .where(eq(authClientTable.id, id))
41
+ .limit(1);
42
+ if (!client) {
43
+ return null;
44
+ }
45
+ const isVerified = await verifyPassword(secret.join(''), client.secret);
46
+ return isVerified ? client : null;
47
+ }
@@ -0,0 +1,4 @@
1
+ import Joi from 'joi';
2
+ import type { LoginFormFields } from './interfaces';
3
+ declare const LoginRequest: Joi.ObjectSchema<LoginFormFields>;
4
+ export default LoginRequest;
@@ -0,0 +1,8 @@
1
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
2
+ import Joi from 'joi';
3
+ import { createSchema } from '../forms/ValidationService';
4
+ const LoginRequest = createSchema({
5
+ email: Joi.string().email({ minDomainSegments: 2, tlds: false }).required(),
6
+ password: Joi.string().min(8).required(),
7
+ });
8
+ export default LoginRequest;
@@ -0,0 +1,4 @@
1
+ import Joi from 'joi';
2
+ import type { MFAFormFields } from './interfaces';
3
+ declare const MFARequest: Joi.ObjectSchema<MFAFormFields>;
4
+ export default MFARequest;
@@ -0,0 +1,9 @@
1
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
2
+ import Joi from 'joi';
3
+ import { createSchema } from '../forms/ValidationService';
4
+ const MFARequest = createSchema({
5
+ token: Joi.string()
6
+ .pattern(/^[0-9]{6}$/u)
7
+ .required(),
8
+ });
9
+ export default MFARequest;
@@ -0,0 +1,6 @@
1
+ import type { Errorable } from '@sqrzro/interfaces';
2
+ import type { MFAFormFields, UserObject } from './interfaces';
3
+ export declare function checkMFAEnabled(): boolean;
4
+ export declare function generateMFA(name: string, email?: string): Promise<string | null>;
5
+ export declare function checkUserHasMFA(user: UserObject): Promise<boolean>;
6
+ export declare function handleMFAForm(formData: MFAFormFields): Promise<Errorable<boolean>>;
@@ -0,0 +1,105 @@
1
+ import { and, eq, isNotNull, isNull } from 'drizzle-orm';
2
+ import qrcode from 'qrcode';
3
+ import { authenticator } from 'otplib';
4
+ import { db } from '../database/DatabaseService';
5
+ import { authMFATable, authUserTable } from '../database/schema';
6
+ import { submitForm } from '../forms/FormService';
7
+ import MFARequest from './MFARequest';
8
+ import { createUserSession, generateID, getSessionUser } from './SessionService';
9
+ export function checkMFAEnabled() {
10
+ return process.env.AUTH_MFA_ENABLED !== 'false';
11
+ }
12
+ export async function generateMFA(name, email) {
13
+ if (!checkMFAEnabled() || !email) {
14
+ return null;
15
+ }
16
+ const [user] = await db
17
+ .select()
18
+ .from(authUserTable)
19
+ .where(eq(authUserTable.email, email))
20
+ .limit(1);
21
+ if (!user) {
22
+ return null;
23
+ }
24
+ const secret = authenticator.generateSecret();
25
+ const otpauth = authenticator.keyuri(email, name, secret);
26
+ // Delete all the unverified MFA entries for this user
27
+ await db
28
+ .delete(authMFATable)
29
+ .where(and(eq(authMFATable.userId, user.id), isNull(authMFATable.verifiedAt)));
30
+ // Add the new MFA entry
31
+ await db.insert(authMFATable).values({
32
+ id: generateID(),
33
+ name: 'Default',
34
+ secret,
35
+ userId: user.id,
36
+ });
37
+ return new Promise((resolve, reject) => {
38
+ qrcode.toDataURL(otpauth, { rendererOpts: { quality: 1 }, margin: 0, scale: 2 }, (err, data) => {
39
+ if (err) {
40
+ reject(err);
41
+ }
42
+ resolve(data);
43
+ });
44
+ });
45
+ }
46
+ export async function checkUserHasMFA(user) {
47
+ if (!checkMFAEnabled()) {
48
+ return false;
49
+ }
50
+ const [mfa] = await db
51
+ .select()
52
+ .from(authMFATable)
53
+ .where(and(eq(authMFATable.userId, user.id), isNotNull(authMFATable.verifiedAt)))
54
+ .limit(1);
55
+ return Boolean(mfa);
56
+ }
57
+ async function markAsVerified(userID) {
58
+ if (!checkMFAEnabled()) {
59
+ return;
60
+ }
61
+ await db
62
+ .update(authMFATable)
63
+ .set({ verifiedAt: new Date() })
64
+ .where(eq(authMFATable.userId, userID));
65
+ }
66
+ async function validateUserToken(userID, token) {
67
+ if (!checkMFAEnabled()) {
68
+ return false;
69
+ }
70
+ const [mfa] = await db
71
+ .select()
72
+ .from(authMFATable)
73
+ .where(eq(authMFATable.userId, userID))
74
+ .limit(1);
75
+ if (!mfa) {
76
+ return false;
77
+ }
78
+ return authenticator.check(token, mfa.secret);
79
+ }
80
+ async function handleMFA(formData) {
81
+ if (!checkMFAEnabled()) {
82
+ return false;
83
+ }
84
+ const user = await getSessionUser();
85
+ if (!user) {
86
+ return false;
87
+ }
88
+ const isValid = await validateUserToken(user.id, formData.token);
89
+ if (!isValid) {
90
+ return false;
91
+ }
92
+ await markAsVerified(user.id);
93
+ return createUserSession(user.id, 'AUTHED');
94
+ }
95
+ export async function handleMFAForm(formData) {
96
+ if (!checkMFAEnabled()) {
97
+ return [null, new Error('MFA is not enabled')];
98
+ }
99
+ const response = await submitForm({
100
+ fn: handleMFA,
101
+ formData,
102
+ request: MFARequest,
103
+ });
104
+ return response;
105
+ }
@@ -0,0 +1,4 @@
1
+ import Joi from 'joi';
2
+ import type { PasswordFormFields } from './interfaces';
3
+ declare const PasswordRequest: Joi.ObjectSchema<PasswordFormFields>;
4
+ export default PasswordRequest;
@@ -0,0 +1,12 @@
1
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
2
+ import Joi from 'joi';
3
+ import { createSchema } from '../forms/ValidationService';
4
+ const PasswordRequest = createSchema({
5
+ email: Joi.string().max(60).email({ minDomainSegments: 2, tlds: false }).required().messages({
6
+ 'any.required': 'Please provide your email address, so we can send you a reset link',
7
+ 'string.empty': 'Please provide your email address, so we can send you a reset link',
8
+ 'string.email': 'Please make sure your email address is valid',
9
+ 'string.max': 'Please make sure your email address is valid',
10
+ }),
11
+ });
12
+ export default PasswordRequest;
@@ -0,0 +1,4 @@
1
+ import Joi from 'joi';
2
+ import type { PasswordResetFormFields } from './interfaces';
3
+ declare const PasswordResetRequest: Joi.ObjectSchema<PasswordResetFormFields>;
4
+ export default PasswordResetRequest;
@@ -0,0 +1,13 @@
1
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
2
+ import Joi from 'joi';
3
+ import { createSchema } from '../forms/ValidationService';
4
+ const PasswordResetRequest = createSchema({
5
+ token: Joi.string()
6
+ .pattern(/[a-z0-9]{40}/u)
7
+ .required(),
8
+ password: Joi.string().required().messages({
9
+ 'any.required': 'Please provide your new password',
10
+ 'string.empty': 'Please provide your new password',
11
+ }),
12
+ });
13
+ export default PasswordResetRequest;
@@ -0,0 +1,8 @@
1
+ type PasswordRule = 'lower' | 'min' | 'number' | 'symbol' | 'upper';
2
+ export type PasswordRuleObject = Partial<Record<PasswordRule, number>>;
3
+ type PasswordValidityObject = Record<PasswordRule, boolean>;
4
+ export declare function hashPassword(password: string): Promise<string>;
5
+ export declare function verifyPassword(data?: string, encrypted?: string): Promise<boolean>;
6
+ export declare function getPasswordComplexity(password: string, rules?: Partial<PasswordRuleObject>): Promise<Partial<PasswordValidityObject>>;
7
+ export declare function checkPasswordComplexity(password: string, rules?: Partial<PasswordRuleObject>): Promise<Partial<boolean>>;
8
+ export {};
@@ -0,0 +1,54 @@
1
+ import bcrypt from 'bcryptjs';
2
+ const PW_SALT_ROUNDS = 12;
3
+ const PASSWORD_RULES = {
4
+ min: 8,
5
+ upper: 1,
6
+ lower: 1,
7
+ number: 1,
8
+ symbol: 1,
9
+ };
10
+ function checkPasswordMin(password, value) {
11
+ return password.length >= value;
12
+ }
13
+ function checkPasswordUpper(password, value) {
14
+ return password.replace(/[^A-Z]/gu, '').length >= value;
15
+ }
16
+ function checkPasswordLower(password, value) {
17
+ return password.replace(/[^a-z]/gu, '').length >= value;
18
+ }
19
+ function checkPasswordNumber(password, value) {
20
+ return password.replace(/[^0-9]/gu, '').length >= value;
21
+ }
22
+ function checkPasswordSymbol(password, value) {
23
+ return password.replace(/[^$]/gu, '').length >= value;
24
+ }
25
+ const PASSWORD_FUNCTIONS = {
26
+ min: checkPasswordMin,
27
+ upper: checkPasswordUpper,
28
+ lower: checkPasswordLower,
29
+ number: checkPasswordNumber,
30
+ symbol: checkPasswordSymbol,
31
+ };
32
+ export async function hashPassword(password) {
33
+ const hash = await bcrypt.hash(password, PW_SALT_ROUNDS);
34
+ return hash;
35
+ }
36
+ export async function verifyPassword(data, encrypted) {
37
+ if (!data || !encrypted) {
38
+ return false;
39
+ }
40
+ const verified = await bcrypt.compare(data, encrypted);
41
+ return verified;
42
+ }
43
+ export async function getPasswordComplexity(password, rules = PASSWORD_RULES) {
44
+ const entries = Object.entries(rules);
45
+ const validity = entries.reduce((acc, [rule, value]) => {
46
+ acc[rule] = PASSWORD_FUNCTIONS[rule](password, value);
47
+ return acc;
48
+ }, {});
49
+ return Promise.resolve(validity);
50
+ }
51
+ export async function checkPasswordComplexity(password, rules = PASSWORD_RULES) {
52
+ const validity = await getPasswordComplexity(password, rules);
53
+ return Promise.resolve(Object.values(validity).every(Boolean));
54
+ }
@@ -0,0 +1,42 @@
1
+ import { Lucia } from 'lucia';
2
+ import { NextResponse } from 'next/server';
3
+ import type { NextRequest } from 'next/server';
4
+ import type { Scope } from '../database/schema';
5
+ interface ScopeData {
6
+ allowedRoute?: string;
7
+ redirectOnAuth?: string;
8
+ redirectOnUnauth?: string;
9
+ }
10
+ export type ScopeObject = Record<Scope, ScopeData>;
11
+ interface DatabaseSessionAttributes {
12
+ scope: Scope;
13
+ }
14
+ interface DatabaseUserAttributes {
15
+ email: string;
16
+ role: number;
17
+ }
18
+ declare module 'lucia' {
19
+ interface Register {
20
+ Lucia: typeof lucia;
21
+ DatabaseSessionAttributes: DatabaseSessionAttributes;
22
+ DatabaseUserAttributes: DatabaseUserAttributes;
23
+ }
24
+ }
25
+ export declare const lucia: Lucia<DatabaseSessionAttributes, DatabaseUserAttributes>;
26
+ export declare function generateID(length?: number): string;
27
+ export declare function invalidateSession(id: string): Promise<void>;
28
+ export declare function invalidateUserSessions(id: string): Promise<void>;
29
+ export declare function createUserSession(id: string, scope?: Scope): Promise<boolean>;
30
+ export declare function getSessionID(): string | null;
31
+ export declare function checkSessionExists(): boolean;
32
+ export declare function getSessionUser(): Promise<{
33
+ id: string;
34
+ email: string;
35
+ role: number;
36
+ } | null>;
37
+ export declare function checkRouteAllowed(pathname: string, route?: string): boolean;
38
+ export declare function getScopes(): Promise<ScopeObject>;
39
+ export declare function getScopeByID(id: Scope): Promise<ScopeData>;
40
+ export declare function setScopes(customScopes?: Partial<ScopeObject>): Promise<void>;
41
+ export declare function handleSession(request: NextRequest, customScopes?: Partial<ScopeObject>): Promise<NextResponse>;
42
+ export {};
@@ -0,0 +1,127 @@
1
+ import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
2
+ import { Lucia, generateId } from 'lucia';
3
+ import { cookies } from 'next/headers';
4
+ import { NextResponse } from 'next/server';
5
+ import { match } from 'path-to-regexp';
6
+ import { getAllowedRoles } from './AuthService';
7
+ import { db } from '../database/DatabaseService';
8
+ import { authSessionTable, authUserTable } from '../database/schema';
9
+ import { getFromCache, setToCache } from '../cache/CacheService';
10
+ import { getOrigin } from '../url/URLService';
11
+ import { getFromObject } from '@sqrzro/utility';
12
+ const DEFAULT_REDIRECT = '/auth/login';
13
+ const ID_LENGTH = 16;
14
+ const DEFAULT_SCOPES = {
15
+ ANON: {
16
+ allowedRoute: '/auth/(login|password)',
17
+ redirectOnUnauth: DEFAULT_REDIRECT,
18
+ },
19
+ MFA: {
20
+ allowedRoute: '/auth/mfa',
21
+ redirectOnUnauth: '/auth/mfa',
22
+ },
23
+ AUTHED: {
24
+ allowedRoute: '*',
25
+ redirectOnAuth: '/',
26
+ },
27
+ };
28
+ const adapter = new DrizzlePostgreSQLAdapter(db, authSessionTable, authUserTable);
29
+ export const lucia = new Lucia(adapter, {
30
+ sessionCookie: {
31
+ attributes: {
32
+ secure: process.env.NODE_ENV === 'production',
33
+ },
34
+ name: process.env.AUTH_COOKIE_NAME || 'auth_session',
35
+ },
36
+ getSessionAttributes: (attributes) => ({
37
+ scope: attributes.scope,
38
+ }),
39
+ getUserAttributes: (attributes) => ({
40
+ email: attributes.email,
41
+ role: attributes.role,
42
+ }),
43
+ });
44
+ export function generateID(length = ID_LENGTH) {
45
+ return generateId(length);
46
+ }
47
+ export async function invalidateSession(id) {
48
+ const cookie = lucia.createBlankSessionCookie();
49
+ cookies().set(cookie.name, cookie.value, cookie.attributes);
50
+ return lucia.invalidateSession(id);
51
+ }
52
+ export async function invalidateUserSessions(id) {
53
+ return lucia.invalidateUserSessions(id);
54
+ }
55
+ export async function createUserSession(id, scope = 'ANON') {
56
+ const session = await lucia.createSession(id, { scope });
57
+ const sessionCookie = lucia.createSessionCookie(session.id);
58
+ cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
59
+ return true;
60
+ }
61
+ export function getSessionID() {
62
+ return cookies().get(lucia.sessionCookieName)?.value ?? null;
63
+ }
64
+ export function checkSessionExists() {
65
+ return Boolean(getSessionID());
66
+ }
67
+ export async function getSessionUser() {
68
+ const sessionID = getSessionID();
69
+ if (!sessionID) {
70
+ return null;
71
+ }
72
+ const { user } = await lucia.validateSession(sessionID);
73
+ if (!user?.role || !getAllowedRoles().includes(user.role)) {
74
+ return null;
75
+ }
76
+ return getFromObject(user, ['id', 'email', 'role']);
77
+ }
78
+ export function checkRouteAllowed(pathname, route) {
79
+ if (!route) {
80
+ return false;
81
+ }
82
+ if (route === '*') {
83
+ return true;
84
+ }
85
+ return match(route)(pathname) !== false;
86
+ }
87
+ export async function getScopes() {
88
+ const scopes = await getFromCache(`${getOrigin()}:scopes`);
89
+ return scopes ? JSON.parse(scopes) : DEFAULT_SCOPES;
90
+ }
91
+ export async function getScopeByID(id) {
92
+ const scopes = await getScopes();
93
+ return scopes[id];
94
+ }
95
+ export async function setScopes(customScopes) {
96
+ const scopes = {
97
+ ANON: {
98
+ ...DEFAULT_SCOPES.ANON,
99
+ ...customScopes?.ANON,
100
+ },
101
+ MFA: {
102
+ ...DEFAULT_SCOPES.MFA,
103
+ ...customScopes?.MFA,
104
+ },
105
+ AUTHED: {
106
+ ...DEFAULT_SCOPES.AUTHED,
107
+ ...customScopes?.AUTHED,
108
+ },
109
+ };
110
+ return setToCache(`${getOrigin()}:scopes`, JSON.stringify(scopes));
111
+ }
112
+ async function validateSessionFromID(sessionID, pathname) {
113
+ const scopes = await getScopes();
114
+ const { session, user } = await lucia.validateSession(sessionID);
115
+ const scope = scopes[session && getAllowedRoles().includes(user?.role) ? session.scope : 'ANON'];
116
+ return checkRouteAllowed(pathname, scope.allowedRoute)
117
+ ? null
118
+ : scope.redirectOnUnauth || DEFAULT_REDIRECT;
119
+ }
120
+ export async function handleSession(request, customScopes) {
121
+ const sessionID = request.nextUrl.searchParams.get('id') || '';
122
+ const pathname = request.nextUrl.searchParams.get('pathname') || '/';
123
+ await setScopes(customScopes);
124
+ return NextResponse.json({
125
+ redirect: await validateSessionFromID(sessionID, pathname),
126
+ });
127
+ }
@@ -0,0 +1,6 @@
1
+ export * from './AuthService';
2
+ export * from './ClientService';
3
+ export * from './MFAService';
4
+ export * from './SessionService';
5
+ export * from './PasswordService';
6
+ export * from './interfaces';
@@ -0,0 +1,6 @@
1
+ export * from './AuthService';
2
+ export * from './ClientService';
3
+ export * from './MFAService';
4
+ export * from './SessionService';
5
+ export * from './PasswordService';
6
+ export * from './interfaces';
@@ -0,0 +1,20 @@
1
+ export interface LoginFormFields {
2
+ email: string;
3
+ password: string;
4
+ }
5
+ export interface MFAFormFields {
6
+ token: string;
7
+ }
8
+ export interface PasswordFormFields {
9
+ email: string;
10
+ }
11
+ export interface PasswordResetFormFields {
12
+ password: string;
13
+ token: string;
14
+ }
15
+ export interface UserObject {
16
+ id: string;
17
+ email: string;
18
+ password?: string | null;
19
+ role?: number;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function getFromCache(key: string): Promise<string | null>;
2
+ export declare function setToCache(key: string, value: string): Promise<void>;