rl-core-api 0.11.4 → 0.13.0
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/core/config/env.schema.d.ts +2 -0
- package/dist/core/config/env.schema.js +10 -0
- package/dist/features/auth/domain/auth.service.d.ts +18 -2
- package/dist/features/auth/domain/auth.service.js +47 -10
- package/dist/features/auth/domain/token.service.d.ts +20 -3
- package/dist/features/auth/domain/token.service.js +81 -4
- package/dist/features/auth/infra/schema/authToken.schema.d.ts +2 -1
- package/dist/features/auth/infra/schema/authToken.schema.js +1 -0
- package/dist/features/auth/presentation/auth.controller.d.ts +10 -2
- package/dist/features/auth/presentation/auth.controller.js +60 -5
- package/dist/features/auth/presentation/commands/deviceLogin.command.d.ts +3 -0
- package/dist/features/auth/presentation/commands/deviceLogin.command.js +23 -0
- package/dist/features/auth/presentation/commands/refreshToken.command.d.ts +3 -0
- package/dist/features/auth/presentation/commands/refreshToken.command.js +13 -1
- package/dist/features/auth/presentation/responses/sessionUser.response.d.ts +15 -0
- package/dist/features/auth/presentation/responses/sessionUser.response.js +58 -1
- package/package.json +1 -1
|
@@ -30,6 +30,8 @@ export declare const envSchema: z.ZodObject<{
|
|
|
30
30
|
JWT_ACCESS_EXPIRES: z.ZodDefault<z.ZodString>;
|
|
31
31
|
JWT_PENDING_EXPIRES: z.ZodDefault<z.ZodString>;
|
|
32
32
|
JWT_REFRESH_EXPIRES_MINUTES: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
33
|
+
JWT_REFRESH_MOBILE_EXPIRES_MINUTES: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
34
|
+
DEVICE_TRUST_EXPIRES_MINUTES: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
33
35
|
TOTP_ENC_KEY: z.ZodString;
|
|
34
36
|
COOKIE_SECURE: z.ZodDefault<z.ZodPipe<z.ZodEnum<{
|
|
35
37
|
true: "true";
|
|
@@ -37,6 +37,16 @@ exports.envSchema = zod_1.z
|
|
|
37
37
|
.int()
|
|
38
38
|
.positive()
|
|
39
39
|
.default(360),
|
|
40
|
+
JWT_REFRESH_MOBILE_EXPIRES_MINUTES: zod_1.z.coerce
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.positive()
|
|
44
|
+
.default(43_200),
|
|
45
|
+
DEVICE_TRUST_EXPIRES_MINUTES: zod_1.z.coerce
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.positive()
|
|
49
|
+
.default(259_200),
|
|
40
50
|
TOTP_ENC_KEY: zod_1.z
|
|
41
51
|
.string()
|
|
42
52
|
.length(64, "TOTP_ENC_KEY deve ter 64 chars hex (32 bytes)"),
|
|
@@ -4,6 +4,7 @@ import { EnvService } from "../../../core/config/env.service";
|
|
|
4
4
|
import { MailerService } from "../../../core/mailer/mailer.service";
|
|
5
5
|
import { PasswordTokenPurpose, PasswordTokenStatus, TokenService } from "./token.service";
|
|
6
6
|
import { TotpSetup, TwoFactorService } from "./twoFactor.service";
|
|
7
|
+
import { ClientType } from "../presentation/commands/clientType.enum";
|
|
7
8
|
import { LoginCommand } from "../presentation/commands/login.command";
|
|
8
9
|
import { VerifyTwoFactorCommand } from "../presentation/commands/twoFactor.command";
|
|
9
10
|
import { RbacService } from "../../rbac/domain/rbac.service";
|
|
@@ -27,6 +28,18 @@ export interface LoginSession {
|
|
|
27
28
|
}
|
|
28
29
|
export interface VerifiedSession extends LoginSession {
|
|
29
30
|
backupCodes?: string[];
|
|
31
|
+
deviceToken?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface DeviceSession extends LoginSession {
|
|
34
|
+
deviceToken: string;
|
|
35
|
+
}
|
|
36
|
+
export interface TrustedDevice {
|
|
37
|
+
id: string;
|
|
38
|
+
family: string;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
expiresAt: Date;
|
|
41
|
+
ipAddress?: string | null;
|
|
42
|
+
userAgent?: string | null;
|
|
30
43
|
}
|
|
31
44
|
export interface RefreshedTokens {
|
|
32
45
|
accessToken: string;
|
|
@@ -54,8 +67,11 @@ export declare class AuthService {
|
|
|
54
67
|
}>;
|
|
55
68
|
verifyTwoFactor(dto: VerifyTwoFactorCommand, ctx: RequestCtx): Promise<VerifiedSession>;
|
|
56
69
|
regenerateBackupCodes(userId: string, code: string): Promise<string[]>;
|
|
57
|
-
refresh(rawToken: string | undefined, ctx: RequestCtx): Promise<RefreshedTokens>;
|
|
58
|
-
|
|
70
|
+
refresh(rawToken: string | undefined, ctx: RequestCtx, client?: ClientType): Promise<RefreshedTokens>;
|
|
71
|
+
loginWithDevice(rawToken: string | undefined, ctx: RequestCtx): Promise<DeviceSession>;
|
|
72
|
+
listTrustedDevices(userId: string): Promise<TrustedDevice[]>;
|
|
73
|
+
forgetTrustedDevice(userId: string, family: string): Promise<void>;
|
|
74
|
+
logout(rawRefreshToken?: string, rawDeviceToken?: string): Promise<void>;
|
|
59
75
|
forgotPassword(email: string): Promise<void>;
|
|
60
76
|
inspectPasswordToken(token: string): Promise<PasswordTokenCheck>;
|
|
61
77
|
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
@@ -20,6 +20,7 @@ const codedError_util_1 = require("../../../core/utils/codedError.util");
|
|
|
20
20
|
const hash_util_1 = require("../../../core/utils/hash.util");
|
|
21
21
|
const token_service_1 = require("./token.service");
|
|
22
22
|
const twoFactor_service_1 = require("./twoFactor.service");
|
|
23
|
+
const clientType_enum_1 = require("../presentation/commands/clientType.enum");
|
|
23
24
|
const rbac_service_1 = require("../../rbac/domain/rbac.service");
|
|
24
25
|
const users_service_1 = require("../../users/domain/users.service");
|
|
25
26
|
const BACKUP_FORMAT = /^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$/;
|
|
@@ -150,8 +151,12 @@ let AuthService = class AuthService {
|
|
|
150
151
|
user.backupCodes = remainingBackup;
|
|
151
152
|
}
|
|
152
153
|
await this.users.save(user);
|
|
153
|
-
const
|
|
154
|
-
|
|
154
|
+
const client = dto.client ?? clientType_enum_1.ClientType.WEB;
|
|
155
|
+
const tokens = await this.finalizeLogin(user, ctx, client);
|
|
156
|
+
const deviceToken = client === clientType_enum_1.ClientType.MOBILE
|
|
157
|
+
? await this.tokens.issueDeviceToken(user.id, ctx)
|
|
158
|
+
: undefined;
|
|
159
|
+
return { ...tokens, backupCodes, deviceToken };
|
|
155
160
|
}
|
|
156
161
|
async regenerateBackupCodes(userId, code) {
|
|
157
162
|
const user = await this.users.findById(userId);
|
|
@@ -170,11 +175,11 @@ let AuthService = class AuthService {
|
|
|
170
175
|
await this.users.save(user);
|
|
171
176
|
return generated.plain;
|
|
172
177
|
}
|
|
173
|
-
async refresh(rawToken, ctx) {
|
|
178
|
+
async refresh(rawToken, ctx, client = clientType_enum_1.ClientType.WEB) {
|
|
174
179
|
if (!rawToken) {
|
|
175
180
|
throw new common_1.UnauthorizedException("Refresh token ausente");
|
|
176
181
|
}
|
|
177
|
-
const { userId, rawToken: newRefresh } = await this.tokens.rotateRefreshToken(rawToken, ctx);
|
|
182
|
+
const { userId, rawToken: newRefresh } = await this.tokens.rotateRefreshToken(rawToken, ctx, client);
|
|
178
183
|
const user = await this.users.findById(userId);
|
|
179
184
|
if (!user.isActive) {
|
|
180
185
|
throw accountInactive();
|
|
@@ -182,11 +187,43 @@ let AuthService = class AuthService {
|
|
|
182
187
|
const accessToken = await this.signAccessToken(user);
|
|
183
188
|
return { accessToken, refreshToken: newRefresh };
|
|
184
189
|
}
|
|
185
|
-
async
|
|
186
|
-
if (!
|
|
187
|
-
|
|
190
|
+
async loginWithDevice(rawToken, ctx) {
|
|
191
|
+
if (!rawToken) {
|
|
192
|
+
throw new common_1.UnauthorizedException("Aparelho não reconhecido");
|
|
193
|
+
}
|
|
194
|
+
const { userId, rawToken: renewed } = await this.tokens.rotateDeviceToken(rawToken, ctx);
|
|
195
|
+
const user = await this.users.findById(userId);
|
|
196
|
+
if (!user.isActive) {
|
|
197
|
+
throw accountInactive();
|
|
198
|
+
}
|
|
199
|
+
if (user.isLocked(this.env.get("LOGIN_MAX_ATTEMPTS"))) {
|
|
200
|
+
throw accountLocked();
|
|
201
|
+
}
|
|
202
|
+
(0, auditContext_1.setAuditUser)(user.id);
|
|
203
|
+
const session = await this.finalizeLogin(user, ctx, clientType_enum_1.ClientType.MOBILE);
|
|
204
|
+
return { ...session, deviceToken: renewed };
|
|
205
|
+
}
|
|
206
|
+
async listTrustedDevices(userId) {
|
|
207
|
+
const devices = await this.tokens.listTrustedDevices(userId);
|
|
208
|
+
return devices.map((device) => ({
|
|
209
|
+
id: device.id,
|
|
210
|
+
family: device.family ?? device.id,
|
|
211
|
+
createdAt: device.createdAt,
|
|
212
|
+
expiresAt: device.expiresAt,
|
|
213
|
+
ipAddress: device.ipAddress,
|
|
214
|
+
userAgent: device.userAgent,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
async forgetTrustedDevice(userId, family) {
|
|
218
|
+
await this.tokens.revokeTrustedDevice(userId, family);
|
|
219
|
+
}
|
|
220
|
+
async logout(rawRefreshToken, rawDeviceToken) {
|
|
221
|
+
if (rawRefreshToken) {
|
|
222
|
+
await this.tokens.revokeByRawToken(rawRefreshToken);
|
|
223
|
+
}
|
|
224
|
+
if (rawDeviceToken) {
|
|
225
|
+
await this.tokens.revokeDeviceByRawToken(rawDeviceToken);
|
|
188
226
|
}
|
|
189
|
-
await this.tokens.revokeByRawToken(rawRefreshToken);
|
|
190
227
|
}
|
|
191
228
|
async forgotPassword(email) {
|
|
192
229
|
const user = await this.users.findByEmail(email);
|
|
@@ -284,9 +321,9 @@ let AuthService = class AuthService {
|
|
|
284
321
|
mustChangePassword: user.mustChangePassword,
|
|
285
322
|
};
|
|
286
323
|
}
|
|
287
|
-
async finalizeLogin(user, ctx) {
|
|
324
|
+
async finalizeLogin(user, ctx, client) {
|
|
288
325
|
const accessToken = await this.signAccessToken(user);
|
|
289
|
-
const { rawToken } = await this.tokens.issueRefreshToken(user.id, undefined, ctx);
|
|
326
|
+
const { rawToken } = await this.tokens.issueRefreshToken(user.id, undefined, ctx, client);
|
|
290
327
|
return {
|
|
291
328
|
accessToken,
|
|
292
329
|
refreshToken: rawToken,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EnvService } from "../../../core/config/env.service";
|
|
2
2
|
import { AuthTokenRepository } from "../infra/repositories/authToken.repository";
|
|
3
|
-
import { TokenType } from "../infra/schema/authToken.schema";
|
|
3
|
+
import { AuthToken, TokenType } from "../infra/schema/authToken.schema";
|
|
4
|
+
import { ClientType } from "../presentation/commands/clientType.enum";
|
|
4
5
|
export interface RotationResult {
|
|
5
6
|
userId: string;
|
|
6
7
|
rawToken: string;
|
|
@@ -29,17 +30,33 @@ export declare class TokenService {
|
|
|
29
30
|
constructor(repo: AuthTokenRepository, env: EnvService);
|
|
30
31
|
private persist;
|
|
31
32
|
private minutesFromNow;
|
|
33
|
+
private refreshMinutes;
|
|
32
34
|
issueRefreshToken(userId: string, family?: string, ctx?: {
|
|
33
35
|
ipAddress?: string;
|
|
34
36
|
userAgent?: string;
|
|
35
|
-
}): Promise<{
|
|
37
|
+
}, client?: ClientType): Promise<{
|
|
36
38
|
rawToken: string;
|
|
37
39
|
family: string;
|
|
38
40
|
}>;
|
|
39
41
|
rotateRefreshToken(rawToken: string, ctx?: {
|
|
40
42
|
ipAddress?: string;
|
|
41
43
|
userAgent?: string;
|
|
42
|
-
}): Promise<RotationResult>;
|
|
44
|
+
}, client?: ClientType): Promise<RotationResult>;
|
|
45
|
+
issueDeviceToken(userId: string, ctx?: {
|
|
46
|
+
ipAddress?: string;
|
|
47
|
+
userAgent?: string;
|
|
48
|
+
}, family?: string): Promise<string>;
|
|
49
|
+
rotateDeviceToken(rawToken: string, ctx?: {
|
|
50
|
+
ipAddress?: string;
|
|
51
|
+
userAgent?: string;
|
|
52
|
+
}): Promise<{
|
|
53
|
+
userId: string;
|
|
54
|
+
rawToken: string;
|
|
55
|
+
}>;
|
|
56
|
+
listTrustedDevices(userId: string): Promise<AuthToken[]>;
|
|
57
|
+
revokeAllDevices(userId: string): Promise<void>;
|
|
58
|
+
revokeTrustedDevice(userId: string, family: string): Promise<void>;
|
|
59
|
+
revokeDeviceByRawToken(rawToken: string): Promise<void>;
|
|
43
60
|
revokeFamily(family: string): Promise<void>;
|
|
44
61
|
revokeAllRefresh(userId: string): Promise<void>;
|
|
45
62
|
revokeByRawToken(rawToken: string): Promise<void>;
|
|
@@ -18,6 +18,7 @@ const env_service_1 = require("../../../core/config/env.service");
|
|
|
18
18
|
const hash_util_1 = require("../../../core/utils/hash.util");
|
|
19
19
|
const authToken_repository_1 = require("../infra/repositories/authToken.repository");
|
|
20
20
|
const authToken_schema_1 = require("../infra/schema/authToken.schema");
|
|
21
|
+
const clientType_enum_1 = require("../presentation/commands/clientType.enum");
|
|
21
22
|
var PasswordTokenStatus;
|
|
22
23
|
(function (PasswordTokenStatus) {
|
|
23
24
|
PasswordTokenStatus["VALID"] = "valid";
|
|
@@ -49,7 +50,12 @@ let TokenService = TokenService_1 = class TokenService {
|
|
|
49
50
|
minutesFromNow(minutes) {
|
|
50
51
|
return new Date(Date.now() + minutes * 60_000);
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
refreshMinutes(client) {
|
|
54
|
+
return client === clientType_enum_1.ClientType.MOBILE
|
|
55
|
+
? this.env.get("JWT_REFRESH_MOBILE_EXPIRES_MINUTES")
|
|
56
|
+
: this.env.get("JWT_REFRESH_EXPIRES_MINUTES");
|
|
57
|
+
}
|
|
58
|
+
async issueRefreshToken(userId, family, ctx, client = clientType_enum_1.ClientType.WEB) {
|
|
53
59
|
const rawToken = (0, hash_util_1.randomToken)(48);
|
|
54
60
|
const fam = family ?? (0, uuid_1.v7)();
|
|
55
61
|
await this.persist({
|
|
@@ -57,13 +63,13 @@ let TokenService = TokenService_1 = class TokenService {
|
|
|
57
63
|
rawToken,
|
|
58
64
|
type: authToken_schema_1.TokenType.REFRESH_TOKEN,
|
|
59
65
|
family: fam,
|
|
60
|
-
expiresAt: this.minutesFromNow(this.
|
|
66
|
+
expiresAt: this.minutesFromNow(this.refreshMinutes(client)),
|
|
61
67
|
ipAddress: ctx?.ipAddress,
|
|
62
68
|
userAgent: ctx?.userAgent,
|
|
63
69
|
});
|
|
64
70
|
return { rawToken, family: fam };
|
|
65
71
|
}
|
|
66
|
-
async rotateRefreshToken(rawToken, ctx) {
|
|
72
|
+
async rotateRefreshToken(rawToken, ctx, client = clientType_enum_1.ClientType.WEB) {
|
|
67
73
|
const tokenHash = (0, hash_util_1.sha256)(rawToken);
|
|
68
74
|
const stored = await this.repo.findOne({
|
|
69
75
|
where: { tokenHash, type: authToken_schema_1.TokenType.REFRESH_TOKEN },
|
|
@@ -83,9 +89,80 @@ let TokenService = TokenService_1 = class TokenService {
|
|
|
83
89
|
}
|
|
84
90
|
stored.used = true;
|
|
85
91
|
await this.repo.save(stored);
|
|
86
|
-
const { rawToken: newRaw, family } = await this.issueRefreshToken(stored.userId, stored.family ?? undefined, ctx);
|
|
92
|
+
const { rawToken: newRaw, family } = await this.issueRefreshToken(stored.userId, stored.family ?? undefined, ctx, client);
|
|
87
93
|
return { userId: stored.userId, rawToken: newRaw, family };
|
|
88
94
|
}
|
|
95
|
+
async issueDeviceToken(userId, ctx, family) {
|
|
96
|
+
if (!family) {
|
|
97
|
+
await this.revokeAllDevices(userId);
|
|
98
|
+
}
|
|
99
|
+
const rawToken = (0, hash_util_1.randomToken)(48);
|
|
100
|
+
await this.persist({
|
|
101
|
+
userId,
|
|
102
|
+
rawToken,
|
|
103
|
+
type: authToken_schema_1.TokenType.DEVICE_TRUST,
|
|
104
|
+
family: family ?? (0, uuid_1.v7)(),
|
|
105
|
+
expiresAt: this.minutesFromNow(this.env.get("DEVICE_TRUST_EXPIRES_MINUTES")),
|
|
106
|
+
ipAddress: ctx?.ipAddress,
|
|
107
|
+
userAgent: ctx?.userAgent,
|
|
108
|
+
});
|
|
109
|
+
return rawToken;
|
|
110
|
+
}
|
|
111
|
+
async rotateDeviceToken(rawToken, ctx) {
|
|
112
|
+
const stored = await this.repo.findOne({
|
|
113
|
+
where: { tokenHash: (0, hash_util_1.sha256)(rawToken), type: authToken_schema_1.TokenType.DEVICE_TRUST },
|
|
114
|
+
});
|
|
115
|
+
if (!stored) {
|
|
116
|
+
throw new common_1.UnauthorizedException("Aparelho não reconhecido");
|
|
117
|
+
}
|
|
118
|
+
if (stored.used || stored.revoked) {
|
|
119
|
+
this.logger.warn(`Reuso de vínculo de aparelho detectado (família ${stored.family})`);
|
|
120
|
+
if (stored.family) {
|
|
121
|
+
await this.revokeFamily(stored.family);
|
|
122
|
+
}
|
|
123
|
+
throw new common_1.UnauthorizedException("Aparelho revogado por segurança");
|
|
124
|
+
}
|
|
125
|
+
if (stored.isExpired()) {
|
|
126
|
+
throw new common_1.UnauthorizedException("Vínculo do aparelho expirado");
|
|
127
|
+
}
|
|
128
|
+
stored.used = true;
|
|
129
|
+
await this.repo.save(stored);
|
|
130
|
+
const renewed = await this.issueDeviceToken(stored.userId, ctx, stored.family ?? undefined);
|
|
131
|
+
return { userId: stored.userId, rawToken: renewed };
|
|
132
|
+
}
|
|
133
|
+
async listTrustedDevices(userId) {
|
|
134
|
+
const tokens = await this.repo.find({
|
|
135
|
+
where: { userId, type: authToken_schema_1.TokenType.DEVICE_TRUST, revoked: false },
|
|
136
|
+
order: { createdAt: "DESC" },
|
|
137
|
+
});
|
|
138
|
+
const latestByFamily = new Map();
|
|
139
|
+
for (const token of tokens) {
|
|
140
|
+
const key = token.family ?? token.id;
|
|
141
|
+
if (!latestByFamily.has(key) && !token.isExpired()) {
|
|
142
|
+
latestByFamily.set(key, token);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [...latestByFamily.values()];
|
|
146
|
+
}
|
|
147
|
+
async revokeAllDevices(userId) {
|
|
148
|
+
await this.repo.update({ userId, type: authToken_schema_1.TokenType.DEVICE_TRUST, revoked: false }, { revoked: true });
|
|
149
|
+
}
|
|
150
|
+
async revokeTrustedDevice(userId, family) {
|
|
151
|
+
await this.repo.update({ userId, family, type: authToken_schema_1.TokenType.DEVICE_TRUST }, { revoked: true });
|
|
152
|
+
}
|
|
153
|
+
async revokeDeviceByRawToken(rawToken) {
|
|
154
|
+
const stored = await this.repo.findOne({
|
|
155
|
+
where: { tokenHash: (0, hash_util_1.sha256)(rawToken), type: authToken_schema_1.TokenType.DEVICE_TRUST },
|
|
156
|
+
});
|
|
157
|
+
if (!stored) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (stored.family) {
|
|
161
|
+
await this.revokeFamily(stored.family);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await this.repo.update({ id: stored.id }, { revoked: true });
|
|
165
|
+
}
|
|
89
166
|
async revokeFamily(family) {
|
|
90
167
|
await this.repo.update({ family }, { revoked: true });
|
|
91
168
|
}
|
|
@@ -19,6 +19,7 @@ var TokenType;
|
|
|
19
19
|
TokenType[TokenType["TWO_FACTOR_EMAIL"] = 2] = "TWO_FACTOR_EMAIL";
|
|
20
20
|
TokenType[TokenType["REFRESH_TOKEN"] = 3] = "REFRESH_TOKEN";
|
|
21
21
|
TokenType[TokenType["TOTP_USED"] = 4] = "TOTP_USED";
|
|
22
|
+
TokenType[TokenType["DEVICE_TRUST"] = 5] = "DEVICE_TRUST";
|
|
22
23
|
})(TokenType || (exports.TokenType = TokenType = {}));
|
|
23
24
|
let AuthToken = class AuthToken extends base_schema_1.BaseEntity {
|
|
24
25
|
isExpired() {
|
|
@@ -5,16 +5,19 @@ import { AuthService } from "../domain/auth.service";
|
|
|
5
5
|
import { PasswordTokenCheck } from "../domain/auth.service";
|
|
6
6
|
import { SessionUser } from "../domain/auth.service";
|
|
7
7
|
import { LoginChallenge } from "../domain/auth.service";
|
|
8
|
+
import { DeviceSession, TrustedDevice } from "../domain/auth.service";
|
|
8
9
|
import { TotpSetup } from "../domain/twoFactor.service";
|
|
10
|
+
import { DeviceLoginCommand } from "./commands/deviceLogin.command";
|
|
9
11
|
import { LoginCommand } from "./commands/login.command";
|
|
10
12
|
import { ForgotPasswordCommand, ResetPasswordCommand } from "./commands/password.command";
|
|
11
|
-
import { RefreshTokenCommand } from "./commands/refreshToken.command";
|
|
13
|
+
import { LogoutCommand, RefreshTokenCommand } from "./commands/refreshToken.command";
|
|
12
14
|
import { PendingTokenCommand, RegenerateBackupCommand, VerifyTwoFactorCommand } from "./commands/twoFactor.command";
|
|
13
15
|
export interface VerifyResponse {
|
|
14
16
|
user: SessionUser;
|
|
15
17
|
backupCodes?: string[];
|
|
16
18
|
accessToken?: string;
|
|
17
19
|
refreshToken?: string;
|
|
20
|
+
deviceToken?: string;
|
|
18
21
|
}
|
|
19
22
|
export interface RefreshResponse {
|
|
20
23
|
ok: boolean;
|
|
@@ -32,7 +35,12 @@ export declare class AuthController {
|
|
|
32
35
|
}>;
|
|
33
36
|
verify(dto: VerifyTwoFactorCommand, req: Request, res: Response): Promise<VerifyResponse>;
|
|
34
37
|
refresh(dto: RefreshTokenCommand, req: Request, res: Response): Promise<RefreshResponse>;
|
|
35
|
-
logout(dto:
|
|
38
|
+
logout(dto: LogoutCommand, req: Request, res: Response): Promise<{
|
|
39
|
+
ok: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
deviceLogin(dto: DeviceLoginCommand, req: Request): Promise<DeviceSession>;
|
|
42
|
+
devices(user: AuthenticatedUser): Promise<TrustedDevice[]>;
|
|
43
|
+
forgetDevice(user: AuthenticatedUser, family: string): Promise<{
|
|
36
44
|
ok: boolean;
|
|
37
45
|
}>;
|
|
38
46
|
profile(user: AuthenticatedUser): AuthenticatedUser;
|
|
@@ -26,6 +26,7 @@ const ip_util_1 = require("../../../core/utils/ip.util");
|
|
|
26
26
|
const auditable_decorator_1 = require("../../audit/presentation/decorators/auditable.decorator");
|
|
27
27
|
const auth_service_1 = require("../domain/auth.service");
|
|
28
28
|
const clientType_enum_1 = require("./commands/clientType.enum");
|
|
29
|
+
const deviceLogin_command_1 = require("./commands/deviceLogin.command");
|
|
29
30
|
const login_command_1 = require("./commands/login.command");
|
|
30
31
|
const password_command_1 = require("./commands/password.command");
|
|
31
32
|
const refreshToken_command_1 = require("./commands/refreshToken.command");
|
|
@@ -52,10 +53,10 @@ let AuthController = class AuthController {
|
|
|
52
53
|
return this.auth.requestEmailCode(dto.pendingToken);
|
|
53
54
|
}
|
|
54
55
|
async verify(dto, req, res) {
|
|
55
|
-
const { accessToken, refreshToken, user, backupCodes } = await this.auth.verifyTwoFactor(dto, ctxFrom(req));
|
|
56
|
+
const { accessToken, refreshToken, user, backupCodes, deviceToken } = await this.auth.verifyTwoFactor(dto, ctxFrom(req));
|
|
56
57
|
(0, requestLog_context_1.setRequestLogUser)(req, user.id);
|
|
57
58
|
if (dto.client === clientType_enum_1.ClientType.MOBILE) {
|
|
58
|
-
return { user, backupCodes, accessToken, refreshToken };
|
|
59
|
+
return { user, backupCodes, accessToken, refreshToken, deviceToken };
|
|
59
60
|
}
|
|
60
61
|
(0, cookie_util_1.setAuthCookies)(res, this.env, { accessToken, refreshToken });
|
|
61
62
|
return { user, backupCodes };
|
|
@@ -63,7 +64,8 @@ let AuthController = class AuthController {
|
|
|
63
64
|
async refresh(dto, req, res) {
|
|
64
65
|
const cookies = req.cookies;
|
|
65
66
|
const raw = dto.refreshToken ?? cookies?.[cookie_util_1.REFRESH_COOKIE];
|
|
66
|
-
const
|
|
67
|
+
const client = dto.refreshToken ? clientType_enum_1.ClientType.MOBILE : clientType_enum_1.ClientType.WEB;
|
|
68
|
+
const tokens = await this.auth.refresh(raw, ctxFrom(req), client);
|
|
67
69
|
if (dto.refreshToken) {
|
|
68
70
|
return { ok: true, ...tokens };
|
|
69
71
|
}
|
|
@@ -72,10 +74,22 @@ let AuthController = class AuthController {
|
|
|
72
74
|
}
|
|
73
75
|
async logout(dto, req, res) {
|
|
74
76
|
const cookies = req.cookies;
|
|
75
|
-
await this.auth.logout(dto.refreshToken ?? cookies?.[cookie_util_1.REFRESH_COOKIE]);
|
|
77
|
+
await this.auth.logout(dto.refreshToken ?? cookies?.[cookie_util_1.REFRESH_COOKIE], dto.deviceToken);
|
|
76
78
|
(0, cookie_util_1.clearAuthCookies)(res, this.env);
|
|
77
79
|
return { ok: true };
|
|
78
80
|
}
|
|
81
|
+
async deviceLogin(dto, req) {
|
|
82
|
+
const session = await this.auth.loginWithDevice(dto.deviceToken, ctxFrom(req));
|
|
83
|
+
(0, requestLog_context_1.setRequestLogUser)(req, session.user.id);
|
|
84
|
+
return session;
|
|
85
|
+
}
|
|
86
|
+
devices(user) {
|
|
87
|
+
return this.auth.listTrustedDevices(user.id);
|
|
88
|
+
}
|
|
89
|
+
async forgetDevice(user, family) {
|
|
90
|
+
await this.auth.forgetTrustedDevice(user.id, family);
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
79
93
|
profile(user) {
|
|
80
94
|
return user;
|
|
81
95
|
}
|
|
@@ -170,9 +184,50 @@ __decorate([
|
|
|
170
184
|
__param(1, (0, common_1.Req)()),
|
|
171
185
|
__param(2, (0, common_1.Res)({ passthrough: true })),
|
|
172
186
|
__metadata("design:type", Function),
|
|
173
|
-
__metadata("design:paramtypes", [refreshToken_command_1.
|
|
187
|
+
__metadata("design:paramtypes", [refreshToken_command_1.LogoutCommand, Object, Object]),
|
|
174
188
|
__metadata("design:returntype", Promise)
|
|
175
189
|
], AuthController.prototype, "logout", null);
|
|
190
|
+
__decorate([
|
|
191
|
+
(0, public_decorator_1.Public)(),
|
|
192
|
+
(0, throttler_1.Throttle)({ default: { limit: 5, ttl: 60_000 } }),
|
|
193
|
+
(0, auditable_decorator_1.Auditable)("Login pelo aparelho confiável"),
|
|
194
|
+
(0, common_1.Post)("device/login"),
|
|
195
|
+
(0, swagger_1.ApiOperation)({
|
|
196
|
+
summary: "Login pelo aparelho confiável: sem senha e sem 2FA",
|
|
197
|
+
description: "O app apresenta o vínculo criado num login completo, depois de destravá-lo com a biometria. O vínculo rotaciona a cada uso.",
|
|
198
|
+
}),
|
|
199
|
+
(0, swagger_1.ApiOkResponse)({ type: sessionUser_response_1.DeviceLoginResponse }),
|
|
200
|
+
__param(0, (0, common_1.Body)()),
|
|
201
|
+
__param(1, (0, common_1.Req)()),
|
|
202
|
+
__metadata("design:type", Function),
|
|
203
|
+
__metadata("design:paramtypes", [deviceLogin_command_1.DeviceLoginCommand, Object]),
|
|
204
|
+
__metadata("design:returntype", Promise)
|
|
205
|
+
], AuthController.prototype, "deviceLogin", null);
|
|
206
|
+
__decorate([
|
|
207
|
+
(0, common_1.UseGuards)(jwtAuth_guard_1.JwtAuthGuard),
|
|
208
|
+
(0, swagger_1.ApiBearerAuth)(),
|
|
209
|
+
(0, common_1.Get)("devices"),
|
|
210
|
+
(0, swagger_1.ApiOperation)({ summary: "Aparelhos confiáveis da conta" }),
|
|
211
|
+
(0, swagger_1.ApiOkResponse)({ type: sessionUser_response_1.TrustedDeviceResponse, isArray: true }),
|
|
212
|
+
__param(0, (0, currentUser_decorator_1.CurrentUser)()),
|
|
213
|
+
__metadata("design:type", Function),
|
|
214
|
+
__metadata("design:paramtypes", [Object]),
|
|
215
|
+
__metadata("design:returntype", Promise)
|
|
216
|
+
], AuthController.prototype, "devices", null);
|
|
217
|
+
__decorate([
|
|
218
|
+
(0, common_1.UseGuards)(jwtAuth_guard_1.JwtAuthGuard),
|
|
219
|
+
(0, swagger_1.ApiBearerAuth)(),
|
|
220
|
+
(0, auditable_decorator_1.Auditable)("Esquecer aparelho confiável"),
|
|
221
|
+
(0, common_1.Delete)("devices/:family"),
|
|
222
|
+
(0, swagger_1.ApiOperation)({
|
|
223
|
+
summary: "Esquece um aparelho: o próximo acesso dele exige senha + 2FA",
|
|
224
|
+
}),
|
|
225
|
+
__param(0, (0, currentUser_decorator_1.CurrentUser)()),
|
|
226
|
+
__param(1, (0, common_1.Param)("family")),
|
|
227
|
+
__metadata("design:type", Function),
|
|
228
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
229
|
+
__metadata("design:returntype", Promise)
|
|
230
|
+
], AuthController.prototype, "forgetDevice", null);
|
|
176
231
|
__decorate([
|
|
177
232
|
(0, common_1.UseGuards)(jwtAuth_guard_1.JwtAuthGuard),
|
|
178
233
|
(0, swagger_1.ApiBearerAuth)(),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DeviceLoginCommand = void 0;
|
|
13
|
+
const swagger_1 = require("@nestjs/swagger");
|
|
14
|
+
const class_validator_1 = require("class-validator");
|
|
15
|
+
class DeviceLoginCommand {
|
|
16
|
+
}
|
|
17
|
+
exports.DeviceLoginCommand = DeviceLoginCommand;
|
|
18
|
+
__decorate([
|
|
19
|
+
(0, swagger_1.ApiProperty)({ description: "Vínculo do aparelho, guardado no Keychain" }),
|
|
20
|
+
(0, class_validator_1.IsString)(),
|
|
21
|
+
(0, class_validator_1.MinLength)(32),
|
|
22
|
+
__metadata("design:type", String)
|
|
23
|
+
], DeviceLoginCommand.prototype, "deviceToken", void 0);
|
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.RefreshTokenCommand = void 0;
|
|
12
|
+
exports.LogoutCommand = exports.RefreshTokenCommand = void 0;
|
|
13
13
|
const swagger_1 = require("@nestjs/swagger");
|
|
14
14
|
const class_validator_1 = require("class-validator");
|
|
15
15
|
class RefreshTokenCommand {
|
|
@@ -24,3 +24,15 @@ __decorate([
|
|
|
24
24
|
(0, class_validator_1.IsString)(),
|
|
25
25
|
__metadata("design:type", String)
|
|
26
26
|
], RefreshTokenCommand.prototype, "refreshToken", void 0);
|
|
27
|
+
class LogoutCommand extends RefreshTokenCommand {
|
|
28
|
+
}
|
|
29
|
+
exports.LogoutCommand = LogoutCommand;
|
|
30
|
+
__decorate([
|
|
31
|
+
(0, swagger_1.ApiProperty)({
|
|
32
|
+
required: false,
|
|
33
|
+
description: "Enviar apenas para esquecer o aparelho junto com a saída",
|
|
34
|
+
}),
|
|
35
|
+
(0, class_validator_1.IsOptional)(),
|
|
36
|
+
(0, class_validator_1.IsString)(),
|
|
37
|
+
__metadata("design:type", String)
|
|
38
|
+
], LogoutCommand.prototype, "deviceToken", void 0);
|
|
@@ -16,4 +16,19 @@ export declare class VerifyTwoFactorResponse {
|
|
|
16
16
|
backupCodes?: string[];
|
|
17
17
|
accessToken?: string;
|
|
18
18
|
refreshToken?: string;
|
|
19
|
+
deviceToken?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class DeviceLoginResponse {
|
|
22
|
+
user: SessionUserResponse;
|
|
23
|
+
accessToken: string;
|
|
24
|
+
refreshToken: string;
|
|
25
|
+
deviceToken: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class TrustedDeviceResponse {
|
|
28
|
+
id: string;
|
|
29
|
+
family: string;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
expiresAt: Date;
|
|
32
|
+
ipAddress?: string | null;
|
|
33
|
+
userAgent?: string | null;
|
|
19
34
|
}
|
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.VerifyTwoFactorResponse = exports.SessionUserResponse = void 0;
|
|
12
|
+
exports.TrustedDeviceResponse = exports.DeviceLoginResponse = exports.VerifyTwoFactorResponse = exports.SessionUserResponse = void 0;
|
|
13
13
|
const swagger_1 = require("@nestjs/swagger");
|
|
14
14
|
class SessionUserResponse {
|
|
15
15
|
}
|
|
@@ -88,3 +88,60 @@ __decorate([
|
|
|
88
88
|
}),
|
|
89
89
|
__metadata("design:type", String)
|
|
90
90
|
], VerifyTwoFactorResponse.prototype, "refreshToken", void 0);
|
|
91
|
+
__decorate([
|
|
92
|
+
(0, swagger_1.ApiProperty)({
|
|
93
|
+
type: String,
|
|
94
|
+
required: false,
|
|
95
|
+
description: "Vínculo do aparelho (client=mobile). Guardar no Keychain: é o que abre os próximos acessos sem senha e sem 2FA",
|
|
96
|
+
}),
|
|
97
|
+
__metadata("design:type", String)
|
|
98
|
+
], VerifyTwoFactorResponse.prototype, "deviceToken", void 0);
|
|
99
|
+
class DeviceLoginResponse {
|
|
100
|
+
}
|
|
101
|
+
exports.DeviceLoginResponse = DeviceLoginResponse;
|
|
102
|
+
__decorate([
|
|
103
|
+
(0, swagger_1.ApiProperty)({ type: SessionUserResponse }),
|
|
104
|
+
__metadata("design:type", SessionUserResponse)
|
|
105
|
+
], DeviceLoginResponse.prototype, "user", void 0);
|
|
106
|
+
__decorate([
|
|
107
|
+
(0, swagger_1.ApiProperty)(),
|
|
108
|
+
__metadata("design:type", String)
|
|
109
|
+
], DeviceLoginResponse.prototype, "accessToken", void 0);
|
|
110
|
+
__decorate([
|
|
111
|
+
(0, swagger_1.ApiProperty)(),
|
|
112
|
+
__metadata("design:type", String)
|
|
113
|
+
], DeviceLoginResponse.prototype, "refreshToken", void 0);
|
|
114
|
+
__decorate([
|
|
115
|
+
(0, swagger_1.ApiProperty)({ description: "O vínculo rotaciona a cada uso: guarde este" }),
|
|
116
|
+
__metadata("design:type", String)
|
|
117
|
+
], DeviceLoginResponse.prototype, "deviceToken", void 0);
|
|
118
|
+
class TrustedDeviceResponse {
|
|
119
|
+
}
|
|
120
|
+
exports.TrustedDeviceResponse = TrustedDeviceResponse;
|
|
121
|
+
__decorate([
|
|
122
|
+
(0, swagger_1.ApiProperty)({ format: "uuid" }),
|
|
123
|
+
__metadata("design:type", String)
|
|
124
|
+
], TrustedDeviceResponse.prototype, "id", void 0);
|
|
125
|
+
__decorate([
|
|
126
|
+
(0, swagger_1.ApiProperty)({
|
|
127
|
+
format: "uuid",
|
|
128
|
+
description: "Identifica o aparelho ao longo das rotações — é o que se revoga",
|
|
129
|
+
}),
|
|
130
|
+
__metadata("design:type", String)
|
|
131
|
+
], TrustedDeviceResponse.prototype, "family", void 0);
|
|
132
|
+
__decorate([
|
|
133
|
+
(0, swagger_1.ApiProperty)(),
|
|
134
|
+
__metadata("design:type", Date)
|
|
135
|
+
], TrustedDeviceResponse.prototype, "createdAt", void 0);
|
|
136
|
+
__decorate([
|
|
137
|
+
(0, swagger_1.ApiProperty)(),
|
|
138
|
+
__metadata("design:type", Date)
|
|
139
|
+
], TrustedDeviceResponse.prototype, "expiresAt", void 0);
|
|
140
|
+
__decorate([
|
|
141
|
+
(0, swagger_1.ApiProperty)({ required: false, nullable: true }),
|
|
142
|
+
__metadata("design:type", Object)
|
|
143
|
+
], TrustedDeviceResponse.prototype, "ipAddress", void 0);
|
|
144
|
+
__decorate([
|
|
145
|
+
(0, swagger_1.ApiProperty)({ required: false, nullable: true }),
|
|
146
|
+
__metadata("design:type", Object)
|
|
147
|
+
], TrustedDeviceResponse.prototype, "userAgent", void 0);
|
package/package.json
CHANGED