@sqrzro/server 2.0.0-bz.7 → 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.
- package/LICENSE +1 -1
- package/README.md +25 -1
- package/auth.d.ts +1 -0
- package/auth.js +1 -0
- package/cache.d.ts +1 -0
- package/cache.js +1 -0
- package/database.d.ts +1 -0
- package/database.js +1 -0
- package/dist/auth/AuthService.d.ts +14 -0
- package/dist/auth/AuthService.js +135 -0
- package/dist/auth/ClientService.d.ts +11 -0
- package/dist/auth/ClientService.js +47 -0
- package/dist/auth/LoginRequest.js +8 -0
- package/dist/auth/MFARequest.d.ts +4 -0
- package/dist/auth/MFARequest.js +9 -0
- package/dist/auth/MFAService.d.ts +6 -0
- package/dist/auth/MFAService.js +105 -0
- package/dist/auth/PasswordRequest.d.ts +4 -0
- package/dist/auth/PasswordRequest.js +12 -0
- package/dist/auth/PasswordResetRequest.d.ts +4 -0
- package/dist/auth/PasswordResetRequest.js +13 -0
- package/dist/auth/PasswordService.d.ts +8 -0
- package/dist/auth/PasswordService.js +54 -0
- package/dist/auth/SessionService.d.ts +42 -0
- package/dist/auth/SessionService.js +127 -0
- package/dist/{index.d.ts → auth/index.d.ts} +3 -5
- package/dist/auth/index.js +6 -0
- package/dist/auth/interfaces.d.ts +20 -0
- package/dist/auth/interfaces.js +1 -0
- package/dist/cache/CacheService.d.ts +2 -0
- package/dist/cache/CacheService.js +14 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/database/DatabaseService.d.ts +7 -0
- package/dist/database/DatabaseService.js +12 -0
- package/dist/database/schema.d.ts +284 -0
- package/dist/database/schema.js +42 -0
- package/dist/forms/FormService.d.ts +16 -0
- package/dist/forms/FormService.js +78 -0
- package/dist/forms/ImageService.js +19 -0
- package/dist/forms/ValidationError.d.ts +4 -0
- package/dist/forms/ValidationError.js +7 -0
- package/dist/{RequestService.d.ts → forms/ValidationService.d.ts} +4 -8
- package/dist/forms/ValidationService.js +59 -0
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.js +3 -0
- package/dist/forms/lang.d.ts +2 -0
- package/dist/forms/lang.js +115 -0
- package/dist/lists/ListService.d.ts +16 -0
- package/dist/lists/ListService.js +28 -0
- package/dist/lists/index.d.ts +1 -0
- package/dist/lists/index.js +1 -0
- package/dist/mail/MailService.d.ts +12 -0
- package/dist/mail/MailService.js +55 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +1 -0
- package/dist/middleware.d.ts +1 -2
- package/dist/middleware.js +23 -29
- package/dist/url/URLService.d.ts +26 -0
- package/dist/url/URLService.js +48 -0
- package/dist/url/index.d.ts +1 -0
- package/dist/url/index.js +1 -0
- package/forms.d.ts +1 -0
- package/forms.js +1 -0
- package/lists.d.ts +1 -0
- package/lists.js +1 -0
- package/mail.d.ts +1 -0
- package/mail.js +1 -0
- package/package.json +36 -11
- package/schema.d.ts +1 -0
- package/schema.js +1 -0
- package/url.d.ts +1 -0
- package/url.js +1 -0
- package/dist/AuthService.d.ts +0 -11
- package/dist/AuthService.js +0 -36
- package/dist/DataService.d.ts +0 -29
- package/dist/DataService.js +0 -64
- package/dist/ImageService.js +0 -23
- package/dist/LoginRequest.js +0 -11
- package/dist/PasswordService.d.ts +0 -6
- package/dist/PasswordService.js +0 -69
- package/dist/RequestService.js +0 -67
- package/dist/SessionService.d.ts +0 -5
- package/dist/SessionService.js +0 -56
- package/dist/index.js +0 -11
- package/dist/interfaces.d.ts +0 -7
- package/dist/interfaces.js +0 -2
- /package/dist/{LoginRequest.d.ts → auth/LoginRequest.d.ts} +0 -0
- /package/dist/{ImageService.d.ts → forms/ImageService.d.ts} +0 -0
package/LICENSE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright 2024 Richard Carter
|
|
2
2
|
|
|
3
3
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
4
4
|
|
package/README.md
CHANGED
|
@@ -1 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<h1>
|
|
4
|
+
<img src="assets/logo.svg" alt="" width="64" style="margin-bottom: 5px">
|
|
5
|
+
<br />
|
|
6
|
+
Square Zero Server
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<hr />
|
|
12
|
+
|
|
13
|
+
## @sqrzro/server/auth
|
|
14
|
+
|
|
15
|
+
## @sqrzro/server/cache
|
|
16
|
+
|
|
17
|
+
## @sqrzro/server/db
|
|
18
|
+
|
|
19
|
+
## @sqrzro/server/forms
|
|
20
|
+
|
|
21
|
+
## @sqrzro/server/lists
|
|
22
|
+
|
|
23
|
+
## @sqrzro/server/mail
|
|
24
|
+
|
|
25
|
+
## @sqrzro/server/url
|
package/auth.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/auth/index';
|
package/auth.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/auth/index');
|
package/cache.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/cache/index';
|
package/cache.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/cache/index');
|
package/database.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/database/index';
|
package/database.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/database/index');
|
|
@@ -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,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,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,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,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 {};
|