@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,400 @@
|
|
|
1
|
+
import { DataTypes, Model, Op } from 'sequelize';
|
|
2
|
+
import { TokenStore } from './base.js';
|
|
3
|
+
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
4
|
+
class TokenModel extends Model {
|
|
5
|
+
}
|
|
6
|
+
function tokenTableOptions(sequelize) {
|
|
7
|
+
const opts = {
|
|
8
|
+
sequelize,
|
|
9
|
+
tableName: 'jwttokens',
|
|
10
|
+
timestamps: false
|
|
11
|
+
};
|
|
12
|
+
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
13
|
+
opts.charset = 'utf8mb4';
|
|
14
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
15
|
+
}
|
|
16
|
+
return opts;
|
|
17
|
+
}
|
|
18
|
+
function initTokenModel(sequelize) {
|
|
19
|
+
TokenModel.init({
|
|
20
|
+
token_id: {
|
|
21
|
+
type: DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())
|
|
22
|
+
? DataTypes.INTEGER.UNSIGNED
|
|
23
|
+
: DataTypes.INTEGER,
|
|
24
|
+
autoIncrement: true,
|
|
25
|
+
allowNull: false,
|
|
26
|
+
primaryKey: true
|
|
27
|
+
},
|
|
28
|
+
user_id: {
|
|
29
|
+
type: DataTypes.STRING(191),
|
|
30
|
+
allowNull: false
|
|
31
|
+
},
|
|
32
|
+
real_user_id: {
|
|
33
|
+
type: DataTypes.STRING(191),
|
|
34
|
+
allowNull: true,
|
|
35
|
+
defaultValue: null
|
|
36
|
+
},
|
|
37
|
+
expires: {
|
|
38
|
+
type: DataTypes.DATE,
|
|
39
|
+
allowNull: false
|
|
40
|
+
},
|
|
41
|
+
issued_at: {
|
|
42
|
+
type: DataTypes.DATE,
|
|
43
|
+
allowNull: false,
|
|
44
|
+
defaultValue: DataTypes.NOW
|
|
45
|
+
},
|
|
46
|
+
last_seen_at: {
|
|
47
|
+
type: DataTypes.DATE,
|
|
48
|
+
allowNull: false,
|
|
49
|
+
defaultValue: DataTypes.NOW
|
|
50
|
+
},
|
|
51
|
+
access: {
|
|
52
|
+
type: DataTypes.STRING(512),
|
|
53
|
+
allowNull: false
|
|
54
|
+
},
|
|
55
|
+
refresh: {
|
|
56
|
+
type: DataTypes.STRING(512),
|
|
57
|
+
allowNull: false
|
|
58
|
+
},
|
|
59
|
+
domain: {
|
|
60
|
+
type: DataTypes.STRING(64),
|
|
61
|
+
allowNull: false,
|
|
62
|
+
defaultValue: ''
|
|
63
|
+
},
|
|
64
|
+
fingerprint: {
|
|
65
|
+
type: DataTypes.STRING(64),
|
|
66
|
+
allowNull: false,
|
|
67
|
+
defaultValue: ''
|
|
68
|
+
},
|
|
69
|
+
label: {
|
|
70
|
+
type: DataTypes.STRING(128),
|
|
71
|
+
allowNull: false,
|
|
72
|
+
defaultValue: ''
|
|
73
|
+
},
|
|
74
|
+
browser: {
|
|
75
|
+
type: DataTypes.STRING(64),
|
|
76
|
+
allowNull: false,
|
|
77
|
+
defaultValue: ''
|
|
78
|
+
},
|
|
79
|
+
device: {
|
|
80
|
+
type: DataTypes.STRING(64),
|
|
81
|
+
allowNull: false,
|
|
82
|
+
defaultValue: ''
|
|
83
|
+
},
|
|
84
|
+
ip: {
|
|
85
|
+
type: DataTypes.STRING(45),
|
|
86
|
+
allowNull: false,
|
|
87
|
+
defaultValue: ''
|
|
88
|
+
},
|
|
89
|
+
os: {
|
|
90
|
+
type: DataTypes.STRING(64),
|
|
91
|
+
allowNull: false,
|
|
92
|
+
defaultValue: ''
|
|
93
|
+
},
|
|
94
|
+
login_type: {
|
|
95
|
+
type: DataTypes.STRING(64),
|
|
96
|
+
allowNull: false,
|
|
97
|
+
defaultValue: ''
|
|
98
|
+
},
|
|
99
|
+
refresh_ttl_seconds: {
|
|
100
|
+
type: DataTypes.INTEGER,
|
|
101
|
+
allowNull: true,
|
|
102
|
+
defaultValue: null
|
|
103
|
+
},
|
|
104
|
+
session_cookie: {
|
|
105
|
+
type: DataTypes.BOOLEAN,
|
|
106
|
+
allowNull: false,
|
|
107
|
+
defaultValue: false
|
|
108
|
+
},
|
|
109
|
+
client_id: {
|
|
110
|
+
type: DataTypes.STRING(128),
|
|
111
|
+
allowNull: true,
|
|
112
|
+
defaultValue: null
|
|
113
|
+
},
|
|
114
|
+
scope: {
|
|
115
|
+
type: DataTypes.TEXT,
|
|
116
|
+
allowNull: false,
|
|
117
|
+
defaultValue: '[]'
|
|
118
|
+
}
|
|
119
|
+
}, {
|
|
120
|
+
...tokenTableOptions(sequelize),
|
|
121
|
+
indexes: [
|
|
122
|
+
{ name: 'jwt_access_unique', unique: true, fields: ['access'] },
|
|
123
|
+
{ name: 'jwt_refresh_unique', unique: true, fields: ['refresh'] }
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
return TokenModel;
|
|
127
|
+
}
|
|
128
|
+
export class SequelizeTokenStore extends TokenStore {
|
|
129
|
+
constructor(options) {
|
|
130
|
+
super();
|
|
131
|
+
if (!options?.sequelize) {
|
|
132
|
+
throw new Error('SequelizeTokenStore requires an initialised Sequelize instance');
|
|
133
|
+
}
|
|
134
|
+
this.Tokens = options.tokenModel ?? (options.tokenModelFactory ?? initTokenModel)(options.sequelize);
|
|
135
|
+
}
|
|
136
|
+
async save(record) {
|
|
137
|
+
const normalized = this.normalizeToken(record);
|
|
138
|
+
const resolvedUserId = this.normalizeUserId(normalized.userId);
|
|
139
|
+
const resolvedRealUserId = this.resolveRealUserId(normalized.ruid);
|
|
140
|
+
const domain = normalized.domain;
|
|
141
|
+
const fingerprint = normalized.fingerprint;
|
|
142
|
+
const browser = normalized.browser;
|
|
143
|
+
const device = normalized.device;
|
|
144
|
+
const ip = normalized.ip;
|
|
145
|
+
const os = normalized.os;
|
|
146
|
+
const label = normalized.label;
|
|
147
|
+
const loginType = normalized.loginType ?? '';
|
|
148
|
+
const refreshTtlSeconds = typeof normalized.refreshTtlSeconds === 'number' && normalized.refreshTtlSeconds > 0
|
|
149
|
+
? Math.floor(normalized.refreshTtlSeconds)
|
|
150
|
+
: null;
|
|
151
|
+
const issuedAt = normalized.issuedAt ?? new Date();
|
|
152
|
+
const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
|
|
153
|
+
const sessionCookie = normalized.sessionCookie ?? false;
|
|
154
|
+
const removalWhere = { user_id: resolvedUserId };
|
|
155
|
+
if (normalized.domain !== undefined && record.domain !== undefined) {
|
|
156
|
+
removalWhere.domain = domain;
|
|
157
|
+
}
|
|
158
|
+
if (normalized.fingerprint !== undefined && record.fingerprint !== undefined) {
|
|
159
|
+
removalWhere.fingerprint = fingerprint;
|
|
160
|
+
}
|
|
161
|
+
if (normalized.clientId) {
|
|
162
|
+
removalWhere.client_id = normalized.clientId;
|
|
163
|
+
}
|
|
164
|
+
await this.Tokens.destroy({ where: removalWhere });
|
|
165
|
+
await this.Tokens.create({
|
|
166
|
+
user_id: resolvedUserId,
|
|
167
|
+
real_user_id: resolvedRealUserId,
|
|
168
|
+
access: normalized.accessToken ?? '',
|
|
169
|
+
refresh: normalized.refreshToken,
|
|
170
|
+
expires: normalized.expires,
|
|
171
|
+
issued_at: issuedAt,
|
|
172
|
+
last_seen_at: lastSeenAt,
|
|
173
|
+
domain,
|
|
174
|
+
fingerprint,
|
|
175
|
+
label,
|
|
176
|
+
browser,
|
|
177
|
+
device,
|
|
178
|
+
ip,
|
|
179
|
+
os,
|
|
180
|
+
client_id: normalized.clientId ?? null,
|
|
181
|
+
scope: this.encodeScope(normalized.scope),
|
|
182
|
+
login_type: loginType,
|
|
183
|
+
refresh_ttl_seconds: refreshTtlSeconds,
|
|
184
|
+
session_cookie: sessionCookie
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async get(query, opts) {
|
|
188
|
+
if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
|
|
189
|
+
throw new Error('At least one token lookup field must be provided');
|
|
190
|
+
}
|
|
191
|
+
const where = {};
|
|
192
|
+
if (query.refreshToken) {
|
|
193
|
+
where.refresh = query.refreshToken;
|
|
194
|
+
}
|
|
195
|
+
if (query.accessToken) {
|
|
196
|
+
where.access = query.accessToken;
|
|
197
|
+
}
|
|
198
|
+
if (query.userId !== undefined) {
|
|
199
|
+
where.user_id = this.normalizeUserId(query.userId);
|
|
200
|
+
}
|
|
201
|
+
if (query.clientId) {
|
|
202
|
+
where.client_id = query.clientId;
|
|
203
|
+
}
|
|
204
|
+
if (query.domain !== undefined && query.domain !== null) {
|
|
205
|
+
where.domain = query.domain;
|
|
206
|
+
}
|
|
207
|
+
if (query.fingerprint !== undefined && query.fingerprint !== null) {
|
|
208
|
+
where.fingerprint = query.fingerprint;
|
|
209
|
+
}
|
|
210
|
+
if (query.label) {
|
|
211
|
+
where.label = query.label;
|
|
212
|
+
}
|
|
213
|
+
if (!(opts?.includeExpired ?? false)) {
|
|
214
|
+
where.expires = { [Op.gt]: new Date() };
|
|
215
|
+
}
|
|
216
|
+
const model = await this.Tokens.findOne({ where });
|
|
217
|
+
return model ? this.toTokenRecord(model) : null;
|
|
218
|
+
}
|
|
219
|
+
async delete(query) {
|
|
220
|
+
if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
const where = {};
|
|
224
|
+
if (query.refreshToken) {
|
|
225
|
+
where.refresh = query.refreshToken;
|
|
226
|
+
}
|
|
227
|
+
if (query.accessToken) {
|
|
228
|
+
where.access = query.accessToken;
|
|
229
|
+
}
|
|
230
|
+
if (query.userId !== undefined) {
|
|
231
|
+
where.user_id = this.normalizeUserId(query.userId);
|
|
232
|
+
}
|
|
233
|
+
if (query.clientId) {
|
|
234
|
+
where.client_id = query.clientId;
|
|
235
|
+
}
|
|
236
|
+
if (query.domain !== undefined && query.domain !== null) {
|
|
237
|
+
where.domain = query.domain;
|
|
238
|
+
}
|
|
239
|
+
if (query.fingerprint !== undefined && query.fingerprint !== null) {
|
|
240
|
+
where.fingerprint = query.fingerprint;
|
|
241
|
+
}
|
|
242
|
+
if (query.label) {
|
|
243
|
+
where.label = query.label;
|
|
244
|
+
}
|
|
245
|
+
return this.Tokens.destroy({ where });
|
|
246
|
+
}
|
|
247
|
+
async update(params) {
|
|
248
|
+
const where = { refresh: params.refreshToken };
|
|
249
|
+
if (params.clientId) {
|
|
250
|
+
where.client_id = params.clientId;
|
|
251
|
+
}
|
|
252
|
+
const updates = {};
|
|
253
|
+
if (params.accessToken !== undefined) {
|
|
254
|
+
updates.access = params.accessToken ?? null;
|
|
255
|
+
}
|
|
256
|
+
if (params.expires !== undefined) {
|
|
257
|
+
updates.expires = params.expires ?? null;
|
|
258
|
+
}
|
|
259
|
+
if (params.scope !== undefined) {
|
|
260
|
+
updates.scope = this.encodeScope(params.scope);
|
|
261
|
+
}
|
|
262
|
+
if (params.label !== undefined) {
|
|
263
|
+
updates.label = params.label ?? '';
|
|
264
|
+
}
|
|
265
|
+
if (params.domain !== undefined) {
|
|
266
|
+
updates.domain = params.domain ?? '';
|
|
267
|
+
}
|
|
268
|
+
if (params.fingerprint !== undefined) {
|
|
269
|
+
updates.fingerprint = params.fingerprint ?? '';
|
|
270
|
+
}
|
|
271
|
+
if (params.browser !== undefined) {
|
|
272
|
+
updates.browser = params.browser ?? '';
|
|
273
|
+
}
|
|
274
|
+
if (params.device !== undefined) {
|
|
275
|
+
updates.device = params.device ?? '';
|
|
276
|
+
}
|
|
277
|
+
if (params.ip !== undefined) {
|
|
278
|
+
updates.ip = params.ip ?? '';
|
|
279
|
+
}
|
|
280
|
+
if (params.os !== undefined) {
|
|
281
|
+
updates.os = params.os ?? '';
|
|
282
|
+
}
|
|
283
|
+
if (params.refreshTtlSeconds !== undefined) {
|
|
284
|
+
updates.refresh_ttl_seconds =
|
|
285
|
+
typeof params.refreshTtlSeconds === 'number' && params.refreshTtlSeconds > 0
|
|
286
|
+
? Math.floor(params.refreshTtlSeconds)
|
|
287
|
+
: null;
|
|
288
|
+
}
|
|
289
|
+
if (params.loginType !== undefined) {
|
|
290
|
+
updates.login_type = params.loginType ?? '';
|
|
291
|
+
}
|
|
292
|
+
if (params.sessionCookie !== undefined) {
|
|
293
|
+
updates.session_cookie = params.sessionCookie;
|
|
294
|
+
}
|
|
295
|
+
if (params.issuedAt !== undefined) {
|
|
296
|
+
updates.issued_at = params.issuedAt ?? null;
|
|
297
|
+
}
|
|
298
|
+
if (params.lastSeenAt !== undefined) {
|
|
299
|
+
updates.last_seen_at = params.lastSeenAt ?? null;
|
|
300
|
+
}
|
|
301
|
+
if (Object.keys(updates).length === 0) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const [updated] = await this.Tokens.update(updates, { where });
|
|
305
|
+
return updated > 0;
|
|
306
|
+
}
|
|
307
|
+
async list(userId, opts = {}) {
|
|
308
|
+
const where = { user_id: this.normalizeUserId(userId) };
|
|
309
|
+
if (!(opts.includeExpired ?? false)) {
|
|
310
|
+
where.expires = { [Op.gt]: new Date() };
|
|
311
|
+
}
|
|
312
|
+
const models = await this.Tokens.findAll({
|
|
313
|
+
where,
|
|
314
|
+
order: [['issued_at', 'DESC']],
|
|
315
|
+
limit: opts.limit,
|
|
316
|
+
offset: opts.offset
|
|
317
|
+
});
|
|
318
|
+
return models.map((model) => this.toTokenRecord(model));
|
|
319
|
+
}
|
|
320
|
+
async close() {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
normalizeUserId(identifier) {
|
|
324
|
+
if (identifier === undefined || identifier === null) {
|
|
325
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
326
|
+
}
|
|
327
|
+
return String(identifier);
|
|
328
|
+
}
|
|
329
|
+
resolveRealUserId(ruid) {
|
|
330
|
+
if (ruid === undefined || ruid === null) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
const value = String(ruid);
|
|
334
|
+
if (value.length === 0 || value === '0') {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
encodeStringArray(values) {
|
|
340
|
+
return JSON.stringify(values ?? []);
|
|
341
|
+
}
|
|
342
|
+
decodeStringArray(raw) {
|
|
343
|
+
if (!raw) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(raw);
|
|
348
|
+
if (Array.isArray(parsed)) {
|
|
349
|
+
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// ignore malformed values
|
|
354
|
+
}
|
|
355
|
+
return raw
|
|
356
|
+
.split(/\s+/)
|
|
357
|
+
.map((entry) => entry.trim())
|
|
358
|
+
.filter((entry) => entry.length > 0);
|
|
359
|
+
}
|
|
360
|
+
encodeScope(scope) {
|
|
361
|
+
if (!scope || (Array.isArray(scope) && scope.length === 0)) {
|
|
362
|
+
return '[]';
|
|
363
|
+
}
|
|
364
|
+
if (Array.isArray(scope)) {
|
|
365
|
+
return this.encodeStringArray(scope);
|
|
366
|
+
}
|
|
367
|
+
return this.encodeStringArray(scope.split(/\s+/).filter((entry) => entry.length > 0));
|
|
368
|
+
}
|
|
369
|
+
decodeScope(raw) {
|
|
370
|
+
return this.decodeStringArray(raw);
|
|
371
|
+
}
|
|
372
|
+
toTokenRecord(model) {
|
|
373
|
+
const scope = this.decodeScope(model.scope);
|
|
374
|
+
const normalized = this.normalizeToken({
|
|
375
|
+
userId: model.user_id,
|
|
376
|
+
refreshToken: model.refresh,
|
|
377
|
+
accessToken: model.access,
|
|
378
|
+
expires: model.expires,
|
|
379
|
+
issuedAt: model.issued_at,
|
|
380
|
+
lastSeenAt: model.last_seen_at,
|
|
381
|
+
domain: model.domain,
|
|
382
|
+
fingerprint: model.fingerprint,
|
|
383
|
+
label: model.label,
|
|
384
|
+
browser: model.browser,
|
|
385
|
+
device: model.device,
|
|
386
|
+
ip: model.ip,
|
|
387
|
+
os: model.os,
|
|
388
|
+
clientId: model.client_id ?? undefined,
|
|
389
|
+
scope,
|
|
390
|
+
loginType: model.login_type || undefined,
|
|
391
|
+
refreshTtlSeconds: model.refresh_ttl_seconds ?? undefined,
|
|
392
|
+
ruid: model.real_user_id ?? undefined,
|
|
393
|
+
sessionCookie: model.session_cookie
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
...normalized,
|
|
397
|
+
scope: normalized.scope ? [...normalized.scope] : undefined
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TokenStatus = 'active' | 'expired' | 'revoked';
|
|
2
|
+
export interface Token {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
expires?: Date;
|
|
7
|
+
issuedAt?: Date;
|
|
8
|
+
lastSeenAt?: Date;
|
|
9
|
+
status?: TokenStatus;
|
|
10
|
+
ruid?: string;
|
|
11
|
+
clientId?: string;
|
|
12
|
+
domain?: string;
|
|
13
|
+
fingerprint?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
browser?: string;
|
|
16
|
+
device?: string;
|
|
17
|
+
ip?: string;
|
|
18
|
+
os?: string;
|
|
19
|
+
scope?: string | string[];
|
|
20
|
+
loginType?: string;
|
|
21
|
+
refreshTtlSeconds?: number;
|
|
22
|
+
sessionCookie?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface TokenPair {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
|
|
2
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
export declare abstract class UserStore<User, PublicUser> {
|
|
4
|
+
protected readonly toPublicUser: PublicUserMapper<User, PublicUser>;
|
|
5
|
+
private readonly bcryptRounds;
|
|
6
|
+
private readonly bcryptPepper?;
|
|
7
|
+
constructor(opts?: {
|
|
8
|
+
toPublic?: PublicUserMapper<User, PublicUser>;
|
|
9
|
+
bcryptRounds?: number;
|
|
10
|
+
bcryptPepper?: string;
|
|
11
|
+
});
|
|
12
|
+
protected hashPassword(plain: string): Promise<string>;
|
|
13
|
+
verifyPassword(plain: string, hashed: string): Promise<boolean>;
|
|
14
|
+
protected normalizeUserInput(input: Partial<CreateUserInput>): CreateUserInput;
|
|
15
|
+
abstract findUser(identifier: AuthIdentifier | string): Promise<User | null>;
|
|
16
|
+
abstract findById(id: AuthIdentifier): Promise<User | null>;
|
|
17
|
+
abstract findByLoginOrEmail(loginOrEmail: string): Promise<User | null>;
|
|
18
|
+
abstract createUser(input: CreateUserInput): Promise<User>;
|
|
19
|
+
abstract upsertUser(input: CreateUserInput): Promise<User>;
|
|
20
|
+
abstract updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<User>;
|
|
21
|
+
abstract setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
22
|
+
abstract getPasswordHash(user: User): string | null;
|
|
23
|
+
abstract getUserId(user: User): AuthIdentifier;
|
|
24
|
+
toPublic(user: User): PublicUser;
|
|
25
|
+
}
|
|
26
|
+
export type { CreateUserInput, UpdateUserInput, PublicUserMapper } from './types.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs';
|
|
2
|
+
export class UserStore {
|
|
3
|
+
constructor(opts = {}) {
|
|
4
|
+
this.toPublicUser = opts.toPublic ?? ((u) => u);
|
|
5
|
+
const rounds = typeof opts.bcryptRounds === 'number' && Number.isFinite(opts.bcryptRounds)
|
|
6
|
+
? Math.max(4, Math.floor(opts.bcryptRounds))
|
|
7
|
+
: 12;
|
|
8
|
+
this.bcryptRounds = rounds;
|
|
9
|
+
this.bcryptPepper =
|
|
10
|
+
typeof opts.bcryptPepper === 'string' && opts.bcryptPepper.length > 0 ? opts.bcryptPepper : undefined;
|
|
11
|
+
}
|
|
12
|
+
async hashPassword(plain) {
|
|
13
|
+
const candidate = this.bcryptPepper ? `${plain}${this.bcryptPepper}` : plain;
|
|
14
|
+
return bcrypt.hash(candidate, this.bcryptRounds);
|
|
15
|
+
}
|
|
16
|
+
async verifyPassword(plain, hashed) {
|
|
17
|
+
const candidate = this.bcryptPepper ? `${plain}${this.bcryptPepper}` : plain;
|
|
18
|
+
return bcrypt.compare(candidate, hashed);
|
|
19
|
+
}
|
|
20
|
+
normalizeUserInput(input) {
|
|
21
|
+
const login = typeof input.login === 'string' ? input.login.trim() : '';
|
|
22
|
+
const email = typeof input.email === 'string' ? input.email.trim() : '';
|
|
23
|
+
if (!login || !email) {
|
|
24
|
+
throw new Error('login and email are required');
|
|
25
|
+
}
|
|
26
|
+
const password = typeof input.password === 'string' ? input.password : undefined;
|
|
27
|
+
return { ...input, login, email, password };
|
|
28
|
+
}
|
|
29
|
+
toPublic(user) {
|
|
30
|
+
const mapped = this.toPublicUser(user);
|
|
31
|
+
if (mapped && typeof mapped === 'object') {
|
|
32
|
+
const { password: _password, ...rest } = mapped;
|
|
33
|
+
void _password;
|
|
34
|
+
return rest;
|
|
35
|
+
}
|
|
36
|
+
return mapped;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -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,169 @@
|
|
|
1
|
+
import { UserStore } from './base.js';
|
|
2
|
+
function cloneUser(user) {
|
|
3
|
+
return { ...user };
|
|
4
|
+
}
|
|
5
|
+
function normalizeUserId(identifier) {
|
|
6
|
+
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
7
|
+
return identifier;
|
|
8
|
+
}
|
|
9
|
+
if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
|
|
10
|
+
return Number(identifier);
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
13
|
+
}
|
|
14
|
+
export class MemoryUserStore extends UserStore {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
super({
|
|
17
|
+
toPublic: options.toPublic,
|
|
18
|
+
bcryptRounds: options.bcryptRounds,
|
|
19
|
+
bcryptPepper: options.bcryptPepper
|
|
20
|
+
});
|
|
21
|
+
this.usersById = new Map();
|
|
22
|
+
this.loginToId = new Map();
|
|
23
|
+
this.emailToId = new Map();
|
|
24
|
+
this.nextUserId = Number.isFinite(options.startingUserId) ? Number(options.startingUserId) : 1;
|
|
25
|
+
this.userIdFactory =
|
|
26
|
+
options.userIdFactory ??
|
|
27
|
+
(() => {
|
|
28
|
+
const id = this.nextUserId;
|
|
29
|
+
this.nextUserId += 1;
|
|
30
|
+
return id;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async findUser(identifier) {
|
|
34
|
+
if (typeof identifier === 'number') {
|
|
35
|
+
const user = this.usersById.get(identifier);
|
|
36
|
+
return user ? cloneUser(user) : null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof identifier === 'string') {
|
|
39
|
+
const numeric = /^\d+$/.test(identifier) ? Number(identifier) : null;
|
|
40
|
+
if (numeric !== null) {
|
|
41
|
+
const user = this.usersById.get(numeric);
|
|
42
|
+
if (user) {
|
|
43
|
+
return cloneUser(user);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const loginId = this.loginToId.get(identifier);
|
|
47
|
+
if (loginId !== undefined) {
|
|
48
|
+
return cloneUser(this.usersById.get(loginId));
|
|
49
|
+
}
|
|
50
|
+
const emailId = this.emailToId.get(identifier);
|
|
51
|
+
if (emailId !== undefined) {
|
|
52
|
+
return cloneUser(this.usersById.get(emailId));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async findById(id) {
|
|
58
|
+
try {
|
|
59
|
+
const numeric = normalizeUserId(id);
|
|
60
|
+
const user = this.usersById.get(numeric);
|
|
61
|
+
return user ? cloneUser(user) : null;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async findByLoginOrEmail(loginOrEmail) {
|
|
68
|
+
return this.findUser(loginOrEmail);
|
|
69
|
+
}
|
|
70
|
+
async createUser(input) {
|
|
71
|
+
const normalizedInput = this.normalizeUserInput(input);
|
|
72
|
+
const providedId = input.user_id;
|
|
73
|
+
const userId = typeof providedId === 'number' && Number.isFinite(providedId) ? providedId : this.userIdFactory();
|
|
74
|
+
if (this.usersById.has(userId)) {
|
|
75
|
+
throw new Error(`User ${userId} already exists`);
|
|
76
|
+
}
|
|
77
|
+
if (this.loginToId.has(normalizedInput.login)) {
|
|
78
|
+
throw new Error(`User with login ${normalizedInput.login} already exists`);
|
|
79
|
+
}
|
|
80
|
+
if (this.emailToId.has(normalizedInput.email)) {
|
|
81
|
+
throw new Error(`User with email ${normalizedInput.email} already exists`);
|
|
82
|
+
}
|
|
83
|
+
const passwordHash = normalizedInput.password ? await this.hashPassword(normalizedInput.password) : '';
|
|
84
|
+
const record = {
|
|
85
|
+
...normalizedInput,
|
|
86
|
+
user_id: userId,
|
|
87
|
+
password: passwordHash
|
|
88
|
+
};
|
|
89
|
+
this.persistUser(record);
|
|
90
|
+
if (typeof providedId === 'number' && Number.isFinite(providedId)) {
|
|
91
|
+
this.nextUserId = Math.max(this.nextUserId, providedId + 1);
|
|
92
|
+
}
|
|
93
|
+
return cloneUser(record);
|
|
94
|
+
}
|
|
95
|
+
async upsertUser(input) {
|
|
96
|
+
const normalizedInput = this.normalizeUserInput(input);
|
|
97
|
+
const providedId = input.user_id;
|
|
98
|
+
if (providedId !== undefined) {
|
|
99
|
+
const existing = this.usersById.get(providedId);
|
|
100
|
+
if (!existing) {
|
|
101
|
+
throw new Error(`User ${providedId} not found`);
|
|
102
|
+
}
|
|
103
|
+
const updates = {
|
|
104
|
+
...existing,
|
|
105
|
+
...normalizedInput
|
|
106
|
+
};
|
|
107
|
+
if (updates.login !== existing.login) {
|
|
108
|
+
const loginOwner = this.loginToId.get(updates.login);
|
|
109
|
+
if (loginOwner !== undefined && loginOwner !== existing.user_id) {
|
|
110
|
+
throw new Error(`User with login ${updates.login} already exists`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (updates.email !== existing.email) {
|
|
114
|
+
const emailOwner = this.emailToId.get(updates.email);
|
|
115
|
+
if (emailOwner !== undefined && emailOwner !== existing.user_id) {
|
|
116
|
+
throw new Error(`User with email ${updates.email} already exists`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (normalizedInput.password) {
|
|
120
|
+
updates.password = await this.hashPassword(normalizedInput.password);
|
|
121
|
+
}
|
|
122
|
+
this.persistUser(updates);
|
|
123
|
+
return cloneUser(updates);
|
|
124
|
+
}
|
|
125
|
+
return this.createUser(input);
|
|
126
|
+
}
|
|
127
|
+
async updateUser(id, patch) {
|
|
128
|
+
const user = await this.findById(id);
|
|
129
|
+
if (!user) {
|
|
130
|
+
throw new Error(`User ${String(id)} not found`);
|
|
131
|
+
}
|
|
132
|
+
const updates = { ...user, ...patch };
|
|
133
|
+
if (patch.password) {
|
|
134
|
+
updates.password = await this.hashPassword(patch.password);
|
|
135
|
+
}
|
|
136
|
+
this.persistUser(updates);
|
|
137
|
+
return cloneUser(updates);
|
|
138
|
+
}
|
|
139
|
+
async setPasswordHash(id, hash) {
|
|
140
|
+
const user = await this.findById(id);
|
|
141
|
+
if (!user) {
|
|
142
|
+
throw new Error(`User ${String(id)} not found`);
|
|
143
|
+
}
|
|
144
|
+
const updates = { ...user, password: hash };
|
|
145
|
+
this.persistUser(updates);
|
|
146
|
+
}
|
|
147
|
+
getPasswordHash(user) {
|
|
148
|
+
return user.password;
|
|
149
|
+
}
|
|
150
|
+
getUserId(user) {
|
|
151
|
+
return user.user_id;
|
|
152
|
+
}
|
|
153
|
+
persistUser(user) {
|
|
154
|
+
const snapshot = { ...user };
|
|
155
|
+
this.usersById.set(user.user_id, snapshot);
|
|
156
|
+
for (const [login, id] of [...this.loginToId.entries()]) {
|
|
157
|
+
if (id === user.user_id && login !== user.login) {
|
|
158
|
+
this.loginToId.delete(login);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [email, id] of [...this.emailToId.entries()]) {
|
|
162
|
+
if (id === user.user_id && email !== user.email) {
|
|
163
|
+
this.emailToId.delete(email);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.loginToId.set(user.login, user.user_id);
|
|
167
|
+
this.emailToId.set(user.email, user.user_id);
|
|
168
|
+
}
|
|
169
|
+
}
|