@technomoron/api-server-base 1.1.13 → 2.0.0-beta.2
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/dist/cjs/api-server-base.cjs +181 -74
- package/dist/cjs/api-server-base.d.ts +66 -29
- package/dist/cjs/auth-api/auth-module.d.ts +96 -0
- package/dist/cjs/auth-api/auth-module.js +1032 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +55 -0
- package/dist/cjs/auth-api/compat-auth-storage.js +116 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +66 -0
- package/dist/cjs/auth-api/mem-auth-store.js +135 -0
- package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +75 -0
- package/dist/cjs/auth-api/sql-auth-store.js +166 -0
- package/dist/cjs/auth-api/storage.d.ts +36 -0
- package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +2 -2
- package/dist/cjs/auth-api/types.d.ts +29 -0
- package/dist/cjs/auth-api/types.js +2 -0
- package/dist/cjs/index.cjs +41 -7
- package/dist/cjs/index.d.ts +29 -5
- package/dist/cjs/oauth/base.d.ts +10 -0
- package/dist/cjs/oauth/base.js +6 -0
- package/dist/cjs/oauth/memory.d.ts +16 -0
- package/dist/cjs/oauth/memory.js +99 -0
- package/dist/cjs/oauth/models.d.ts +45 -0
- package/dist/cjs/oauth/models.js +58 -0
- package/dist/cjs/oauth/sequelize.d.ts +68 -0
- package/dist/cjs/oauth/sequelize.js +210 -0
- package/dist/cjs/oauth/types.d.ts +50 -0
- package/dist/cjs/oauth/types.js +3 -0
- package/dist/cjs/passkey/base.d.ts +15 -0
- package/dist/cjs/passkey/base.js +6 -0
- package/dist/cjs/passkey/memory.d.ts +26 -0
- package/dist/cjs/passkey/memory.js +82 -0
- package/dist/cjs/passkey/models.d.ts +25 -0
- package/dist/cjs/passkey/models.js +115 -0
- package/dist/cjs/passkey/sequelize.d.ts +54 -0
- package/dist/cjs/passkey/sequelize.js +211 -0
- package/dist/cjs/passkey/service.d.ts +17 -0
- package/dist/cjs/passkey/service.js +221 -0
- package/dist/cjs/passkey/types.d.ts +75 -0
- package/dist/cjs/passkey/types.js +2 -0
- package/dist/cjs/token/base.d.ts +38 -0
- package/dist/cjs/token/base.js +114 -0
- package/dist/cjs/token/memory.d.ts +19 -0
- package/dist/cjs/token/memory.js +149 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/sequelize.js +404 -0
- package/dist/cjs/token/types.d.ts +27 -0
- package/dist/cjs/token/types.js +2 -0
- package/dist/cjs/user/base.d.ts +26 -0
- package/dist/cjs/user/base.js +45 -0
- package/dist/cjs/user/memory.d.ts +35 -0
- package/dist/cjs/user/memory.js +173 -0
- package/dist/cjs/user/sequelize.d.ts +41 -0
- package/dist/cjs/user/sequelize.js +182 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/cjs/user/types.js +2 -0
- package/dist/esm/api-server-base.d.ts +66 -29
- package/dist/esm/api-server-base.js +179 -72
- package/dist/esm/auth-api/auth-module.d.ts +96 -0
- package/dist/esm/auth-api/auth-module.js +1030 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +55 -0
- package/dist/esm/auth-api/compat-auth-storage.js +112 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +66 -0
- package/dist/esm/auth-api/mem-auth-store.js +131 -0
- package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +75 -0
- package/dist/esm/auth-api/sql-auth-store.js +162 -0
- package/dist/esm/auth-api/storage.d.ts +36 -0
- package/dist/esm/{auth-storage.js → auth-api/storage.js} +2 -2
- package/dist/esm/auth-api/types.d.ts +29 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/index.d.ts +29 -5
- package/dist/esm/index.js +19 -2
- package/dist/esm/oauth/base.d.ts +10 -0
- package/dist/esm/oauth/base.js +2 -0
- package/dist/esm/oauth/memory.d.ts +16 -0
- package/dist/esm/oauth/memory.js +92 -0
- package/dist/esm/oauth/models.d.ts +45 -0
- package/dist/esm/oauth/models.js +51 -0
- package/dist/esm/oauth/sequelize.d.ts +68 -0
- package/dist/esm/oauth/sequelize.js +199 -0
- package/dist/esm/oauth/types.d.ts +50 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +15 -0
- package/dist/esm/passkey/base.js +2 -0
- package/dist/esm/passkey/memory.d.ts +26 -0
- package/dist/esm/passkey/memory.js +78 -0
- package/dist/esm/passkey/models.d.ts +25 -0
- package/dist/esm/passkey/models.js +108 -0
- package/dist/esm/passkey/sequelize.d.ts +54 -0
- package/dist/esm/passkey/sequelize.js +207 -0
- package/dist/esm/passkey/service.d.ts +17 -0
- package/dist/esm/passkey/service.js +217 -0
- package/dist/esm/passkey/types.d.ts +75 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/token/base.d.ts +38 -0
- package/dist/esm/token/base.js +107 -0
- package/dist/esm/token/memory.d.ts +19 -0
- package/dist/esm/token/memory.js +145 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +400 -0
- package/dist/esm/token/types.d.ts +27 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/user/base.d.ts +26 -0
- package/dist/esm/user/base.js +38 -0
- package/dist/esm/user/memory.d.ts +35 -0
- package/dist/esm/user/memory.js +169 -0
- package/dist/esm/user/sequelize.d.ts +41 -0
- package/dist/esm/user/sequelize.js +176 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/package.json +11 -3
- package/dist/cjs/auth-storage.d.ts +0 -133
- package/dist/esm/auth-storage.d.ts +0 -133
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UserStore = void 0;
|
|
7
|
+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
8
|
+
class UserStore {
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.toPublicUser = opts.toPublic ?? ((u) => u);
|
|
11
|
+
const rounds = typeof opts.bcryptRounds === 'number' && Number.isFinite(opts.bcryptRounds)
|
|
12
|
+
? Math.max(4, Math.floor(opts.bcryptRounds))
|
|
13
|
+
: 12;
|
|
14
|
+
this.bcryptRounds = rounds;
|
|
15
|
+
this.bcryptPepper =
|
|
16
|
+
typeof opts.bcryptPepper === 'string' && opts.bcryptPepper.length > 0 ? opts.bcryptPepper : undefined;
|
|
17
|
+
}
|
|
18
|
+
async hashPassword(plain) {
|
|
19
|
+
const candidate = this.bcryptPepper ? `${plain}${this.bcryptPepper}` : plain;
|
|
20
|
+
return bcryptjs_1.default.hash(candidate, this.bcryptRounds);
|
|
21
|
+
}
|
|
22
|
+
async verifyPassword(plain, hashed) {
|
|
23
|
+
const candidate = this.bcryptPepper ? `${plain}${this.bcryptPepper}` : plain;
|
|
24
|
+
return bcryptjs_1.default.compare(candidate, hashed);
|
|
25
|
+
}
|
|
26
|
+
normalizeUserInput(input) {
|
|
27
|
+
const login = typeof input.login === 'string' ? input.login.trim() : '';
|
|
28
|
+
const email = typeof input.email === 'string' ? input.email.trim() : '';
|
|
29
|
+
if (!login || !email) {
|
|
30
|
+
throw new Error('login and email are required');
|
|
31
|
+
}
|
|
32
|
+
const password = typeof input.password === 'string' ? input.password : undefined;
|
|
33
|
+
return { ...input, login, email, password };
|
|
34
|
+
}
|
|
35
|
+
toPublic(user) {
|
|
36
|
+
const mapped = this.toPublicUser(user);
|
|
37
|
+
if (mapped && typeof mapped === 'object') {
|
|
38
|
+
const { password: _password, ...rest } = mapped;
|
|
39
|
+
void _password;
|
|
40
|
+
return rest;
|
|
41
|
+
}
|
|
42
|
+
return mapped;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.UserStore = UserStore;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { UserStore } from './base.js';
|
|
2
|
+
import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
|
|
3
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
4
|
+
export interface MemoryUserAttributes extends Record<string, unknown> {
|
|
5
|
+
user_id: number;
|
|
6
|
+
login: string;
|
|
7
|
+
email: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}
|
|
10
|
+
export type MemoryPublicUser<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes> = Omit<UserAttributes, 'password'>;
|
|
11
|
+
export interface MemoryUserStoreOptions<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> {
|
|
12
|
+
bcryptRounds?: number;
|
|
13
|
+
bcryptPepper?: string;
|
|
14
|
+
toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
|
|
15
|
+
userIdFactory?: () => number;
|
|
16
|
+
startingUserId?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class MemoryUserStore<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
|
|
19
|
+
private readonly usersById;
|
|
20
|
+
private readonly loginToId;
|
|
21
|
+
private readonly emailToId;
|
|
22
|
+
private readonly userIdFactory;
|
|
23
|
+
private nextUserId;
|
|
24
|
+
constructor(options?: MemoryUserStoreOptions<UserAttributes, PublicUserShape>);
|
|
25
|
+
findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
|
|
26
|
+
findById(id: AuthIdentifier): Promise<UserAttributes | null>;
|
|
27
|
+
findByLoginOrEmail(loginOrEmail: string): Promise<UserAttributes | null>;
|
|
28
|
+
createUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
29
|
+
upsertUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
30
|
+
updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<UserAttributes>;
|
|
31
|
+
setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
32
|
+
getPasswordHash(user: UserAttributes): string | null;
|
|
33
|
+
getUserId(user: UserAttributes): AuthIdentifier;
|
|
34
|
+
private persistUser;
|
|
35
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryUserStore = void 0;
|
|
4
|
+
const base_js_1 = require("./base.js");
|
|
5
|
+
function cloneUser(user) {
|
|
6
|
+
return { ...user };
|
|
7
|
+
}
|
|
8
|
+
function normalizeUserId(identifier) {
|
|
9
|
+
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
10
|
+
return identifier;
|
|
11
|
+
}
|
|
12
|
+
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
13
|
+
return Number(identifier);
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
16
|
+
}
|
|
17
|
+
class MemoryUserStore extends base_js_1.UserStore {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super({
|
|
20
|
+
toPublic: options.toPublic,
|
|
21
|
+
bcryptRounds: options.bcryptRounds,
|
|
22
|
+
bcryptPepper: options.bcryptPepper
|
|
23
|
+
});
|
|
24
|
+
this.usersById = new Map();
|
|
25
|
+
this.loginToId = new Map();
|
|
26
|
+
this.emailToId = new Map();
|
|
27
|
+
this.nextUserId = Number.isFinite(options.startingUserId) ? Number(options.startingUserId) : 1;
|
|
28
|
+
this.userIdFactory =
|
|
29
|
+
options.userIdFactory ??
|
|
30
|
+
(() => {
|
|
31
|
+
const id = this.nextUserId;
|
|
32
|
+
this.nextUserId += 1;
|
|
33
|
+
return id;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async findUser(identifier) {
|
|
37
|
+
if (typeof identifier === 'number') {
|
|
38
|
+
const user = this.usersById.get(identifier);
|
|
39
|
+
return user ? cloneUser(user) : null;
|
|
40
|
+
}
|
|
41
|
+
if (typeof identifier === 'string') {
|
|
42
|
+
const numeric = /^\d+$/.test(identifier) ? Number(identifier) : null;
|
|
43
|
+
if (numeric !== null) {
|
|
44
|
+
const user = this.usersById.get(numeric);
|
|
45
|
+
if (user) {
|
|
46
|
+
return cloneUser(user);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const loginId = this.loginToId.get(identifier);
|
|
50
|
+
if (loginId !== undefined) {
|
|
51
|
+
return cloneUser(this.usersById.get(loginId));
|
|
52
|
+
}
|
|
53
|
+
const emailId = this.emailToId.get(identifier);
|
|
54
|
+
if (emailId !== undefined) {
|
|
55
|
+
return cloneUser(this.usersById.get(emailId));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
async findById(id) {
|
|
61
|
+
try {
|
|
62
|
+
const numeric = normalizeUserId(id);
|
|
63
|
+
const user = this.usersById.get(numeric);
|
|
64
|
+
return user ? cloneUser(user) : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async findByLoginOrEmail(loginOrEmail) {
|
|
71
|
+
return this.findUser(loginOrEmail);
|
|
72
|
+
}
|
|
73
|
+
async createUser(input) {
|
|
74
|
+
const normalizedInput = this.normalizeUserInput(input);
|
|
75
|
+
const providedId = input.user_id;
|
|
76
|
+
const userId = typeof providedId === 'number' && Number.isFinite(providedId) ? providedId : this.userIdFactory();
|
|
77
|
+
if (this.usersById.has(userId)) {
|
|
78
|
+
throw new Error(`User ${userId} already exists`);
|
|
79
|
+
}
|
|
80
|
+
if (this.loginToId.has(normalizedInput.login)) {
|
|
81
|
+
throw new Error(`User with login ${normalizedInput.login} already exists`);
|
|
82
|
+
}
|
|
83
|
+
if (this.emailToId.has(normalizedInput.email)) {
|
|
84
|
+
throw new Error(`User with email ${normalizedInput.email} already exists`);
|
|
85
|
+
}
|
|
86
|
+
const passwordHash = normalizedInput.password ? await this.hashPassword(normalizedInput.password) : '';
|
|
87
|
+
const record = {
|
|
88
|
+
...normalizedInput,
|
|
89
|
+
user_id: userId,
|
|
90
|
+
password: passwordHash
|
|
91
|
+
};
|
|
92
|
+
this.persistUser(record);
|
|
93
|
+
if (typeof providedId === 'number' && Number.isFinite(providedId)) {
|
|
94
|
+
this.nextUserId = Math.max(this.nextUserId, providedId + 1);
|
|
95
|
+
}
|
|
96
|
+
return cloneUser(record);
|
|
97
|
+
}
|
|
98
|
+
async upsertUser(input) {
|
|
99
|
+
const normalizedInput = this.normalizeUserInput(input);
|
|
100
|
+
const providedId = input.user_id;
|
|
101
|
+
if (providedId !== undefined) {
|
|
102
|
+
const existing = this.usersById.get(providedId);
|
|
103
|
+
if (!existing) {
|
|
104
|
+
throw new Error(`User ${providedId} not found`);
|
|
105
|
+
}
|
|
106
|
+
const updates = {
|
|
107
|
+
...existing,
|
|
108
|
+
...normalizedInput
|
|
109
|
+
};
|
|
110
|
+
if (updates.login !== existing.login) {
|
|
111
|
+
const loginOwner = this.loginToId.get(updates.login);
|
|
112
|
+
if (loginOwner !== undefined && loginOwner !== existing.user_id) {
|
|
113
|
+
throw new Error(`User with login ${updates.login} already exists`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (updates.email !== existing.email) {
|
|
117
|
+
const emailOwner = this.emailToId.get(updates.email);
|
|
118
|
+
if (emailOwner !== undefined && emailOwner !== existing.user_id) {
|
|
119
|
+
throw new Error(`User with email ${updates.email} already exists`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (normalizedInput.password) {
|
|
123
|
+
updates.password = await this.hashPassword(normalizedInput.password);
|
|
124
|
+
}
|
|
125
|
+
this.persistUser(updates);
|
|
126
|
+
return cloneUser(updates);
|
|
127
|
+
}
|
|
128
|
+
return this.createUser(input);
|
|
129
|
+
}
|
|
130
|
+
async updateUser(id, patch) {
|
|
131
|
+
const user = await this.findById(id);
|
|
132
|
+
if (!user) {
|
|
133
|
+
throw new Error(`User ${String(id)} not found`);
|
|
134
|
+
}
|
|
135
|
+
const updates = { ...user, ...patch };
|
|
136
|
+
if (patch.password) {
|
|
137
|
+
updates.password = await this.hashPassword(patch.password);
|
|
138
|
+
}
|
|
139
|
+
this.persistUser(updates);
|
|
140
|
+
return cloneUser(updates);
|
|
141
|
+
}
|
|
142
|
+
async setPasswordHash(id, hash) {
|
|
143
|
+
const user = await this.findById(id);
|
|
144
|
+
if (!user) {
|
|
145
|
+
throw new Error(`User ${String(id)} not found`);
|
|
146
|
+
}
|
|
147
|
+
const updates = { ...user, password: hash };
|
|
148
|
+
this.persistUser(updates);
|
|
149
|
+
}
|
|
150
|
+
getPasswordHash(user) {
|
|
151
|
+
return user.password;
|
|
152
|
+
}
|
|
153
|
+
getUserId(user) {
|
|
154
|
+
return user.user_id;
|
|
155
|
+
}
|
|
156
|
+
persistUser(user) {
|
|
157
|
+
const snapshot = { ...user };
|
|
158
|
+
this.usersById.set(user.user_id, snapshot);
|
|
159
|
+
for (const [login, id] of [...this.loginToId.entries()]) {
|
|
160
|
+
if (id === user.user_id && login !== user.login) {
|
|
161
|
+
this.loginToId.delete(login);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const [email, id] of [...this.emailToId.entries()]) {
|
|
165
|
+
if (id === user.user_id && email !== user.email) {
|
|
166
|
+
this.emailToId.delete(email);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.loginToId.set(user.login, user.user_id);
|
|
170
|
+
this.emailToId.set(user.email, user.user_id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.MemoryUserStore = MemoryUserStore;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CreationOptional, Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
+
import { UserStore } from './base.js';
|
|
3
|
+
import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
|
|
4
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
5
|
+
export declare class AuthUserModel extends Model<InferAttributes<AuthUserModel>, InferCreationAttributes<AuthUserModel>> implements InferAttributes<AuthUserModel> {
|
|
6
|
+
user_id: CreationOptional<number>;
|
|
7
|
+
login: string;
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
export type AuthUserAttributes = InferAttributes<AuthUserModel>;
|
|
12
|
+
export type AuthUserCreationAttributes = InferCreationAttributes<AuthUserModel>;
|
|
13
|
+
export declare function initAuthUserModel(sequelize: Sequelize): typeof AuthUserModel;
|
|
14
|
+
export type GenericUserModel = Model<Record<string, unknown>, Record<string, unknown>>;
|
|
15
|
+
export type GenericUserModelStatic = ModelStatic<GenericUserModel>;
|
|
16
|
+
export interface SequelizeUserStoreOptions<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> {
|
|
17
|
+
bcryptRounds?: number;
|
|
18
|
+
bcryptPepper?: string;
|
|
19
|
+
sequelize: Sequelize;
|
|
20
|
+
userModel?: GenericUserModelStatic;
|
|
21
|
+
userModelFactory?: (sequelize: Sequelize) => GenericUserModelStatic;
|
|
22
|
+
recordMapper?: (model: GenericUserModel) => UserAttributes;
|
|
23
|
+
toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
|
|
24
|
+
}
|
|
25
|
+
export declare class SequelizeUserStore<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
|
|
26
|
+
readonly Users: GenericUserModelStatic;
|
|
27
|
+
private readonly recordMapper;
|
|
28
|
+
constructor(options: SequelizeUserStoreOptions<UserAttributes, PublicUserShape>);
|
|
29
|
+
findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
|
|
30
|
+
findById(id: AuthIdentifier): Promise<UserAttributes | null>;
|
|
31
|
+
findByLoginOrEmail(loginOrEmail: string): Promise<UserAttributes | null>;
|
|
32
|
+
createUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
33
|
+
upsertUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
34
|
+
updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<UserAttributes>;
|
|
35
|
+
setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
36
|
+
getPasswordHash(user: UserAttributes): string | null;
|
|
37
|
+
getUserId(user: UserAttributes): AuthIdentifier;
|
|
38
|
+
protected toUserRecord(model: GenericUserModel): UserAttributes;
|
|
39
|
+
private static mapModelToUser;
|
|
40
|
+
private normalizeUserId;
|
|
41
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SequelizeUserStore = exports.AuthUserModel = void 0;
|
|
4
|
+
exports.initAuthUserModel = initAuthUserModel;
|
|
5
|
+
const sequelize_1 = require("sequelize");
|
|
6
|
+
const base_js_1 = require("./base.js");
|
|
7
|
+
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
8
|
+
function integerIdType(sequelize) {
|
|
9
|
+
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? sequelize_1.DataTypes.INTEGER.UNSIGNED : sequelize_1.DataTypes.INTEGER;
|
|
10
|
+
}
|
|
11
|
+
function userTableOptions(sequelize) {
|
|
12
|
+
const opts = {
|
|
13
|
+
sequelize,
|
|
14
|
+
tableName: 'users',
|
|
15
|
+
timestamps: false
|
|
16
|
+
};
|
|
17
|
+
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
18
|
+
opts.charset = 'utf8mb4';
|
|
19
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
20
|
+
}
|
|
21
|
+
return opts;
|
|
22
|
+
}
|
|
23
|
+
class AuthUserModel extends sequelize_1.Model {
|
|
24
|
+
}
|
|
25
|
+
exports.AuthUserModel = AuthUserModel;
|
|
26
|
+
function initAuthUserModel(sequelize) {
|
|
27
|
+
const idType = integerIdType(sequelize);
|
|
28
|
+
AuthUserModel.init({
|
|
29
|
+
user_id: {
|
|
30
|
+
type: idType,
|
|
31
|
+
autoIncrement: true,
|
|
32
|
+
allowNull: false,
|
|
33
|
+
primaryKey: true
|
|
34
|
+
},
|
|
35
|
+
login: {
|
|
36
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
37
|
+
allowNull: false,
|
|
38
|
+
unique: true
|
|
39
|
+
},
|
|
40
|
+
email: {
|
|
41
|
+
type: sequelize_1.DataTypes.STRING(100),
|
|
42
|
+
allowNull: false,
|
|
43
|
+
unique: true
|
|
44
|
+
},
|
|
45
|
+
password: {
|
|
46
|
+
type: sequelize_1.DataTypes.STRING(255),
|
|
47
|
+
allowNull: false
|
|
48
|
+
}
|
|
49
|
+
}, {
|
|
50
|
+
...userTableOptions(sequelize)
|
|
51
|
+
});
|
|
52
|
+
return AuthUserModel;
|
|
53
|
+
}
|
|
54
|
+
class SequelizeUserStore extends base_js_1.UserStore {
|
|
55
|
+
constructor(options) {
|
|
56
|
+
super({
|
|
57
|
+
toPublic: options.toPublic,
|
|
58
|
+
bcryptRounds: options.bcryptRounds,
|
|
59
|
+
bcryptPepper: options.bcryptPepper
|
|
60
|
+
});
|
|
61
|
+
if (!options?.sequelize) {
|
|
62
|
+
throw new Error('SequelizeUserStore requires a configured Sequelize instance');
|
|
63
|
+
}
|
|
64
|
+
this.Users = options.userModel
|
|
65
|
+
? options.userModel
|
|
66
|
+
: (options.userModelFactory ?? initAuthUserModel)(options.sequelize);
|
|
67
|
+
this.recordMapper =
|
|
68
|
+
options.recordMapper ??
|
|
69
|
+
((model) => SequelizeUserStore.mapModelToUser(model));
|
|
70
|
+
}
|
|
71
|
+
async findUser(identifier) {
|
|
72
|
+
const where = [];
|
|
73
|
+
try {
|
|
74
|
+
where.push({ user_id: this.normalizeUserId(identifier) });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// ignore
|
|
78
|
+
}
|
|
79
|
+
if (typeof identifier === 'string') {
|
|
80
|
+
where.push({ login: identifier });
|
|
81
|
+
where.push({ email: identifier });
|
|
82
|
+
}
|
|
83
|
+
if (where.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const model = await this.Users.findOne({ where: { [sequelize_1.Op.or]: where } });
|
|
87
|
+
return model ? this.toUserRecord(model) : null;
|
|
88
|
+
}
|
|
89
|
+
async findById(id) {
|
|
90
|
+
try {
|
|
91
|
+
const model = await this.Users.findByPk(this.normalizeUserId(id));
|
|
92
|
+
return model ? this.toUserRecord(model) : null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async findByLoginOrEmail(loginOrEmail) {
|
|
99
|
+
const model = await this.Users.findOne({
|
|
100
|
+
where: { [sequelize_1.Op.or]: [{ login: loginOrEmail }, { email: loginOrEmail }] }
|
|
101
|
+
});
|
|
102
|
+
return model ? this.toUserRecord(model) : null;
|
|
103
|
+
}
|
|
104
|
+
async createUser(input) {
|
|
105
|
+
const normalized = this.normalizeUserInput(input);
|
|
106
|
+
const { password, ...rest } = normalized;
|
|
107
|
+
const defaults = {
|
|
108
|
+
...rest,
|
|
109
|
+
password: password ? await this.hashPassword(password) : ''
|
|
110
|
+
};
|
|
111
|
+
const providedId = input.user_id;
|
|
112
|
+
if (providedId !== undefined && providedId !== null && Number.isFinite(providedId)) {
|
|
113
|
+
defaults.user_id = Number(providedId);
|
|
114
|
+
}
|
|
115
|
+
const [model] = await this.Users.findOrCreate({
|
|
116
|
+
where: { login: rest.login },
|
|
117
|
+
defaults: defaults
|
|
118
|
+
});
|
|
119
|
+
return this.toUserRecord(model);
|
|
120
|
+
}
|
|
121
|
+
async upsertUser(input) {
|
|
122
|
+
const normalized = this.normalizeUserInput(input);
|
|
123
|
+
const providedId = input.user_id;
|
|
124
|
+
if (providedId !== undefined) {
|
|
125
|
+
const model = await this.Users.findByPk(Number(providedId));
|
|
126
|
+
if (!model) {
|
|
127
|
+
throw new Error(`User ${String(providedId)} not found`);
|
|
128
|
+
}
|
|
129
|
+
const next = { ...normalized };
|
|
130
|
+
if (normalized.password) {
|
|
131
|
+
next.password = await this.hashPassword(normalized.password);
|
|
132
|
+
}
|
|
133
|
+
await model.set(next);
|
|
134
|
+
await model.save();
|
|
135
|
+
return this.toUserRecord(model);
|
|
136
|
+
}
|
|
137
|
+
return this.createUser(input);
|
|
138
|
+
}
|
|
139
|
+
async updateUser(id, patch) {
|
|
140
|
+
const model = await this.Users.findByPk(this.normalizeUserId(id));
|
|
141
|
+
if (!model) {
|
|
142
|
+
throw new Error(`User ${String(id)} not found`);
|
|
143
|
+
}
|
|
144
|
+
const updates = { ...patch };
|
|
145
|
+
if (patch.password) {
|
|
146
|
+
updates.password = await this.hashPassword(patch.password);
|
|
147
|
+
}
|
|
148
|
+
await model.set(updates);
|
|
149
|
+
await model.save();
|
|
150
|
+
return this.toUserRecord(model);
|
|
151
|
+
}
|
|
152
|
+
async setPasswordHash(id, hash) {
|
|
153
|
+
await this.Users.update({ password: hash }, { where: { user_id: this.normalizeUserId(id) } });
|
|
154
|
+
}
|
|
155
|
+
getPasswordHash(user) {
|
|
156
|
+
return user.password;
|
|
157
|
+
}
|
|
158
|
+
getUserId(user) {
|
|
159
|
+
return user.user_id;
|
|
160
|
+
}
|
|
161
|
+
toUserRecord(model) {
|
|
162
|
+
return this.recordMapper(model);
|
|
163
|
+
}
|
|
164
|
+
static mapModelToUser(model) {
|
|
165
|
+
return {
|
|
166
|
+
user_id: model.get('user_id'),
|
|
167
|
+
login: model.get('login'),
|
|
168
|
+
email: model.get('email'),
|
|
169
|
+
password: model.get('password')
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
normalizeUserId(identifier) {
|
|
173
|
+
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
174
|
+
return identifier;
|
|
175
|
+
}
|
|
176
|
+
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
177
|
+
return Number(identifier);
|
|
178
|
+
}
|
|
179
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
exports.SequelizeUserStore = SequelizeUserStore;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BcryptHasherOptions {
|
|
2
|
+
rounds?: number;
|
|
3
|
+
pepper?: string;
|
|
4
|
+
}
|
|
5
|
+
export type CreateUserInput = Record<string, unknown> & {
|
|
6
|
+
login: string;
|
|
7
|
+
email: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
};
|
|
10
|
+
export type UpdateUserInput = Partial<CreateUserInput>;
|
|
11
|
+
export type PublicUserMapper<User, PublicUser> = (user: User) => PublicUser;
|
|
@@ -5,48 +5,39 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
import { Application, Request, Response } from 'express';
|
|
8
|
-
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
9
8
|
import { ApiModule } from './api-module.js';
|
|
9
|
+
import { TokenStore, type JwtDecodeResult, type JwtSignResult, type JwtVerifyResult } from './token/base.js';
|
|
10
10
|
import type { ApiAuthClass, ApiKey } from './api-module.js';
|
|
11
|
-
import type { AuthProviderModule } from './auth-module.js';
|
|
12
|
-
import type { AuthStorage, AuthIdentifier
|
|
11
|
+
import type { AuthProviderModule } from './auth-api/module.js';
|
|
12
|
+
import type { AuthStorage, AuthIdentifier } from './auth-api/types.js';
|
|
13
|
+
import type { OAuthStore } from './oauth/base.js';
|
|
14
|
+
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
|
|
15
|
+
import type { PasskeyService } from './passkey/service.js';
|
|
16
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
17
|
+
import type { Token } from './token/types.js';
|
|
18
|
+
import type { UserStore } from './user/base.js';
|
|
19
|
+
import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
13
20
|
export type { Application, Request, Response, NextFunction, Router } from 'express';
|
|
14
21
|
export type { Multer } from 'multer';
|
|
15
22
|
export type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
16
|
-
export interface
|
|
23
|
+
export interface ExtendedReq extends Request {
|
|
17
24
|
file?: Express.Multer.File;
|
|
18
25
|
files?: Express.Multer.File[] | {
|
|
19
26
|
[fieldname: string]: Express.Multer.File[];
|
|
20
27
|
};
|
|
21
28
|
}
|
|
22
|
-
interface
|
|
23
|
-
success: boolean;
|
|
24
|
-
token?: string;
|
|
25
|
-
error?: string;
|
|
26
|
-
}
|
|
27
|
-
interface JwtVerifyResult<T> {
|
|
28
|
-
success: boolean;
|
|
29
|
-
data?: T;
|
|
30
|
-
expired?: boolean;
|
|
31
|
-
error?: string;
|
|
32
|
-
}
|
|
33
|
-
interface JwtDecodeResult<T> {
|
|
34
|
-
success: boolean;
|
|
35
|
-
data?: T;
|
|
36
|
-
error?: string;
|
|
37
|
-
}
|
|
38
|
-
export interface ApiTokenData extends JwtPayload, AuthTokenMetadata {
|
|
29
|
+
export interface ApiTokenData extends JwtPayload, Partial<Token> {
|
|
39
30
|
uid: unknown;
|
|
40
31
|
iat?: number;
|
|
41
32
|
exp?: number;
|
|
42
33
|
}
|
|
43
34
|
export interface ApiRequest {
|
|
44
35
|
server: any;
|
|
45
|
-
req:
|
|
36
|
+
req: ExtendedReq;
|
|
46
37
|
res: Response;
|
|
47
38
|
tokenData?: ApiTokenData | null;
|
|
48
39
|
token?: string;
|
|
49
|
-
authToken?:
|
|
40
|
+
authToken?: Token | null;
|
|
50
41
|
apiKey?: ApiKey | null;
|
|
51
42
|
clientInfo?: ClientInfo;
|
|
52
43
|
realUid?: AuthIdentifier | null;
|
|
@@ -66,6 +57,16 @@ export interface ClientInfo extends ClientAgentProfile {
|
|
|
66
57
|
ip: string | null;
|
|
67
58
|
ipchain: string[];
|
|
68
59
|
}
|
|
60
|
+
export interface ApiServerAuthStores {
|
|
61
|
+
userStore: UserStore<any, any>;
|
|
62
|
+
tokenStore: TokenStore;
|
|
63
|
+
passkeyService?: PasskeyService;
|
|
64
|
+
oauthStore?: OAuthStore;
|
|
65
|
+
canImpersonate?: (params: {
|
|
66
|
+
realUserId: AuthIdentifier;
|
|
67
|
+
effectiveUserId: AuthIdentifier;
|
|
68
|
+
}) => boolean | Promise<boolean>;
|
|
69
|
+
}
|
|
69
70
|
export { ApiModule } from './api-module.js';
|
|
70
71
|
export type { ApiHandler, ApiAuthType, ApiAuthClass, ApiRoute, ApiKey } from './api-module.js';
|
|
71
72
|
export interface ApiErrorParams {
|
|
@@ -102,6 +103,8 @@ export interface ApiServerConf {
|
|
|
102
103
|
validateTokens: boolean;
|
|
103
104
|
apiVersion: string;
|
|
104
105
|
minClientVersion: string;
|
|
106
|
+
tokenStore?: TokenStore;
|
|
107
|
+
authStores?: ApiServerAuthStores;
|
|
105
108
|
}
|
|
106
109
|
export declare class ApiServer {
|
|
107
110
|
app: Application;
|
|
@@ -112,6 +115,12 @@ export declare class ApiServer {
|
|
|
112
115
|
private storageAdapter;
|
|
113
116
|
private moduleAdapter;
|
|
114
117
|
private apiNotFoundHandler;
|
|
118
|
+
private tokenStoreAdapter;
|
|
119
|
+
private userStoreAdapter;
|
|
120
|
+
private passkeyServiceAdapter;
|
|
121
|
+
private oauthStoreAdapter;
|
|
122
|
+
private canImpersonateAdapter;
|
|
123
|
+
private readonly jwtHelper;
|
|
115
124
|
constructor(config?: Partial<ApiServerConf>);
|
|
116
125
|
authStorage<UserRow, SafeUser>(storage: AuthStorage<UserRow, SafeUser>): this;
|
|
117
126
|
/**
|
|
@@ -125,20 +134,48 @@ export declare class ApiServer {
|
|
|
125
134
|
useAuthModule<UserRow>(module: AuthProviderModule<UserRow>): this;
|
|
126
135
|
getAuthStorage(): AuthStorage<any, any>;
|
|
127
136
|
getAuthModule(): AuthProviderModule<any>;
|
|
137
|
+
setTokenStore(store: TokenStore): this;
|
|
138
|
+
getTokenStore(): TokenStore | null;
|
|
139
|
+
private ensureUserStore;
|
|
140
|
+
private ensureTokenStore;
|
|
141
|
+
private ensurePasskeyService;
|
|
142
|
+
private ensureOAuthStore;
|
|
143
|
+
getUser(identifier: AuthIdentifier): Promise<any | null>;
|
|
144
|
+
getUserPasswordHash(user: any): string;
|
|
145
|
+
getUserId(user: any): AuthIdentifier;
|
|
146
|
+
filterUser(user: any): any;
|
|
147
|
+
verifyPassword(password: string, hash: string): Promise<boolean>;
|
|
148
|
+
storeToken(data: Token): Promise<void>;
|
|
149
|
+
getToken(query: Partial<Token> & {
|
|
150
|
+
userId?: AuthIdentifier;
|
|
151
|
+
ruid?: AuthIdentifier;
|
|
152
|
+
}, opts?: {
|
|
153
|
+
includeExpired?: boolean;
|
|
154
|
+
}): Promise<Token | null>;
|
|
155
|
+
deleteToken(query: Partial<Token> & {
|
|
156
|
+
userId?: AuthIdentifier;
|
|
157
|
+
ruid?: AuthIdentifier;
|
|
158
|
+
}): Promise<number>;
|
|
159
|
+
createPasskeyChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
160
|
+
verifyPasskeyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
161
|
+
getClient(clientId: string): Promise<OAuthClient | null>;
|
|
162
|
+
verifyClientSecret(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
|
|
163
|
+
createAuthCode(request: AuthCodeRequest): Promise<AuthCodeData>;
|
|
164
|
+
consumeAuthCode(code: string, clientId: string): Promise<AuthCodeData | null>;
|
|
165
|
+
canImpersonate(params: {
|
|
166
|
+
realUserId: AuthIdentifier;
|
|
167
|
+
effectiveUserId: AuthIdentifier;
|
|
168
|
+
}): Promise<boolean>;
|
|
128
169
|
jwtSign(payload: any, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
129
170
|
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
130
|
-
jwtDecode<T>(token: string, options?:
|
|
171
|
+
jwtDecode<T>(token: string, options?: import('jsonwebtoken').DecodeOptions): JwtDecodeResult<T>;
|
|
131
172
|
getApiKey<T = ApiKey>(token: string): Promise<T | null>;
|
|
132
173
|
authenticateUser(params: {
|
|
133
174
|
login: string;
|
|
134
175
|
password: string;
|
|
135
176
|
}): Promise<boolean>;
|
|
136
|
-
updateToken(updates: {
|
|
137
|
-
accessToken: string;
|
|
177
|
+
updateToken(updates: Partial<Token> & {
|
|
138
178
|
refreshToken: string;
|
|
139
|
-
expires?: Date;
|
|
140
|
-
clientId?: string;
|
|
141
|
-
scope?: string[];
|
|
142
179
|
}): Promise<boolean>;
|
|
143
180
|
guessExceptionText(error: any, defMsg?: string): string;
|
|
144
181
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|