ar-saas 0.3.3 → 0.4.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/dist/generator.js +2 -6
- package/package.json +1 -1
- package/templates/backend/.env.example +1 -1
- package/templates/backend/README.md +6 -6
- package/templates/backend/package.json +5 -2
- package/templates/backend/src/app.module.ts +68 -40
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +27 -45
- package/templates/backend/src/main.ts +50 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +162 -158
- package/templates/backend/src/modules/auth/auth.service.ts +236 -257
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +45 -43
- package/templates/backend/src/modules/users/users.controller.ts +28 -0
- package/templates/backend/src/modules/users/users.module.ts +16 -14
- package/templates/backend/src/modules/users/users.repository.ts +57 -51
- package/templates/backend/src/modules/users/users.service.ts +130 -104
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +38 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +51 -42
- package/templates/frontend/README.md +2 -2
- package/templates/frontend/package.json +2 -6
- package/templates/frontend/pnpm-workspace.yaml +2 -2
- package/templates/frontend/src/app/(auth)/layout.tsx +29 -28
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +241 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +155 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +179 -178
- package/templates/frontend/src/app/layout.tsx +29 -26
- package/templates/frontend/src/app/page.tsx +1 -1
- package/templates/frontend/src/app/setup/page.tsx +1 -1
- package/templates/frontend/src/components/dashboard/header.tsx +5 -3
- package/templates/frontend/src/config/site.ts +1 -1
|
@@ -1,257 +1,236 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BadRequestException,
|
|
3
|
-
Injectable,
|
|
4
|
-
InternalServerErrorException,
|
|
5
|
-
UnauthorizedException,
|
|
6
|
-
} from '@nestjs/common';
|
|
7
|
-
import { ConfigService } from '@nestjs/config';
|
|
8
|
-
import { JwtService } from '@nestjs/jwt';
|
|
9
|
-
import * as crypto from 'crypto';
|
|
10
|
-
import * as bcrypt from 'bcryptjs';
|
|
11
|
-
import type { StringValue } from 'ms';
|
|
12
|
-
import { MailService } from '../mail/mail.service';
|
|
13
|
-
import { UsersService } from '../users/users.service';
|
|
14
|
-
import { UserDocument } from '../users/schemas/user.schema';
|
|
15
|
-
import { WorkspacesService } from '../workspaces/workspaces.service';
|
|
16
|
-
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
|
17
|
-
import { LoginDto } from './dto/login.dto';
|
|
18
|
-
import { RegisterDto } from './dto/register.dto';
|
|
19
|
-
import { ResetPasswordDto } from './dto/reset-password.dto';
|
|
20
|
-
|
|
21
|
-
interface TokenPair {
|
|
22
|
-
accessToken: string;
|
|
23
|
-
refreshToken: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
@Injectable()
|
|
27
|
-
export class AuthService {
|
|
28
|
-
constructor(
|
|
29
|
-
private readonly usersService: UsersService,
|
|
30
|
-
private readonly workspacesService: WorkspacesService,
|
|
31
|
-
private readonly mailService: MailService,
|
|
32
|
-
private readonly jwtService: JwtService,
|
|
33
|
-
private readonly configService: ConfigService,
|
|
34
|
-
) {}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
dto
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
userId,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const accessExpiresIn = (
|
|
238
|
-
this.configService.get<string>('JWT_ACCESS_EXPIRES_IN') ?? '15m'
|
|
239
|
-
) as StringValue;
|
|
240
|
-
const refreshExpiresIn = (
|
|
241
|
-
this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') ?? '7d'
|
|
242
|
-
) as StringValue;
|
|
243
|
-
|
|
244
|
-
const [accessToken, refreshToken] = await Promise.all([
|
|
245
|
-
this.jwtService.signAsync(accessPayload, {
|
|
246
|
-
secret: this.configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
|
|
247
|
-
expiresIn: accessExpiresIn,
|
|
248
|
-
}),
|
|
249
|
-
this.jwtService.signAsync(refreshPayload, {
|
|
250
|
-
secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
|
|
251
|
-
expiresIn: refreshExpiresIn,
|
|
252
|
-
}),
|
|
253
|
-
]);
|
|
254
|
-
|
|
255
|
-
return { accessToken, refreshToken };
|
|
256
|
-
}
|
|
257
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Injectable,
|
|
4
|
+
InternalServerErrorException,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { ConfigService } from '@nestjs/config';
|
|
8
|
+
import { JwtService } from '@nestjs/jwt';
|
|
9
|
+
import * as crypto from 'crypto';
|
|
10
|
+
import * as bcrypt from 'bcryptjs';
|
|
11
|
+
import type { StringValue } from 'ms';
|
|
12
|
+
import { MailService } from '../mail/mail.service';
|
|
13
|
+
import { UsersService } from '../users/users.service';
|
|
14
|
+
import { UserDocument } from '../users/schemas/user.schema';
|
|
15
|
+
import { WorkspacesService } from '../workspaces/workspaces.service';
|
|
16
|
+
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
|
17
|
+
import { LoginDto } from './dto/login.dto';
|
|
18
|
+
import { RegisterDto } from './dto/register.dto';
|
|
19
|
+
import { ResetPasswordDto } from './dto/reset-password.dto';
|
|
20
|
+
|
|
21
|
+
interface TokenPair {
|
|
22
|
+
accessToken: string;
|
|
23
|
+
refreshToken: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Injectable()
|
|
27
|
+
export class AuthService {
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly usersService: UsersService,
|
|
30
|
+
private readonly workspacesService: WorkspacesService,
|
|
31
|
+
private readonly mailService: MailService,
|
|
32
|
+
private readonly jwtService: JwtService,
|
|
33
|
+
private readonly configService: ConfigService,
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async register(dto: RegisterDto): Promise<{ message: string }> {
|
|
37
|
+
const workspace = await this.workspacesService.create('pending', dto.name);
|
|
38
|
+
const workspaceId = String(workspace._id);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const user = await this.usersService.create({
|
|
42
|
+
name: dto.name,
|
|
43
|
+
email: dto.email,
|
|
44
|
+
password: dto.password,
|
|
45
|
+
workspaceId,
|
|
46
|
+
});
|
|
47
|
+
const userId = String(user._id);
|
|
48
|
+
|
|
49
|
+
await this.workspacesService.update(workspaceId, { ownerId: userId });
|
|
50
|
+
|
|
51
|
+
const token = this.generateSecureToken();
|
|
52
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
53
|
+
await this.usersService.setEmailVerificationToken(userId, token, expiresAt);
|
|
54
|
+
await this.mailService.sendVerificationEmail(dto.email, dto.name, token);
|
|
55
|
+
|
|
56
|
+
return { message: 'Verification email sent' };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
await this.workspacesService.delete(workspaceId);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async verifyEmail(token: string): Promise<{ message: string }> {
|
|
64
|
+
const user = await this.usersService.findByVerificationToken(token);
|
|
65
|
+
if (!user) {
|
|
66
|
+
throw new BadRequestException('Token inválido o expirado.');
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
user.emailVerificationTokenExpiresAt &&
|
|
70
|
+
user.emailVerificationTokenExpiresAt < new Date()
|
|
71
|
+
) {
|
|
72
|
+
throw new BadRequestException(
|
|
73
|
+
'El enlace de verificación expiró. Solicitá uno nuevo.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
await this.usersService.markEmailVerified(String(user._id));
|
|
77
|
+
return { message: 'Email verified' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async login(
|
|
81
|
+
dto: LoginDto,
|
|
82
|
+
): Promise<{ user: UserDocument } & TokenPair> {
|
|
83
|
+
const user = await this.usersService.findByEmailWithPassword(dto.email);
|
|
84
|
+
if (!user?.password) {
|
|
85
|
+
throw new UnauthorizedException('Email o contraseña incorrectos.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const valid = await bcrypt.compare(dto.password, user.password);
|
|
89
|
+
if (!valid) {
|
|
90
|
+
throw new UnauthorizedException('Email o contraseña incorrectos.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!user.emailVerified) {
|
|
94
|
+
throw new UnauthorizedException(
|
|
95
|
+
'Por favor verificá tu email antes de ingresar.',
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const userId = String(user._id);
|
|
100
|
+
const tokens = await this.generateTokens(
|
|
101
|
+
userId,
|
|
102
|
+
user.email,
|
|
103
|
+
user.workspaceId,
|
|
104
|
+
user.role,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await Promise.all([
|
|
108
|
+
this.usersService.updateRefreshToken(userId, tokens.refreshToken),
|
|
109
|
+
this.usersService.updateLastLoginAt(userId),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const cleanUser = await this.usersService.findById(userId);
|
|
113
|
+
if (!cleanUser) {
|
|
114
|
+
throw new InternalServerErrorException('Error al obtener el usuario.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { user: cleanUser, ...tokens };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async refresh(token: string): Promise<TokenPair> {
|
|
121
|
+
let payload: { sub: string };
|
|
122
|
+
try {
|
|
123
|
+
payload = await this.jwtService.verifyAsync<{ sub: string }>(token, {
|
|
124
|
+
secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
throw new UnauthorizedException('Refresh token inválido.');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const user = await this.usersService.validateRefreshToken(
|
|
131
|
+
payload.sub,
|
|
132
|
+
token,
|
|
133
|
+
);
|
|
134
|
+
if (!user) {
|
|
135
|
+
throw new UnauthorizedException('Refresh token inválido.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const userId = String(user._id);
|
|
139
|
+
const tokens = await this.generateTokens(
|
|
140
|
+
userId,
|
|
141
|
+
user.email,
|
|
142
|
+
user.workspaceId,
|
|
143
|
+
user.role,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
await this.usersService.updateRefreshToken(userId, tokens.refreshToken);
|
|
147
|
+
return tokens;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async logout(userId: string): Promise<void> {
|
|
151
|
+
await this.usersService.updateRefreshToken(userId, null);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async forgotPassword(
|
|
155
|
+
dto: ForgotPasswordDto,
|
|
156
|
+
): Promise<{ message: string }> {
|
|
157
|
+
const user = await this.usersService.findByEmail(dto.email);
|
|
158
|
+
|
|
159
|
+
if (user?.emailVerified) {
|
|
160
|
+
const token = this.generateSecureToken();
|
|
161
|
+
const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
|
|
162
|
+
await this.usersService.setPasswordResetToken(
|
|
163
|
+
String(user._id),
|
|
164
|
+
token,
|
|
165
|
+
expiresAt,
|
|
166
|
+
);
|
|
167
|
+
await this.mailService.sendPasswordResetEmail(
|
|
168
|
+
dto.email,
|
|
169
|
+
user.name,
|
|
170
|
+
token,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { message: 'Reset email sent' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async resetPassword(dto: ResetPasswordDto): Promise<{ message: string }> {
|
|
178
|
+
const user = await this.usersService.findByPasswordResetToken(dto.token);
|
|
179
|
+
if (!user) {
|
|
180
|
+
throw new BadRequestException('Token inválido o expirado.');
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
user.passwordResetTokenExpiresAt &&
|
|
184
|
+
user.passwordResetTokenExpiresAt < new Date()
|
|
185
|
+
) {
|
|
186
|
+
throw new BadRequestException(
|
|
187
|
+
'El enlace de restablecimiento expiró. Solicitá uno nuevo.',
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await this.usersService.updatePassword(String(user._id), dto.newPassword);
|
|
192
|
+
return { message: 'Password reset successful' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async getMe(userId: string): Promise<UserDocument> {
|
|
196
|
+
const user = await this.usersService.findById(userId);
|
|
197
|
+
if (!user) {
|
|
198
|
+
throw new UnauthorizedException('Usuario no encontrado.');
|
|
199
|
+
}
|
|
200
|
+
return user;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private generateSecureToken(): string {
|
|
204
|
+
return crypto.randomBytes(32).toString('hex');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async generateTokens(
|
|
208
|
+
userId: string,
|
|
209
|
+
email: string,
|
|
210
|
+
workspaceId: string,
|
|
211
|
+
role: string,
|
|
212
|
+
): Promise<TokenPair> {
|
|
213
|
+
const accessPayload = { sub: userId, email, workspaceId, role };
|
|
214
|
+
const refreshPayload = { sub: userId };
|
|
215
|
+
|
|
216
|
+
const accessExpiresIn = (
|
|
217
|
+
this.configService.get<string>('JWT_ACCESS_EXPIRES_IN') ?? '15m'
|
|
218
|
+
) as StringValue;
|
|
219
|
+
const refreshExpiresIn = (
|
|
220
|
+
this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') ?? '7d'
|
|
221
|
+
) as StringValue;
|
|
222
|
+
|
|
223
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
224
|
+
this.jwtService.signAsync(accessPayload, {
|
|
225
|
+
secret: this.configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
|
|
226
|
+
expiresIn: accessExpiresIn,
|
|
227
|
+
}),
|
|
228
|
+
this.jwtService.signAsync(refreshPayload, {
|
|
229
|
+
secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
|
|
230
|
+
expiresIn: refreshExpiresIn,
|
|
231
|
+
}),
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
return { accessToken, refreshToken };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -1,43 +1,45 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import { ConfigService } from '@nestjs/config';
|
|
3
|
-
import { PassportStrategy } from '@nestjs/passport';
|
|
4
|
-
import { Request } from 'express';
|
|
5
|
-
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
6
|
-
|
|
7
|
-
export interface JwtPayload {
|
|
8
|
-
sub: string;
|
|
9
|
-
email: string;
|
|
10
|
-
workspaceId: string;
|
|
11
|
-
role: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface TokenUser {
|
|
15
|
-
userId: string;
|
|
16
|
-
email: string;
|
|
17
|
-
workspaceId: string;
|
|
18
|
-
role: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
@Injectable()
|
|
22
|
-
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
23
|
-
constructor(configService: ConfigService) {
|
|
24
|
-
super({
|
|
25
|
-
jwtFromRequest: ExtractJwt.fromExtractors([
|
|
26
|
-
(req: Request): string | null =>
|
|
27
|
-
(req?.cookies as Record<string, string>)?.access_token ?? null,
|
|
28
|
-
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
29
|
-
]),
|
|
30
|
-
ignoreExpiration: false,
|
|
31
|
-
secretOrKey: configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
4
|
+
import { Request } from 'express';
|
|
5
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
6
|
+
|
|
7
|
+
export interface JwtPayload {
|
|
8
|
+
sub: string;
|
|
9
|
+
email: string;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
role: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TokenUser {
|
|
15
|
+
userId: string;
|
|
16
|
+
email: string;
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
role: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
23
|
+
constructor(configService: ConfigService) {
|
|
24
|
+
super({
|
|
25
|
+
jwtFromRequest: ExtractJwt.fromExtractors([
|
|
26
|
+
(req: Request): string | null =>
|
|
27
|
+
(req?.cookies as Record<string, string>)?.access_token ?? null,
|
|
28
|
+
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
29
|
+
]),
|
|
30
|
+
ignoreExpiration: false,
|
|
31
|
+
secretOrKey: configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
|
|
32
|
+
passReqToCallback: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
validate(req: Request, payload: JwtPayload): TokenUser {
|
|
37
|
+
(req as Record<string, unknown>).workspaceId = payload.workspaceId;
|
|
38
|
+
return {
|
|
39
|
+
userId: payload.sub,
|
|
40
|
+
email: payload.email,
|
|
41
|
+
workspaceId: payload.workspaceId,
|
|
42
|
+
role: payload.role,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Body, Controller, HttpCode, HttpStatus, Patch, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|
4
|
+
import type { TokenPayload } from '../../common/decorators/current-user.decorator';
|
|
5
|
+
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|
6
|
+
import { UsersService } from './users.service';
|
|
7
|
+
|
|
8
|
+
class UpdateUserDto {
|
|
9
|
+
name?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@ApiTags('Users')
|
|
14
|
+
@Controller('users')
|
|
15
|
+
export class UsersController {
|
|
16
|
+
constructor(private readonly usersService: UsersService) {}
|
|
17
|
+
|
|
18
|
+
@Patch('me')
|
|
19
|
+
@HttpCode(HttpStatus.OK)
|
|
20
|
+
@UseGuards(JwtAuthGuard)
|
|
21
|
+
@ApiOperation({ summary: 'Actualizar perfil del usuario autenticado' })
|
|
22
|
+
async updateMe(
|
|
23
|
+
@CurrentUser() user: TokenPayload,
|
|
24
|
+
@Body() dto: UpdateUserDto,
|
|
25
|
+
) {
|
|
26
|
+
return this.usersService.updateMe(user.userId, dto);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { MongooseModule } from '@nestjs/mongoose';
|
|
3
|
-
import { User, UserSchema } from './schemas/user.schema';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
3
|
+
import { User, UserSchema } from './schemas/user.schema';
|
|
4
|
+
import { UsersController } from './users.controller';
|
|
5
|
+
import { UsersRepository } from './users.repository';
|
|
6
|
+
import { UsersService } from './users.service';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [
|
|
10
|
+
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
|
|
11
|
+
],
|
|
12
|
+
controllers: [UsersController],
|
|
13
|
+
providers: [UsersService, UsersRepository],
|
|
14
|
+
exports: [UsersService],
|
|
15
|
+
})
|
|
16
|
+
export class UsersModule {}
|