@technomoron/apicore-server 1.0.0-beta.1
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 +21 -0
- package/dist/cjs/api-module.cjs +34 -0
- package/dist/cjs/api-module.d.ts +45 -0
- package/dist/cjs/apicore-server.cjs +1561 -0
- package/dist/cjs/apicore-server.d.ts +288 -0
- package/dist/cjs/auth-api/auth-module.cjs +1248 -0
- package/dist/cjs/auth-api/auth-module.d.ts +116 -0
- package/dist/cjs/auth-api/compat-auth-storage.cjs +128 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/cjs/auth-api/mem-auth-store.cjs +121 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/cjs/auth-api/module.cjs +25 -0
- package/dist/cjs/auth-api/module.d.ts +20 -0
- package/dist/cjs/auth-api/schemas.cjs +171 -0
- package/dist/cjs/auth-api/schemas.d.ts +21 -0
- package/dist/cjs/auth-api/sql-auth-store.cjs +179 -0
- package/dist/cjs/auth-api/sql-auth-store.d.ts +87 -0
- package/dist/cjs/auth-api/storage.cjs +102 -0
- package/dist/cjs/auth-api/storage.d.ts +38 -0
- package/dist/cjs/auth-api/types.cjs +2 -0
- package/dist/cjs/auth-api/types.d.ts +34 -0
- package/dist/cjs/auth-api/user-id.cjs +47 -0
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-cookie-options.cjs +66 -0
- package/dist/cjs/auth-cookie-options.d.ts +13 -0
- package/dist/cjs/base/client-info.cjs +285 -0
- package/dist/cjs/base/client-info.d.ts +27 -0
- package/dist/cjs/base/error-utils.cjs +50 -0
- package/dist/cjs/base/error-utils.d.ts +16 -0
- package/dist/cjs/base/request-utils.cjs +27 -0
- package/dist/cjs/base/request-utils.d.ts +8 -0
- package/dist/cjs/index.cjs +51 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/limiter/auth-rate-limiter.cjs +35 -0
- package/dist/cjs/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/cjs/limiter/fixed-window.cjs +41 -0
- package/dist/cjs/limiter/fixed-window.d.ts +11 -0
- package/dist/cjs/oauth/base.cjs +7 -0
- package/dist/cjs/oauth/base.d.ts +17 -0
- package/dist/cjs/oauth/memory.cjs +135 -0
- package/dist/cjs/oauth/memory.d.ts +22 -0
- package/dist/cjs/oauth/models.cjs +47 -0
- package/dist/cjs/oauth/models.d.ts +50 -0
- package/dist/cjs/oauth/sequelize.cjs +159 -0
- package/dist/cjs/oauth/sequelize.d.ts +30 -0
- package/dist/cjs/oauth/types.cjs +3 -0
- package/dist/cjs/oauth/types.d.ts +51 -0
- package/dist/cjs/passkey/base.cjs +7 -0
- package/dist/cjs/passkey/base.d.ts +28 -0
- package/dist/cjs/passkey/config.cjs +26 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/memory.cjs +123 -0
- package/dist/cjs/passkey/memory.d.ts +34 -0
- package/dist/cjs/passkey/models.cjs +142 -0
- package/dist/cjs/passkey/models.d.ts +34 -0
- package/dist/cjs/passkey/sequelize.cjs +126 -0
- package/dist/cjs/passkey/sequelize.d.ts +42 -0
- package/dist/cjs/passkey/service.cjs +413 -0
- package/dist/cjs/passkey/service.d.ts +21 -0
- package/dist/cjs/passkey/types.cjs +2 -0
- package/dist/cjs/passkey/types.d.ts +84 -0
- package/dist/cjs/sequelize-utils.cjs +56 -0
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/token/base.cjs +120 -0
- package/dist/cjs/token/base.d.ts +46 -0
- package/dist/cjs/token/memory.cjs +234 -0
- package/dist/cjs/token/memory.d.ts +29 -0
- package/dist/cjs/token/sequelize.cjs +400 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/types.cjs +2 -0
- package/dist/cjs/token/types.d.ts +34 -0
- package/dist/cjs/upload/memory.cjs +92 -0
- package/dist/cjs/upload/memory.d.ts +17 -0
- package/dist/cjs/upload/tus-module.cjs +270 -0
- package/dist/cjs/upload/tus-module.d.ts +38 -0
- package/dist/cjs/upload/types.cjs +2 -0
- package/dist/cjs/upload/types.d.ts +28 -0
- package/dist/cjs/user/base.cjs +53 -0
- package/dist/cjs/user/base.d.ts +36 -0
- package/dist/cjs/user/memory.cjs +194 -0
- package/dist/cjs/user/memory.d.ts +37 -0
- package/dist/cjs/user/sequelize.cjs +194 -0
- package/dist/cjs/user/sequelize.d.ts +46 -0
- package/dist/cjs/user/types.cjs +2 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/esm/api-module.d.ts +45 -0
- package/dist/esm/api-module.js +30 -0
- package/dist/esm/apicore-server.d.ts +288 -0
- package/dist/esm/apicore-server.js +1552 -0
- package/dist/esm/auth-api/auth-module.d.ts +116 -0
- package/dist/esm/auth-api/auth-module.js +1246 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/esm/auth-api/compat-auth-storage.js +124 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/esm/auth-api/mem-auth-store.js +117 -0
- package/dist/esm/auth-api/module.d.ts +20 -0
- package/dist/esm/auth-api/module.js +21 -0
- package/dist/esm/auth-api/schemas.d.ts +21 -0
- package/dist/esm/auth-api/schemas.js +168 -0
- package/dist/esm/auth-api/sql-auth-store.d.ts +87 -0
- package/dist/esm/auth-api/sql-auth-store.js +175 -0
- package/dist/esm/auth-api/storage.d.ts +38 -0
- package/dist/esm/auth-api/storage.js +98 -0
- package/dist/esm/auth-api/types.d.ts +34 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +41 -0
- package/dist/esm/auth-cookie-options.d.ts +13 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/base/client-info.d.ts +27 -0
- package/dist/esm/base/client-info.js +282 -0
- package/dist/esm/base/error-utils.d.ts +16 -0
- package/dist/esm/base/error-utils.js +44 -0
- package/dist/esm/base/request-utils.d.ts +8 -0
- package/dist/esm/base/request-utils.js +23 -0
- package/dist/esm/index.d.ts +34 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/esm/limiter/auth-rate-limiter.js +32 -0
- package/dist/esm/limiter/fixed-window.d.ts +11 -0
- package/dist/esm/limiter/fixed-window.js +37 -0
- package/dist/esm/oauth/base.d.ts +17 -0
- package/dist/esm/oauth/base.js +3 -0
- package/dist/esm/oauth/memory.d.ts +22 -0
- package/dist/esm/oauth/memory.js +128 -0
- package/dist/esm/oauth/models.d.ts +50 -0
- package/dist/esm/oauth/models.js +38 -0
- package/dist/esm/oauth/sequelize.d.ts +30 -0
- package/dist/esm/oauth/sequelize.js +148 -0
- package/dist/esm/oauth/types.d.ts +51 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +28 -0
- package/dist/esm/passkey/base.js +3 -0
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.d.ts +34 -0
- package/dist/esm/passkey/memory.js +119 -0
- package/dist/esm/passkey/models.d.ts +34 -0
- package/dist/esm/passkey/models.js +135 -0
- package/dist/esm/passkey/sequelize.d.ts +42 -0
- package/dist/esm/passkey/sequelize.js +122 -0
- package/dist/esm/passkey/service.d.ts +21 -0
- package/dist/esm/passkey/service.js +376 -0
- package/dist/esm/passkey/types.d.ts +84 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +47 -0
- package/dist/esm/token/base.d.ts +46 -0
- package/dist/esm/token/base.js +113 -0
- package/dist/esm/token/memory.d.ts +29 -0
- package/dist/esm/token/memory.js +230 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +396 -0
- package/dist/esm/token/types.d.ts +34 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/upload/memory.d.ts +17 -0
- package/dist/esm/upload/memory.js +86 -0
- package/dist/esm/upload/tus-module.d.ts +38 -0
- package/dist/esm/upload/tus-module.js +266 -0
- package/dist/esm/upload/types.d.ts +28 -0
- package/dist/esm/upload/types.js +1 -0
- package/dist/esm/user/base.d.ts +36 -0
- package/dist/esm/user/base.js +46 -0
- package/dist/esm/user/memory.d.ts +37 -0
- package/dist/esm/user/memory.js +190 -0
- package/dist/esm/user/sequelize.d.ts +46 -0
- package/dist/esm/user/sequelize.js +188 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/docs/swagger/openapi.json +2162 -0
- package/package.json +131 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ApiError, ApiModule } from '../apicore-server.js';
|
|
2
|
+
import { MemoryTusUploadStore, TusUploadExceedsLengthError, TusUploadOffsetError } from './memory.js';
|
|
3
|
+
const TUS_VERSION = '1.0.0';
|
|
4
|
+
function parseNonNegativeInt(raw, headerName) {
|
|
5
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
6
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
8
|
+
throw new ApiError({ code: 400, message: `Missing or invalid ${headerName} header` });
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
function parseUploadLength(raw) {
|
|
13
|
+
return parseNonNegativeInt(raw, 'Upload-Length');
|
|
14
|
+
}
|
|
15
|
+
function parseOffset(raw) {
|
|
16
|
+
return parseNonNegativeInt(raw, 'Upload-Offset');
|
|
17
|
+
}
|
|
18
|
+
function decodeMetadata(raw) {
|
|
19
|
+
const source = Array.isArray(raw) ? raw[0] : raw;
|
|
20
|
+
const input = String(source ?? '').trim();
|
|
21
|
+
if (!input) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const pair of input.split(',')) {
|
|
26
|
+
const trimmed = pair.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const [key, encoded = ''] = trimmed.split(' ');
|
|
31
|
+
if (!key) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
out[key] = Buffer.from(encoded, 'base64').toString('utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
out[key] = '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
async function readChunk(request, maxBytes) {
|
|
44
|
+
if (Buffer.isBuffer(request.body)) {
|
|
45
|
+
return request.body;
|
|
46
|
+
}
|
|
47
|
+
if (typeof request.body === 'string') {
|
|
48
|
+
return Buffer.from(request.body);
|
|
49
|
+
}
|
|
50
|
+
// Fastify only sets request.body when the Content-Type parser fires.
|
|
51
|
+
// Fall back to reading the raw stream, but enforce the same size cap to
|
|
52
|
+
// prevent bodyLimit from being bypassed by omitting the Content-Type header.
|
|
53
|
+
const parts = [];
|
|
54
|
+
let total = 0;
|
|
55
|
+
for await (const chunk of request.raw) {
|
|
56
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
57
|
+
total += buf.length;
|
|
58
|
+
if (total > maxBytes) {
|
|
59
|
+
throw new ApiError({ code: 413, message: 'Upload chunk exceeds maximum chunk size' });
|
|
60
|
+
}
|
|
61
|
+
parts.push(buf);
|
|
62
|
+
}
|
|
63
|
+
return Buffer.concat(parts);
|
|
64
|
+
}
|
|
65
|
+
function setTusHeaders(reply, supportsTermination = false) {
|
|
66
|
+
reply.header('Tus-Resumable', TUS_VERSION);
|
|
67
|
+
reply.header('Tus-Version', TUS_VERSION);
|
|
68
|
+
reply.header('Tus-Extension', supportsTermination ? 'creation,termination' : 'creation');
|
|
69
|
+
}
|
|
70
|
+
function toLocation(basePath, uploadId) {
|
|
71
|
+
const normalized = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
72
|
+
return `${normalized}/${uploadId}`;
|
|
73
|
+
}
|
|
74
|
+
export class TusUploadModule extends ApiModule {
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
super({ namespace: '' });
|
|
77
|
+
const rawPath = options.basePath?.trim() || '/api/v1/upload';
|
|
78
|
+
this.basePath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
|
79
|
+
this.chunkMaxBytes = options.chunkMaxBytes ?? 64 * 1024 * 1024;
|
|
80
|
+
this.uploadMaxBytes = options.uploadMaxBytes ?? 10 * 1024 * 1024 * 1024; // 10 GiB
|
|
81
|
+
this.store = options.store ?? new MemoryTusUploadStore();
|
|
82
|
+
this.auth = options.auth;
|
|
83
|
+
this.onUploadComplete = options.onUploadComplete;
|
|
84
|
+
}
|
|
85
|
+
onMount() {
|
|
86
|
+
this.installTusRoutes();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Authenticate the request if an auth config was provided.
|
|
90
|
+
* Returns false and sends a 401/403 response if auth fails, so the caller
|
|
91
|
+
* can bail out early with `if (!await this.checkAuth(request, reply)) return`.
|
|
92
|
+
*/
|
|
93
|
+
async checkAuth(request, reply) {
|
|
94
|
+
if (!this.auth) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const apiReq = await this.server.resolveRequest(request, reply, this.auth);
|
|
99
|
+
return apiReq.tokenData?.uid != null ? String(apiReq.tokenData.uid) : null;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const code = error instanceof ApiError ? error.code : 401;
|
|
103
|
+
const message = error instanceof ApiError ? error.message : 'Unauthorized';
|
|
104
|
+
reply.code(code).send({ success: false, code, message, data: null, errors: {} });
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
authFailed(result) {
|
|
109
|
+
// When auth is configured and checkAuth catches an error, it returns
|
|
110
|
+
// `false as unknown as string` after sending a response. We distinguish
|
|
111
|
+
// that from a legitimate `null` (no auth configured / no uid in token)
|
|
112
|
+
// by checking the exact `false` sentinel.
|
|
113
|
+
return result === false;
|
|
114
|
+
}
|
|
115
|
+
verifyOwnership(upload, authUserId, reply) {
|
|
116
|
+
if (upload.userId && authUserId && upload.userId !== authUserId) {
|
|
117
|
+
reply
|
|
118
|
+
.code(403)
|
|
119
|
+
.send({ success: false, code: 403, message: 'Upload belongs to another user', data: null, errors: {} });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
installTusRoutes() {
|
|
125
|
+
const app = this.server.fastify;
|
|
126
|
+
const hasTermination = typeof this.store.deleteUpload === 'function';
|
|
127
|
+
try {
|
|
128
|
+
app.addContentTypeParser('application/offset+octet-stream', { parseAs: 'buffer' }, (_req, body, done) => {
|
|
129
|
+
done(null, body);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Parser may already be present when mounting multiple upload modules.
|
|
134
|
+
const message = error instanceof Error ? error.message : '';
|
|
135
|
+
if (!message.includes('already registered') && !message.includes('already exists')) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
app.options(this.basePath, async (_request, reply) => {
|
|
140
|
+
setTusHeaders(reply, hasTermination);
|
|
141
|
+
reply.code(204).send();
|
|
142
|
+
});
|
|
143
|
+
app.post(this.basePath, async (request, reply) => {
|
|
144
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
145
|
+
if (this.authFailed(authUserId))
|
|
146
|
+
return;
|
|
147
|
+
setTusHeaders(reply, hasTermination);
|
|
148
|
+
const length = parseUploadLength(request.headers['upload-length']);
|
|
149
|
+
if (length <= 0) {
|
|
150
|
+
reply.code(400).send({
|
|
151
|
+
success: false,
|
|
152
|
+
code: 400,
|
|
153
|
+
message: 'Upload-Length must be greater than zero',
|
|
154
|
+
data: null,
|
|
155
|
+
errors: {}
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (length > this.uploadMaxBytes) {
|
|
160
|
+
reply.code(413).send({
|
|
161
|
+
success: false,
|
|
162
|
+
code: 413,
|
|
163
|
+
message: `Upload-Length exceeds the maximum allowed size of ${this.uploadMaxBytes} bytes`,
|
|
164
|
+
data: null,
|
|
165
|
+
errors: {}
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const metadata = decodeMetadata(request.headers['upload-metadata']);
|
|
170
|
+
const created = await this.store.createUpload({ length, metadata, userId: authUserId ?? undefined });
|
|
171
|
+
reply.header('Location', toLocation(this.basePath, created.id));
|
|
172
|
+
reply.header('Upload-Offset', String(created.offset));
|
|
173
|
+
reply.code(201).send();
|
|
174
|
+
});
|
|
175
|
+
app.head(`${this.basePath}/:uploadId`, async (request, reply) => {
|
|
176
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
177
|
+
if (this.authFailed(authUserId))
|
|
178
|
+
return;
|
|
179
|
+
setTusHeaders(reply, hasTermination);
|
|
180
|
+
const uploadId = String(request.params.uploadId ?? '');
|
|
181
|
+
const upload = await this.store.getUpload(uploadId);
|
|
182
|
+
if (!upload) {
|
|
183
|
+
reply.code(404).send();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!this.verifyOwnership(upload, authUserId, reply))
|
|
187
|
+
return;
|
|
188
|
+
reply.header('Upload-Offset', String(upload.offset));
|
|
189
|
+
reply.header('Upload-Length', String(upload.length));
|
|
190
|
+
reply.code(200).send();
|
|
191
|
+
});
|
|
192
|
+
app.patch(`${this.basePath}/:uploadId`, { bodyLimit: this.chunkMaxBytes }, async (request, reply) => {
|
|
193
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
194
|
+
if (this.authFailed(authUserId))
|
|
195
|
+
return;
|
|
196
|
+
setTusHeaders(reply, hasTermination);
|
|
197
|
+
const uploadId = String(request.params.uploadId ?? '');
|
|
198
|
+
const upload = await this.store.getUpload(uploadId);
|
|
199
|
+
if (!upload) {
|
|
200
|
+
reply.code(404).send();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!this.verifyOwnership(upload, authUserId, reply))
|
|
204
|
+
return;
|
|
205
|
+
const offset = parseOffset(request.headers['upload-offset']);
|
|
206
|
+
if (offset !== upload.offset) {
|
|
207
|
+
reply.header('Upload-Offset', String(upload.offset));
|
|
208
|
+
reply.code(409).send();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const chunk = await readChunk(request, this.chunkMaxBytes);
|
|
212
|
+
try {
|
|
213
|
+
const updated = await this.store.appendUpload({ uploadId, offset, chunk });
|
|
214
|
+
reply.header('Upload-Offset', String(updated.offset));
|
|
215
|
+
if (updated.completedAt && this.onUploadComplete) {
|
|
216
|
+
try {
|
|
217
|
+
await this.onUploadComplete(updated);
|
|
218
|
+
}
|
|
219
|
+
catch (completionError) {
|
|
220
|
+
console.error('[TusUploadModule] onUploadComplete callback failed', completionError);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
reply.code(204).send();
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof TusUploadOffsetError) {
|
|
227
|
+
reply.header('Upload-Offset', String(error.currentOffset));
|
|
228
|
+
reply.code(409).send();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (error instanceof TusUploadExceedsLengthError) {
|
|
232
|
+
reply.code(413).send({
|
|
233
|
+
success: false,
|
|
234
|
+
code: 413,
|
|
235
|
+
message: 'Upload chunk exceeds declared upload length',
|
|
236
|
+
data: null,
|
|
237
|
+
errors: {}
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
app.delete(`${this.basePath}/:uploadId`, async (request, reply) => {
|
|
245
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
246
|
+
if (this.authFailed(authUserId))
|
|
247
|
+
return;
|
|
248
|
+
setTusHeaders(reply, hasTermination);
|
|
249
|
+
const uploadId = String(request.params.uploadId ?? '');
|
|
250
|
+
if (!this.store.deleteUpload) {
|
|
251
|
+
reply.header('Allow', 'OPTIONS, POST, HEAD, PATCH');
|
|
252
|
+
reply.code(405).send();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const upload = await this.store.getUpload(uploadId);
|
|
256
|
+
if (!upload) {
|
|
257
|
+
reply.code(404).send();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!this.verifyOwnership(upload, authUserId, reply))
|
|
261
|
+
return;
|
|
262
|
+
const deleted = await this.store.deleteUpload(uploadId);
|
|
263
|
+
reply.code(deleted ? 204 : 404).send();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type TusMetadata = Record<string, string>;
|
|
2
|
+
export interface TusUploadRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
length: number;
|
|
5
|
+
offset: number;
|
|
6
|
+
metadata: TusMetadata;
|
|
7
|
+
userId?: string;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
updatedAt: Date;
|
|
10
|
+
completedAt?: Date;
|
|
11
|
+
}
|
|
12
|
+
export interface TusCreateUploadInput {
|
|
13
|
+
id?: string;
|
|
14
|
+
length: number;
|
|
15
|
+
metadata: TusMetadata;
|
|
16
|
+
userId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TusAppendInput {
|
|
19
|
+
uploadId: string;
|
|
20
|
+
offset: number;
|
|
21
|
+
chunk: Buffer;
|
|
22
|
+
}
|
|
23
|
+
export interface TusUploadStore {
|
|
24
|
+
createUpload(input: TusCreateUploadInput): Promise<TusUploadRecord>;
|
|
25
|
+
getUpload(uploadId: string): Promise<TusUploadRecord | null>;
|
|
26
|
+
appendUpload(input: TusAppendInput): Promise<TusUploadRecord>;
|
|
27
|
+
deleteUpload?(uploadId: string): Promise<boolean>;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
|
|
2
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
/** Base contract for user persistence backends. */
|
|
4
|
+
export declare abstract class UserStore<User, PublicUser> {
|
|
5
|
+
protected readonly toPublicUser: PublicUserMapper<User, PublicUser>;
|
|
6
|
+
private readonly bcryptRounds;
|
|
7
|
+
private readonly bcryptPepper?;
|
|
8
|
+
constructor(opts?: {
|
|
9
|
+
toPublic?: PublicUserMapper<User, PublicUser>;
|
|
10
|
+
bcryptRounds?: number;
|
|
11
|
+
bcryptPepper?: string;
|
|
12
|
+
});
|
|
13
|
+
private applyPepper;
|
|
14
|
+
protected hashPassword(plain: string): Promise<string>;
|
|
15
|
+
verifyPassword(plain: string, hashed: string): Promise<boolean>;
|
|
16
|
+
protected normalizeUserInput(input: Partial<CreateUserInput>): CreateUserInput;
|
|
17
|
+
/** Find a user by id/login/email identifier. */
|
|
18
|
+
abstract findUser(identifier: AuthIdentifier | string): Promise<User | null>;
|
|
19
|
+
/** Find a user by primary id only. */
|
|
20
|
+
abstract findById(id: AuthIdentifier): Promise<User | null>;
|
|
21
|
+
/** Find a user by login or email value. */
|
|
22
|
+
abstract findByLoginOrEmail(loginOrEmail: string): Promise<User | null>;
|
|
23
|
+
/** Create a new user record. */
|
|
24
|
+
abstract createUser(input: CreateUserInput): Promise<User>;
|
|
25
|
+
abstract upsertUser(input: CreateUserInput): Promise<User>;
|
|
26
|
+
/** Update selected user fields. */
|
|
27
|
+
abstract updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<User>;
|
|
28
|
+
/** Persist a password hash for the given user id. */
|
|
29
|
+
abstract setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
30
|
+
/** Extract the password hash from a user record. */
|
|
31
|
+
abstract getPasswordHash(user: User): string | null;
|
|
32
|
+
/** Extract the stable user identifier from a user record. */
|
|
33
|
+
abstract getUserId(user: User): AuthIdentifier;
|
|
34
|
+
toPublic(user: User): PublicUser;
|
|
35
|
+
}
|
|
36
|
+
export type { CreateUserInput, UpdateUserInput, PublicUserMapper } from './types.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
/** Base contract for user persistence backends. */
|
|
4
|
+
export class UserStore {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
this.toPublicUser = opts.toPublic ?? ((u) => u);
|
|
7
|
+
const rounds = typeof opts.bcryptRounds === 'number' && Number.isFinite(opts.bcryptRounds)
|
|
8
|
+
? Math.max(4, Math.floor(opts.bcryptRounds))
|
|
9
|
+
: 12;
|
|
10
|
+
this.bcryptRounds = rounds;
|
|
11
|
+
this.bcryptPepper =
|
|
12
|
+
typeof opts.bcryptPepper === 'string' && opts.bcryptPepper.length > 0 ? opts.bcryptPepper : undefined;
|
|
13
|
+
}
|
|
14
|
+
applyPepper(plain) {
|
|
15
|
+
if (!this.bcryptPepper) {
|
|
16
|
+
return plain;
|
|
17
|
+
}
|
|
18
|
+
return createHmac('sha256', this.bcryptPepper).update(plain).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
async hashPassword(plain) {
|
|
21
|
+
const candidate = this.applyPepper(plain);
|
|
22
|
+
return bcrypt.hash(candidate, this.bcryptRounds);
|
|
23
|
+
}
|
|
24
|
+
async verifyPassword(plain, hashed) {
|
|
25
|
+
const candidate = this.applyPepper(plain);
|
|
26
|
+
return bcrypt.compare(candidate, hashed);
|
|
27
|
+
}
|
|
28
|
+
normalizeUserInput(input) {
|
|
29
|
+
const login = typeof input.login === 'string' ? input.login.trim() : '';
|
|
30
|
+
const email = typeof input.email === 'string' ? input.email.trim() : '';
|
|
31
|
+
if (!login || !email) {
|
|
32
|
+
throw new Error('login and email are required');
|
|
33
|
+
}
|
|
34
|
+
const password = typeof input.password === 'string' ? input.password : undefined;
|
|
35
|
+
return { ...input, login, email, password };
|
|
36
|
+
}
|
|
37
|
+
toPublic(user) {
|
|
38
|
+
const mapped = this.toPublicUser(user);
|
|
39
|
+
if (mapped && typeof mapped === 'object') {
|
|
40
|
+
const rest = { ...mapped };
|
|
41
|
+
delete rest.password;
|
|
42
|
+
return rest;
|
|
43
|
+
}
|
|
44
|
+
return mapped;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
maxUsers?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class MemoryUserStore<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
|
|
20
|
+
private readonly usersById;
|
|
21
|
+
private readonly loginToId;
|
|
22
|
+
private readonly emailToId;
|
|
23
|
+
private readonly userIdFactory;
|
|
24
|
+
private readonly maxUsers?;
|
|
25
|
+
private nextUserId;
|
|
26
|
+
constructor(options?: MemoryUserStoreOptions<UserAttributes, PublicUserShape>);
|
|
27
|
+
findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
|
|
28
|
+
findById(id: AuthIdentifier): Promise<UserAttributes | null>;
|
|
29
|
+
findByLoginOrEmail(loginOrEmail: string): Promise<UserAttributes | null>;
|
|
30
|
+
createUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
31
|
+
upsertUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
32
|
+
updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<UserAttributes>;
|
|
33
|
+
setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
34
|
+
getPasswordHash(user: UserAttributes): string | null;
|
|
35
|
+
getUserId(user: UserAttributes): AuthIdentifier;
|
|
36
|
+
private persistUser;
|
|
37
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { normalizeNumericUserId } from '../auth-api/user-id.js';
|
|
2
|
+
import { UserStore } from './base.js';
|
|
3
|
+
function cloneUser(user) {
|
|
4
|
+
return { ...user };
|
|
5
|
+
}
|
|
6
|
+
export class MemoryUserStore extends UserStore {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
super({
|
|
9
|
+
toPublic: options.toPublic,
|
|
10
|
+
bcryptRounds: options.bcryptRounds,
|
|
11
|
+
bcryptPepper: options.bcryptPepper
|
|
12
|
+
});
|
|
13
|
+
this.usersById = new Map();
|
|
14
|
+
this.loginToId = new Map();
|
|
15
|
+
this.emailToId = new Map();
|
|
16
|
+
this.nextUserId = Number.isFinite(options.startingUserId) ? Number(options.startingUserId) : 1;
|
|
17
|
+
this.maxUsers =
|
|
18
|
+
typeof options.maxUsers === 'number' && Number.isFinite(options.maxUsers) && options.maxUsers > 0
|
|
19
|
+
? Math.floor(options.maxUsers)
|
|
20
|
+
: undefined;
|
|
21
|
+
this.userIdFactory =
|
|
22
|
+
options.userIdFactory ??
|
|
23
|
+
(() => {
|
|
24
|
+
const id = this.nextUserId;
|
|
25
|
+
this.nextUserId += 1;
|
|
26
|
+
return id;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async findUser(identifier) {
|
|
30
|
+
if (typeof identifier === 'number') {
|
|
31
|
+
const user = this.usersById.get(identifier);
|
|
32
|
+
return user ? cloneUser(user) : null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof identifier === 'string') {
|
|
35
|
+
const numeric = /^\d+$/.test(identifier) ? Number(identifier) : null;
|
|
36
|
+
if (numeric !== null) {
|
|
37
|
+
const user = this.usersById.get(numeric);
|
|
38
|
+
if (user) {
|
|
39
|
+
return cloneUser(user);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const loginId = this.loginToId.get(identifier);
|
|
43
|
+
if (loginId !== undefined) {
|
|
44
|
+
const loginUser = this.usersById.get(loginId);
|
|
45
|
+
if (loginUser)
|
|
46
|
+
return cloneUser(loginUser);
|
|
47
|
+
}
|
|
48
|
+
const emailId = this.emailToId.get(identifier);
|
|
49
|
+
if (emailId !== undefined) {
|
|
50
|
+
const emailUser = this.usersById.get(emailId);
|
|
51
|
+
if (emailUser)
|
|
52
|
+
return cloneUser(emailUser);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async findById(id) {
|
|
58
|
+
try {
|
|
59
|
+
const numeric = normalizeNumericUserId(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
|
+
const loginId = this.loginToId.get(loginOrEmail);
|
|
69
|
+
if (loginId !== undefined) {
|
|
70
|
+
const loginUser = this.usersById.get(loginId);
|
|
71
|
+
if (loginUser)
|
|
72
|
+
return cloneUser(loginUser);
|
|
73
|
+
}
|
|
74
|
+
const emailId = this.emailToId.get(loginOrEmail);
|
|
75
|
+
if (emailId !== undefined) {
|
|
76
|
+
const emailUser = this.usersById.get(emailId);
|
|
77
|
+
if (emailUser)
|
|
78
|
+
return cloneUser(emailUser);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
async createUser(input) {
|
|
83
|
+
const normalizedInput = this.normalizeUserInput(input);
|
|
84
|
+
const providedId = input.user_id;
|
|
85
|
+
const userId = typeof providedId === 'number' && Number.isFinite(providedId) ? providedId : this.userIdFactory();
|
|
86
|
+
if (this.usersById.has(userId)) {
|
|
87
|
+
throw new Error(`User ${userId} already exists`);
|
|
88
|
+
}
|
|
89
|
+
if (this.maxUsers !== undefined && this.usersById.size >= this.maxUsers) {
|
|
90
|
+
throw new Error('MemoryUserStore maxUsers limit reached');
|
|
91
|
+
}
|
|
92
|
+
if (this.loginToId.has(normalizedInput.login)) {
|
|
93
|
+
throw new Error(`User with login ${normalizedInput.login} already exists`);
|
|
94
|
+
}
|
|
95
|
+
if (this.emailToId.has(normalizedInput.email)) {
|
|
96
|
+
throw new Error(`User with email ${normalizedInput.email} already exists`);
|
|
97
|
+
}
|
|
98
|
+
const passwordHash = normalizedInput.password ? await this.hashPassword(normalizedInput.password) : '';
|
|
99
|
+
const record = {
|
|
100
|
+
...normalizedInput,
|
|
101
|
+
user_id: userId,
|
|
102
|
+
password: passwordHash
|
|
103
|
+
};
|
|
104
|
+
this.persistUser(record);
|
|
105
|
+
if (typeof providedId === 'number' && Number.isFinite(providedId)) {
|
|
106
|
+
this.nextUserId = Math.max(this.nextUserId, providedId + 1);
|
|
107
|
+
}
|
|
108
|
+
return cloneUser(record);
|
|
109
|
+
}
|
|
110
|
+
async upsertUser(input) {
|
|
111
|
+
const normalizedInput = this.normalizeUserInput(input);
|
|
112
|
+
const providedId = input.user_id;
|
|
113
|
+
if (providedId !== undefined) {
|
|
114
|
+
const existing = this.usersById.get(providedId);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
throw new Error(`User ${providedId} not found`);
|
|
117
|
+
}
|
|
118
|
+
const updates = {
|
|
119
|
+
...existing,
|
|
120
|
+
...normalizedInput
|
|
121
|
+
};
|
|
122
|
+
if (updates.login !== existing.login) {
|
|
123
|
+
const loginOwner = this.loginToId.get(updates.login);
|
|
124
|
+
if (loginOwner !== undefined && loginOwner !== existing.user_id) {
|
|
125
|
+
throw new Error(`User with login ${updates.login} already exists`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (updates.email !== existing.email) {
|
|
129
|
+
const emailOwner = this.emailToId.get(updates.email);
|
|
130
|
+
if (emailOwner !== undefined && emailOwner !== existing.user_id) {
|
|
131
|
+
throw new Error(`User with email ${updates.email} already exists`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (normalizedInput.password && normalizedInput.password.length > 0) {
|
|
135
|
+
updates.password = await this.hashPassword(normalizedInput.password);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
updates.password = existing.password;
|
|
139
|
+
}
|
|
140
|
+
this.persistUser(updates);
|
|
141
|
+
return cloneUser(updates);
|
|
142
|
+
}
|
|
143
|
+
return this.createUser(input);
|
|
144
|
+
}
|
|
145
|
+
async updateUser(id, patch) {
|
|
146
|
+
const user = await this.findById(id);
|
|
147
|
+
if (!user) {
|
|
148
|
+
throw new Error(`User ${String(id)} not found`);
|
|
149
|
+
}
|
|
150
|
+
const updates = { ...user, ...patch };
|
|
151
|
+
if (patch.password && patch.password.length > 0) {
|
|
152
|
+
updates.password = await this.hashPassword(patch.password);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
updates.password = user.password;
|
|
156
|
+
}
|
|
157
|
+
this.persistUser(updates);
|
|
158
|
+
return cloneUser(updates);
|
|
159
|
+
}
|
|
160
|
+
async setPasswordHash(id, hash) {
|
|
161
|
+
const user = await this.findById(id);
|
|
162
|
+
if (!user) {
|
|
163
|
+
throw new Error(`User ${String(id)} not found`);
|
|
164
|
+
}
|
|
165
|
+
const updates = { ...user, password: hash };
|
|
166
|
+
this.persistUser(updates);
|
|
167
|
+
}
|
|
168
|
+
getPasswordHash(user) {
|
|
169
|
+
return user.password;
|
|
170
|
+
}
|
|
171
|
+
getUserId(user) {
|
|
172
|
+
return user.user_id;
|
|
173
|
+
}
|
|
174
|
+
persistUser(user) {
|
|
175
|
+
const snapshot = { ...user };
|
|
176
|
+
this.usersById.set(user.user_id, snapshot);
|
|
177
|
+
for (const [login, id] of [...this.loginToId.entries()]) {
|
|
178
|
+
if (id === user.user_id && login !== user.login) {
|
|
179
|
+
this.loginToId.delete(login);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const [email, id] of [...this.emailToId.entries()]) {
|
|
183
|
+
if (id === user.user_id && email !== user.email) {
|
|
184
|
+
this.emailToId.delete(email);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.loginToId.set(user.login, user.user_id);
|
|
188
|
+
this.emailToId.set(user.email, user.user_id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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, options?: {
|
|
14
|
+
tablePrefix?: string;
|
|
15
|
+
}): typeof AuthUserModel;
|
|
16
|
+
export type GenericUserModel = Model<Record<string, unknown>, Record<string, unknown>>;
|
|
17
|
+
export type GenericUserModelStatic = ModelStatic<GenericUserModel>;
|
|
18
|
+
export interface SequelizeUserStoreOptions<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> {
|
|
19
|
+
bcryptRounds?: number;
|
|
20
|
+
bcryptPepper?: string;
|
|
21
|
+
sequelize: Sequelize;
|
|
22
|
+
tablePrefix?: string;
|
|
23
|
+
userModel?: GenericUserModelStatic;
|
|
24
|
+
userModelFactory?: (sequelize: Sequelize, options?: {
|
|
25
|
+
tablePrefix?: string;
|
|
26
|
+
}) => GenericUserModelStatic;
|
|
27
|
+
recordMapper?: (model: GenericUserModel) => UserAttributes;
|
|
28
|
+
toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
|
|
29
|
+
}
|
|
30
|
+
export declare class SequelizeUserStore<UserAttributes extends AuthUserAttributes = AuthUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
|
|
31
|
+
readonly Users: GenericUserModelStatic;
|
|
32
|
+
private readonly recordMapper;
|
|
33
|
+
constructor(options: SequelizeUserStoreOptions<UserAttributes, PublicUserShape>);
|
|
34
|
+
findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
|
|
35
|
+
findById(id: AuthIdentifier): Promise<UserAttributes | null>;
|
|
36
|
+
findByLoginOrEmail(loginOrEmail: string): Promise<UserAttributes | null>;
|
|
37
|
+
createUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
38
|
+
upsertUser(input: CreateUserInput): Promise<UserAttributes>;
|
|
39
|
+
updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<UserAttributes>;
|
|
40
|
+
setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
41
|
+
getPasswordHash(user: UserAttributes): string | null;
|
|
42
|
+
getUserId(user: UserAttributes): AuthIdentifier;
|
|
43
|
+
protected toUserRecord(model: GenericUserModel): UserAttributes;
|
|
44
|
+
private static mapModelToUser;
|
|
45
|
+
private normalizeUserId;
|
|
46
|
+
}
|