@ministerjs/auth 2.0.2 → 3.0.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/README.md +53 -8
- package/dist/nestjs.d.mts +163 -8
- package/dist/nestjs.d.ts +163 -8
- package/dist/nestjs.js +429 -35
- package/dist/nestjs.mjs +410 -16
- package/dist/vue.d.mts +2 -0
- package/dist/vue.d.ts +2 -0
- package/dist/vue.js +20 -0
- package/dist/vue.mjs +20 -0
- package/package.json +6 -1
- package/types/nestjs/AuthController.d.ts +1 -0
- package/types/nestjs/AuthModule.d.ts +2 -0
- package/types/nestjs/JwtPrismaCookieAuthDriver.d.ts +64 -6
- package/types/nestjs/guards/JwtAuthGuard.d.ts +17 -0
- package/types/nestjs/guards/RolesGuard.d.ts +8 -0
- package/types/nestjs/guards/index.d.ts +2 -0
- package/types/nestjs/index.d.ts +3 -0
- package/types/nestjs/rbac.d.ts +3 -0
- package/types/nestjs/stores.d.ts +36 -0
- package/types/nestjs/types.d.ts +37 -2
- package/types/vue/VueAuth.d.ts +2 -0
package/dist/nestjs.mjs
CHANGED
|
@@ -13,6 +13,165 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
|
|
|
13
13
|
// src/nestjs/token.ts
|
|
14
14
|
var AUTH_DRIVER = Symbol("AUTH_DRIVER");
|
|
15
15
|
|
|
16
|
+
// src/nestjs/stores.ts
|
|
17
|
+
var InMemoryLockoutStore = class {
|
|
18
|
+
constructor(windowMs) {
|
|
19
|
+
this.windowMs = windowMs;
|
|
20
|
+
}
|
|
21
|
+
entries = /* @__PURE__ */ new Map();
|
|
22
|
+
async recordFailure(key) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const entry = this.entries.get(key);
|
|
25
|
+
if (!entry || now - entry.firstFailure > this.windowMs) {
|
|
26
|
+
this.entries.set(key, { count: 1, firstFailure: now });
|
|
27
|
+
} else {
|
|
28
|
+
entry.count++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async getFailures(key) {
|
|
32
|
+
const entry = this.entries.get(key);
|
|
33
|
+
if (!entry) return 0;
|
|
34
|
+
if (Date.now() - entry.firstFailure > this.windowMs) {
|
|
35
|
+
this.entries.delete(key);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
return entry.count;
|
|
39
|
+
}
|
|
40
|
+
async reset(key) {
|
|
41
|
+
this.entries.delete(key);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var InMemoryRefreshTokenStore = class {
|
|
45
|
+
tokens = /* @__PURE__ */ new Map();
|
|
46
|
+
userJtis = /* @__PURE__ */ new Map();
|
|
47
|
+
async save(jti, userId, expiresAt) {
|
|
48
|
+
this.tokens.set(jti, { userId, expiresAt });
|
|
49
|
+
let jtis = this.userJtis.get(userId);
|
|
50
|
+
if (!jtis) {
|
|
51
|
+
jtis = /* @__PURE__ */ new Set();
|
|
52
|
+
this.userJtis.set(userId, jtis);
|
|
53
|
+
}
|
|
54
|
+
jtis.add(jti);
|
|
55
|
+
}
|
|
56
|
+
async find(jti) {
|
|
57
|
+
const entry = this.tokens.get(jti);
|
|
58
|
+
if (!entry) return null;
|
|
59
|
+
if (entry.expiresAt.getTime() < Date.now()) {
|
|
60
|
+
this.tokens.delete(jti);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
async revoke(jti) {
|
|
66
|
+
const entry = this.tokens.get(jti);
|
|
67
|
+
if (entry) {
|
|
68
|
+
this.userJtis.get(entry.userId)?.delete(jti);
|
|
69
|
+
}
|
|
70
|
+
this.tokens.delete(jti);
|
|
71
|
+
}
|
|
72
|
+
async revokeAll(userId) {
|
|
73
|
+
const jtis = this.userJtis.get(userId);
|
|
74
|
+
if (jtis) {
|
|
75
|
+
for (const jti of jtis) {
|
|
76
|
+
this.tokens.delete(jti);
|
|
77
|
+
}
|
|
78
|
+
this.userJtis.delete(userId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/nestjs/rbac.ts
|
|
84
|
+
import { SetMetadata } from "@nestjs/common";
|
|
85
|
+
var ROLES_KEY = "ministerjs:roles";
|
|
86
|
+
var Roles = (...roles) => SetMetadata(ROLES_KEY, roles);
|
|
87
|
+
|
|
88
|
+
// src/nestjs/guards/JwtAuthGuard.ts
|
|
89
|
+
import {
|
|
90
|
+
Injectable,
|
|
91
|
+
UnauthorizedException
|
|
92
|
+
} from "@nestjs/common";
|
|
93
|
+
import { verify } from "jsonwebtoken";
|
|
94
|
+
var JwtAuthGuard = class {
|
|
95
|
+
jwtSecret;
|
|
96
|
+
cookieName;
|
|
97
|
+
csrfEnabled;
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.jwtSecret = options.jwtSecret;
|
|
100
|
+
this.cookieName = options.cookieName ?? "auth_token";
|
|
101
|
+
this.csrfEnabled = options.csrfEnabled ?? false;
|
|
102
|
+
}
|
|
103
|
+
canActivate(context) {
|
|
104
|
+
const request = context.switchToHttp().getRequest();
|
|
105
|
+
const token = this.extractToken(request);
|
|
106
|
+
if (!token) {
|
|
107
|
+
throw new UnauthorizedException("No token provided");
|
|
108
|
+
}
|
|
109
|
+
let payload;
|
|
110
|
+
try {
|
|
111
|
+
payload = verify(token, this.jwtSecret, {
|
|
112
|
+
algorithms: ["HS256"]
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
throw new UnauthorizedException("Invalid token");
|
|
116
|
+
}
|
|
117
|
+
if (this.csrfEnabled) {
|
|
118
|
+
this.validateCsrf(request);
|
|
119
|
+
}
|
|
120
|
+
request.user = {
|
|
121
|
+
id: payload.sub,
|
|
122
|
+
roles: payload.roles ?? []
|
|
123
|
+
};
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
extractToken(request) {
|
|
127
|
+
const fromCookie = request.cookies?.[this.cookieName];
|
|
128
|
+
if (fromCookie) return fromCookie;
|
|
129
|
+
const authHeader = request.headers.authorization;
|
|
130
|
+
if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7);
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
validateCsrf(request) {
|
|
134
|
+
const headerToken = request.headers["x-csrf-token"];
|
|
135
|
+
const cookieToken = request.cookies?.["csrf_token"];
|
|
136
|
+
if (!headerToken || headerToken !== cookieToken) {
|
|
137
|
+
throw new UnauthorizedException("CSRF token mismatch");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
JwtAuthGuard = __decorateClass([
|
|
142
|
+
Injectable()
|
|
143
|
+
], JwtAuthGuard);
|
|
144
|
+
|
|
145
|
+
// src/nestjs/guards/RolesGuard.ts
|
|
146
|
+
import {
|
|
147
|
+
ForbiddenException,
|
|
148
|
+
Injectable as Injectable2
|
|
149
|
+
} from "@nestjs/common";
|
|
150
|
+
var RolesGuard = class {
|
|
151
|
+
constructor(reflector) {
|
|
152
|
+
this.reflector = reflector;
|
|
153
|
+
}
|
|
154
|
+
canActivate(context) {
|
|
155
|
+
const required = this.reflector.getAllAndOverride(ROLES_KEY, [
|
|
156
|
+
context.getHandler(),
|
|
157
|
+
context.getClass()
|
|
158
|
+
]);
|
|
159
|
+
if (!required?.length) return true;
|
|
160
|
+
const request = context.switchToHttp().getRequest();
|
|
161
|
+
if (!request.user) {
|
|
162
|
+
throw new ForbiddenException("Not authenticated");
|
|
163
|
+
}
|
|
164
|
+
const hasRole = required.some((r) => request.user.roles.includes(r));
|
|
165
|
+
if (!hasRole) {
|
|
166
|
+
throw new ForbiddenException("Insufficient permissions");
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
RolesGuard = __decorateClass([
|
|
172
|
+
Injectable2()
|
|
173
|
+
], RolesGuard);
|
|
174
|
+
|
|
16
175
|
// src/nestjs/AuthController.ts
|
|
17
176
|
import {
|
|
18
177
|
Body,
|
|
@@ -22,7 +181,7 @@ import {
|
|
|
22
181
|
Post,
|
|
23
182
|
Req,
|
|
24
183
|
Res,
|
|
25
|
-
UnauthorizedException
|
|
184
|
+
UnauthorizedException as UnauthorizedException2
|
|
26
185
|
} from "@nestjs/common";
|
|
27
186
|
var AuthController = class {
|
|
28
187
|
constructor(driver) {
|
|
@@ -35,7 +194,17 @@ var AuthController = class {
|
|
|
35
194
|
async checkIn(request, response) {
|
|
36
195
|
const user = await this.driver.checkIn(request, response);
|
|
37
196
|
if (!user) {
|
|
38
|
-
throw new
|
|
197
|
+
throw new UnauthorizedException2("Not authenticated");
|
|
198
|
+
}
|
|
199
|
+
return { message: "ok", data: user };
|
|
200
|
+
}
|
|
201
|
+
async refresh(request, response) {
|
|
202
|
+
if (!this.driver.refresh) {
|
|
203
|
+
throw new UnauthorizedException2("Refresh not supported");
|
|
204
|
+
}
|
|
205
|
+
const user = await this.driver.refresh(request, response);
|
|
206
|
+
if (!user) {
|
|
207
|
+
throw new UnauthorizedException2("Invalid refresh token");
|
|
39
208
|
}
|
|
40
209
|
return { message: "ok", data: user };
|
|
41
210
|
}
|
|
@@ -57,6 +226,12 @@ __decorateClass([
|
|
|
57
226
|
__decorateParam(0, Req()),
|
|
58
227
|
__decorateParam(1, Res({ passthrough: true }))
|
|
59
228
|
], AuthController.prototype, "checkIn", 1);
|
|
229
|
+
__decorateClass([
|
|
230
|
+
Post("/refresh"),
|
|
231
|
+
HttpCode(200),
|
|
232
|
+
__decorateParam(0, Req()),
|
|
233
|
+
__decorateParam(1, Res({ passthrough: true }))
|
|
234
|
+
], AuthController.prototype, "refresh", 1);
|
|
60
235
|
__decorateClass([
|
|
61
236
|
Post("/logout"),
|
|
62
237
|
HttpCode(200),
|
|
@@ -94,6 +269,9 @@ var AuthModule = class {
|
|
|
94
269
|
exports: [driverProvider]
|
|
95
270
|
};
|
|
96
271
|
}
|
|
272
|
+
static createGuard(options) {
|
|
273
|
+
return new JwtAuthGuard(options);
|
|
274
|
+
}
|
|
97
275
|
static normalizeDriverProvider(driver) {
|
|
98
276
|
if (typeof driver === "function") {
|
|
99
277
|
return { provide: AUTH_DRIVER, useClass: driver };
|
|
@@ -119,12 +297,26 @@ AuthModule = __decorateClass([
|
|
|
119
297
|
], AuthModule);
|
|
120
298
|
|
|
121
299
|
// src/nestjs/JwtPrismaCookieAuthDriver.ts
|
|
122
|
-
import {
|
|
123
|
-
import {
|
|
300
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
301
|
+
import { UnauthorizedException as UnauthorizedException3 } from "@nestjs/common";
|
|
302
|
+
import { sign, verify as verify2 } from "jsonwebtoken";
|
|
303
|
+
var DURATION_UNITS = {
|
|
304
|
+
s: 1e3,
|
|
305
|
+
m: 6e4,
|
|
306
|
+
h: 36e5,
|
|
307
|
+
d: 864e5
|
|
308
|
+
};
|
|
309
|
+
function parseDuration(value) {
|
|
310
|
+
const match = value.match(/^(\d+)\s*([smhd])$/);
|
|
311
|
+
if (!match) return 7 * 864e5;
|
|
312
|
+
return parseInt(match[1], 10) * DURATION_UNITS[match[2]];
|
|
313
|
+
}
|
|
314
|
+
var isProduction = process.env.NODE_ENV === "production";
|
|
124
315
|
var defaultCookieOptions = {
|
|
125
316
|
httpOnly: true,
|
|
126
317
|
sameSite: "lax",
|
|
127
|
-
path: "/"
|
|
318
|
+
path: "/",
|
|
319
|
+
secure: isProduction
|
|
128
320
|
};
|
|
129
321
|
var JwtPrismaCookieAuthDriver = class {
|
|
130
322
|
prismaModel;
|
|
@@ -137,13 +329,23 @@ var JwtPrismaCookieAuthDriver = class {
|
|
|
137
329
|
cookieOptions;
|
|
138
330
|
comparePassword;
|
|
139
331
|
transformUser;
|
|
332
|
+
csrf;
|
|
333
|
+
onAudit;
|
|
334
|
+
onRateLimitCheck;
|
|
335
|
+
lockoutStore;
|
|
336
|
+
lockoutMaxAttempts;
|
|
337
|
+
refreshTokenEnabled;
|
|
338
|
+
refreshTokenCookieName;
|
|
339
|
+
refreshTokenExpiresIn;
|
|
340
|
+
refreshTokenStore;
|
|
140
341
|
constructor(options) {
|
|
141
|
-
|
|
142
|
-
if (!
|
|
342
|
+
const model = options.prisma[options.modelName];
|
|
343
|
+
if (!model) {
|
|
143
344
|
throw new Error(
|
|
144
345
|
`Model "${options.modelName}" n\xE3o encontrado no PrismaClient.`
|
|
145
346
|
);
|
|
146
347
|
}
|
|
348
|
+
this.prismaModel = model;
|
|
147
349
|
this.identifierField = options.identifierField ?? "email";
|
|
148
350
|
this.passwordField = options.passwordField ?? "password";
|
|
149
351
|
this.idField = options.idField ?? "id";
|
|
@@ -154,34 +356,107 @@ var JwtPrismaCookieAuthDriver = class {
|
|
|
154
356
|
...defaultCookieOptions,
|
|
155
357
|
...options.cookieOptions ?? {}
|
|
156
358
|
};
|
|
157
|
-
this.comparePassword = options.comparePassword
|
|
359
|
+
this.comparePassword = options.comparePassword;
|
|
158
360
|
this.transformUser = options.transformUser ?? ((user) => {
|
|
159
361
|
const clone = { ...user };
|
|
160
362
|
delete clone[this.passwordField];
|
|
161
363
|
return clone;
|
|
162
364
|
});
|
|
365
|
+
this.csrf = options.csrf ?? false;
|
|
366
|
+
this.onAudit = options.onAudit;
|
|
367
|
+
this.onRateLimitCheck = options.onRateLimitCheck;
|
|
368
|
+
if (options.lockout) {
|
|
369
|
+
const windowMs = options.lockout.windowMs ?? 15 * 60 * 1e3;
|
|
370
|
+
this.lockoutMaxAttempts = options.lockout.maxAttempts ?? 5;
|
|
371
|
+
this.lockoutStore = options.lockout.store ?? new InMemoryLockoutStore(windowMs);
|
|
372
|
+
} else {
|
|
373
|
+
this.lockoutMaxAttempts = 0;
|
|
374
|
+
}
|
|
375
|
+
if (options.refreshToken?.enabled) {
|
|
376
|
+
this.refreshTokenEnabled = true;
|
|
377
|
+
this.refreshTokenCookieName = options.refreshToken.cookieName ?? "refresh_token";
|
|
378
|
+
this.refreshTokenExpiresIn = options.refreshToken.expiresIn ?? "30d";
|
|
379
|
+
this.refreshTokenStore = options.refreshToken.store ?? new InMemoryRefreshTokenStore();
|
|
380
|
+
} else {
|
|
381
|
+
this.refreshTokenEnabled = false;
|
|
382
|
+
this.refreshTokenCookieName = "refresh_token";
|
|
383
|
+
this.refreshTokenExpiresIn = "30d";
|
|
384
|
+
}
|
|
163
385
|
}
|
|
164
|
-
async login(payload,
|
|
386
|
+
async login(payload, req, res) {
|
|
165
387
|
const identifier = payload[this.identifierField];
|
|
388
|
+
const ip = req.ip ?? req.socket?.remoteAddress ?? "unknown";
|
|
389
|
+
if (this.onRateLimitCheck) {
|
|
390
|
+
const allowed = await this.onRateLimitCheck(ip, identifier);
|
|
391
|
+
if (!allowed) {
|
|
392
|
+
await this.emitAudit({
|
|
393
|
+
action: "login",
|
|
394
|
+
ip,
|
|
395
|
+
success: false,
|
|
396
|
+
metadata: { reason: "rate_limited" }
|
|
397
|
+
});
|
|
398
|
+
throw new UnauthorizedException3("Too many requests");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (this.lockoutStore) {
|
|
402
|
+
const failures = await this.lockoutStore.getFailures(identifier);
|
|
403
|
+
if (failures >= this.lockoutMaxAttempts) {
|
|
404
|
+
await this.emitAudit({
|
|
405
|
+
action: "lockout",
|
|
406
|
+
ip,
|
|
407
|
+
success: false,
|
|
408
|
+
metadata: { identifier }
|
|
409
|
+
});
|
|
410
|
+
throw new UnauthorizedException3("Invalid credentials");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
166
413
|
const user = await this.prismaModel.findUnique({
|
|
167
414
|
where: { [this.identifierField]: identifier }
|
|
168
415
|
});
|
|
169
416
|
if (!user) {
|
|
170
|
-
|
|
417
|
+
await this.lockoutStore?.recordFailure(identifier);
|
|
418
|
+
await this.emitAudit({ action: "login", ip, success: false });
|
|
419
|
+
throw new UnauthorizedException3("Invalid credentials");
|
|
171
420
|
}
|
|
172
421
|
const ok = await this.comparePassword(
|
|
173
422
|
payload[this.passwordField],
|
|
174
423
|
user[this.passwordField]
|
|
175
424
|
);
|
|
176
425
|
if (!ok) {
|
|
177
|
-
|
|
426
|
+
await this.lockoutStore?.recordFailure(identifier);
|
|
427
|
+
await this.emitAudit({
|
|
428
|
+
action: "login",
|
|
429
|
+
userId: String(user[this.idField]),
|
|
430
|
+
ip,
|
|
431
|
+
success: false
|
|
432
|
+
});
|
|
433
|
+
throw new UnauthorizedException3("Invalid credentials");
|
|
178
434
|
}
|
|
435
|
+
await this.lockoutStore?.reset(identifier);
|
|
179
436
|
const token = sign(
|
|
180
437
|
{ sub: user[this.idField] },
|
|
181
438
|
this.jwtSecret,
|
|
182
|
-
{ expiresIn: this.jwtExpiresIn }
|
|
439
|
+
{ expiresIn: this.jwtExpiresIn, algorithm: "HS256" }
|
|
183
440
|
);
|
|
184
441
|
res.cookie(this.cookieName, token, this.cookieOptions);
|
|
442
|
+
if (this.refreshTokenEnabled && this.refreshTokenStore) {
|
|
443
|
+
await this.issueRefreshToken(String(user[this.idField]), res);
|
|
444
|
+
}
|
|
445
|
+
if (this.csrf) {
|
|
446
|
+
const csrfToken = randomBytes(32).toString("hex");
|
|
447
|
+
res.cookie("csrf_token", csrfToken, {
|
|
448
|
+
httpOnly: false,
|
|
449
|
+
sameSite: this.cookieOptions.sameSite,
|
|
450
|
+
path: "/",
|
|
451
|
+
secure: this.cookieOptions.secure
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
await this.emitAudit({
|
|
455
|
+
action: "login",
|
|
456
|
+
userId: String(user[this.idField]),
|
|
457
|
+
ip,
|
|
458
|
+
success: true
|
|
459
|
+
});
|
|
185
460
|
return this.transformUser(user);
|
|
186
461
|
}
|
|
187
462
|
async checkIn(req, res) {
|
|
@@ -191,7 +466,9 @@ var JwtPrismaCookieAuthDriver = class {
|
|
|
191
466
|
}
|
|
192
467
|
let payload;
|
|
193
468
|
try {
|
|
194
|
-
payload =
|
|
469
|
+
payload = verify2(token, this.jwtSecret, {
|
|
470
|
+
algorithms: ["HS256"]
|
|
471
|
+
});
|
|
195
472
|
} catch {
|
|
196
473
|
return null;
|
|
197
474
|
}
|
|
@@ -201,19 +478,136 @@ var JwtPrismaCookieAuthDriver = class {
|
|
|
201
478
|
if (!user) {
|
|
202
479
|
return null;
|
|
203
480
|
}
|
|
204
|
-
|
|
481
|
+
const newToken = sign(
|
|
482
|
+
{ sub: payload.sub },
|
|
483
|
+
this.jwtSecret,
|
|
484
|
+
{ expiresIn: this.jwtExpiresIn, algorithm: "HS256" }
|
|
485
|
+
);
|
|
486
|
+
res.cookie(this.cookieName, newToken, this.cookieOptions);
|
|
487
|
+
await this.emitAudit({
|
|
488
|
+
action: "checkin",
|
|
489
|
+
userId: String(payload.sub),
|
|
490
|
+
ip: req.ip ?? req.socket?.remoteAddress,
|
|
491
|
+
success: true
|
|
492
|
+
});
|
|
205
493
|
return this.transformUser(user);
|
|
206
494
|
}
|
|
207
|
-
async logout(
|
|
495
|
+
async logout(req, res) {
|
|
208
496
|
res.clearCookie(this.cookieName, {
|
|
209
497
|
...this.cookieOptions,
|
|
210
498
|
maxAge: 0
|
|
211
499
|
});
|
|
500
|
+
if (this.refreshTokenEnabled) {
|
|
501
|
+
const refreshCookie = req.cookies?.[this.refreshTokenCookieName];
|
|
502
|
+
if (refreshCookie && this.refreshTokenStore) {
|
|
503
|
+
try {
|
|
504
|
+
const payload = verify2(refreshCookie, this.jwtSecret, {
|
|
505
|
+
algorithms: ["HS256"]
|
|
506
|
+
});
|
|
507
|
+
if (payload.jti) {
|
|
508
|
+
await this.refreshTokenStore.revokeAll(String(payload.sub));
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
res.clearCookie(this.refreshTokenCookieName, {
|
|
514
|
+
...this.cookieOptions,
|
|
515
|
+
maxAge: 0
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (this.csrf) {
|
|
519
|
+
res.clearCookie("csrf_token", { path: "/", maxAge: 0 });
|
|
520
|
+
}
|
|
521
|
+
await this.emitAudit({
|
|
522
|
+
action: "logout",
|
|
523
|
+
ip: req.ip ?? req.socket?.remoteAddress,
|
|
524
|
+
success: true
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async refresh(req, res) {
|
|
528
|
+
if (!this.refreshTokenEnabled || !this.refreshTokenStore) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
const refreshCookie = req.cookies?.[this.refreshTokenCookieName];
|
|
532
|
+
if (!refreshCookie) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
let payload;
|
|
536
|
+
try {
|
|
537
|
+
payload = verify2(refreshCookie, this.jwtSecret, {
|
|
538
|
+
algorithms: ["HS256"]
|
|
539
|
+
});
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
if (!payload.jti) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const stored = await this.refreshTokenStore.find(payload.jti);
|
|
547
|
+
if (!stored) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
await this.refreshTokenStore.revoke(payload.jti);
|
|
551
|
+
const userId = String(payload.sub);
|
|
552
|
+
const user = await this.prismaModel.findUnique({
|
|
553
|
+
where: { [this.idField]: userId }
|
|
554
|
+
});
|
|
555
|
+
if (!user) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const accessToken = sign(
|
|
559
|
+
{ sub: userId },
|
|
560
|
+
this.jwtSecret,
|
|
561
|
+
{ expiresIn: this.jwtExpiresIn, algorithm: "HS256" }
|
|
562
|
+
);
|
|
563
|
+
res.cookie(this.cookieName, accessToken, this.cookieOptions);
|
|
564
|
+
await this.issueRefreshToken(userId, res);
|
|
565
|
+
if (this.csrf) {
|
|
566
|
+
const csrfToken = randomBytes(32).toString("hex");
|
|
567
|
+
res.cookie("csrf_token", csrfToken, {
|
|
568
|
+
httpOnly: false,
|
|
569
|
+
sameSite: this.cookieOptions.sameSite,
|
|
570
|
+
path: "/",
|
|
571
|
+
secure: this.cookieOptions.secure
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
await this.emitAudit({
|
|
575
|
+
action: "refresh",
|
|
576
|
+
userId,
|
|
577
|
+
ip: req.ip ?? req.socket?.remoteAddress,
|
|
578
|
+
success: true
|
|
579
|
+
});
|
|
580
|
+
return this.transformUser(user);
|
|
581
|
+
}
|
|
582
|
+
async issueRefreshToken(userId, res) {
|
|
583
|
+
const jti = randomUUID();
|
|
584
|
+
const expiresInMs = typeof this.refreshTokenExpiresIn === "number" ? this.refreshTokenExpiresIn * 1e3 : parseDuration(this.refreshTokenExpiresIn);
|
|
585
|
+
const expiresAt = new Date(Date.now() + expiresInMs);
|
|
586
|
+
const refreshToken = sign(
|
|
587
|
+
{ sub: userId, jti },
|
|
588
|
+
this.jwtSecret,
|
|
589
|
+
{ expiresIn: this.refreshTokenExpiresIn, algorithm: "HS256" }
|
|
590
|
+
);
|
|
591
|
+
await this.refreshTokenStore.save(jti, userId, expiresAt);
|
|
592
|
+
res.cookie(this.refreshTokenCookieName, refreshToken, {
|
|
593
|
+
...this.cookieOptions,
|
|
594
|
+
path: "/"
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async emitAudit(partial) {
|
|
598
|
+
if (!this.onAudit) return;
|
|
599
|
+
await this.onAudit({ ...partial, timestamp: /* @__PURE__ */ new Date() });
|
|
212
600
|
}
|
|
213
601
|
};
|
|
214
602
|
export {
|
|
215
603
|
AUTH_DRIVER,
|
|
216
604
|
AuthController,
|
|
217
605
|
AuthModule,
|
|
218
|
-
|
|
606
|
+
InMemoryLockoutStore,
|
|
607
|
+
InMemoryRefreshTokenStore,
|
|
608
|
+
JwtAuthGuard,
|
|
609
|
+
JwtPrismaCookieAuthDriver,
|
|
610
|
+
ROLES_KEY,
|
|
611
|
+
Roles,
|
|
612
|
+
RolesGuard
|
|
219
613
|
};
|
package/dist/vue.d.mts
CHANGED
|
@@ -19,12 +19,14 @@ declare class Auth<User extends Record<string, any>> {
|
|
|
19
19
|
on: Ref<boolean, boolean>;
|
|
20
20
|
loading: Ref<boolean, boolean>;
|
|
21
21
|
checkedIn: Ref<boolean, boolean>;
|
|
22
|
+
roles: Ref<string[]>;
|
|
22
23
|
private fetch;
|
|
23
24
|
private mapUser;
|
|
24
25
|
private afterLogout;
|
|
25
26
|
private afterCheckIn;
|
|
26
27
|
private routes;
|
|
27
28
|
constructor({ fetch, mapUser, routes, afterLogout, afterCheckIn, }: Options<User>);
|
|
29
|
+
signUp(payload: Record<string, any>): Promise<void>;
|
|
28
30
|
login(payload: Record<string, any>): Promise<void>;
|
|
29
31
|
checkIn(): Promise<void>;
|
|
30
32
|
logout(): Promise<void>;
|
package/dist/vue.d.ts
CHANGED
|
@@ -19,12 +19,14 @@ declare class Auth<User extends Record<string, any>> {
|
|
|
19
19
|
on: Ref<boolean, boolean>;
|
|
20
20
|
loading: Ref<boolean, boolean>;
|
|
21
21
|
checkedIn: Ref<boolean, boolean>;
|
|
22
|
+
roles: Ref<string[]>;
|
|
22
23
|
private fetch;
|
|
23
24
|
private mapUser;
|
|
24
25
|
private afterLogout;
|
|
25
26
|
private afterCheckIn;
|
|
26
27
|
private routes;
|
|
27
28
|
constructor({ fetch, mapUser, routes, afterLogout, afterCheckIn, }: Options<User>);
|
|
29
|
+
signUp(payload: Record<string, any>): Promise<void>;
|
|
28
30
|
login(payload: Record<string, any>): Promise<void>;
|
|
29
31
|
checkIn(): Promise<void>;
|
|
30
32
|
logout(): Promise<void>;
|
package/dist/vue.js
CHANGED
|
@@ -31,11 +31,13 @@ var Auth = class {
|
|
|
31
31
|
on = (0, import_vue.ref)(false);
|
|
32
32
|
loading = (0, import_vue.ref)(false);
|
|
33
33
|
checkedIn = (0, import_vue.ref)(false);
|
|
34
|
+
roles = (0, import_vue.ref)([]);
|
|
34
35
|
fetch;
|
|
35
36
|
mapUser;
|
|
36
37
|
afterLogout;
|
|
37
38
|
afterCheckIn;
|
|
38
39
|
routes = {
|
|
40
|
+
signUp: "/api/signup",
|
|
39
41
|
login: "/api/login",
|
|
40
42
|
checkIn: "/api/checkin",
|
|
41
43
|
logout: "/api/logout"
|
|
@@ -57,6 +59,20 @@ var Auth = class {
|
|
|
57
59
|
Object.assign(this.routes, routes);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
62
|
+
async signUp(payload) {
|
|
63
|
+
this.loading.value = true;
|
|
64
|
+
try {
|
|
65
|
+
const response = await this.fetch(this.routes.signUp, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify(payload)
|
|
68
|
+
});
|
|
69
|
+
this.on.value = true;
|
|
70
|
+
const { data } = await response.json();
|
|
71
|
+
this.setUser(data);
|
|
72
|
+
} finally {
|
|
73
|
+
this.loading.value = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
60
76
|
async login(payload) {
|
|
61
77
|
this.loading.value = true;
|
|
62
78
|
try {
|
|
@@ -104,6 +120,7 @@ var Auth = class {
|
|
|
104
120
|
setUser = (data) => {
|
|
105
121
|
if (data === null || data === void 0) {
|
|
106
122
|
this.user.value = null;
|
|
123
|
+
this.roles.value = [];
|
|
107
124
|
return;
|
|
108
125
|
}
|
|
109
126
|
const parsedData = this.mapUser(data);
|
|
@@ -113,6 +130,9 @@ var Auth = class {
|
|
|
113
130
|
for (const key in parsedData) {
|
|
114
131
|
this.user.value[key] = parsedData[key];
|
|
115
132
|
}
|
|
133
|
+
if (Array.isArray(parsedData.roles)) {
|
|
134
|
+
this.roles.value = parsedData.roles;
|
|
135
|
+
}
|
|
116
136
|
};
|
|
117
137
|
};
|
|
118
138
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/vue.mjs
CHANGED
|
@@ -5,11 +5,13 @@ var Auth = class {
|
|
|
5
5
|
on = ref(false);
|
|
6
6
|
loading = ref(false);
|
|
7
7
|
checkedIn = ref(false);
|
|
8
|
+
roles = ref([]);
|
|
8
9
|
fetch;
|
|
9
10
|
mapUser;
|
|
10
11
|
afterLogout;
|
|
11
12
|
afterCheckIn;
|
|
12
13
|
routes = {
|
|
14
|
+
signUp: "/api/signup",
|
|
13
15
|
login: "/api/login",
|
|
14
16
|
checkIn: "/api/checkin",
|
|
15
17
|
logout: "/api/logout"
|
|
@@ -31,6 +33,20 @@ var Auth = class {
|
|
|
31
33
|
Object.assign(this.routes, routes);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
async signUp(payload) {
|
|
37
|
+
this.loading.value = true;
|
|
38
|
+
try {
|
|
39
|
+
const response = await this.fetch(this.routes.signUp, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: JSON.stringify(payload)
|
|
42
|
+
});
|
|
43
|
+
this.on.value = true;
|
|
44
|
+
const { data } = await response.json();
|
|
45
|
+
this.setUser(data);
|
|
46
|
+
} finally {
|
|
47
|
+
this.loading.value = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
34
50
|
async login(payload) {
|
|
35
51
|
this.loading.value = true;
|
|
36
52
|
try {
|
|
@@ -78,6 +94,7 @@ var Auth = class {
|
|
|
78
94
|
setUser = (data) => {
|
|
79
95
|
if (data === null || data === void 0) {
|
|
80
96
|
this.user.value = null;
|
|
97
|
+
this.roles.value = [];
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
100
|
const parsedData = this.mapUser(data);
|
|
@@ -87,6 +104,9 @@ var Auth = class {
|
|
|
87
104
|
for (const key in parsedData) {
|
|
88
105
|
this.user.value[key] = parsedData[key];
|
|
89
106
|
}
|
|
107
|
+
if (Array.isArray(parsedData.roles)) {
|
|
108
|
+
this.roles.value = parsedData.roles;
|
|
109
|
+
}
|
|
90
110
|
};
|
|
91
111
|
};
|
|
92
112
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ministerjs/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"vue": "^3.5.12",
|
|
31
31
|
"@nestjs/common": "^11.0.11",
|
|
32
|
+
"@nestjs/core": "^11.0.11",
|
|
32
33
|
"@prisma/client": "^6.5.0",
|
|
33
34
|
"jsonwebtoken": "^9.0.2"
|
|
34
35
|
},
|
|
@@ -36,6 +37,9 @@
|
|
|
36
37
|
"@nestjs/common": {
|
|
37
38
|
"optional": true
|
|
38
39
|
},
|
|
40
|
+
"@nestjs/core": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
39
43
|
"@prisma/client": {
|
|
40
44
|
"optional": true
|
|
41
45
|
},
|
|
@@ -58,6 +62,7 @@
|
|
|
58
62
|
"vitest": "^3.0.9",
|
|
59
63
|
"vue": "^3.5.13",
|
|
60
64
|
"@nestjs/common": "^11.0.11",
|
|
65
|
+
"@nestjs/core": "^11.0.11",
|
|
61
66
|
"@types/jsonwebtoken": "^9.0.6",
|
|
62
67
|
"jsonwebtoken": "^9.0.2"
|
|
63
68
|
},
|