ar-saas 0.1.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.
Files changed (85) hide show
  1. package/README.md +62 -0
  2. package/dist/cli.js +67 -0
  3. package/dist/generator.js +242 -0
  4. package/dist/index.js +13 -0
  5. package/dist/license.js +71 -0
  6. package/package.json +46 -0
  7. package/templates/backend/.env.example +67 -0
  8. package/templates/backend/.prettierrc +4 -0
  9. package/templates/backend/README.md +168 -0
  10. package/templates/backend/eslint.config.mjs +35 -0
  11. package/templates/backend/nest-cli.json +8 -0
  12. package/templates/backend/package-lock.json +10979 -0
  13. package/templates/backend/package.json +88 -0
  14. package/templates/backend/src/app.controller.spec.ts +24 -0
  15. package/templates/backend/src/app.controller.ts +15 -0
  16. package/templates/backend/src/app.module.ts +40 -0
  17. package/templates/backend/src/app.service.ts +11 -0
  18. package/templates/backend/src/common/base/base.repository.ts +221 -0
  19. package/templates/backend/src/common/base/base.schema.ts +24 -0
  20. package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -0
  21. package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -0
  22. package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -0
  23. package/templates/backend/src/common/filters/global-exception.filter.ts +61 -0
  24. package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -0
  25. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -0
  26. package/templates/backend/src/main.ts +51 -0
  27. package/templates/backend/src/modules/auth/auth.controller.ts +158 -0
  28. package/templates/backend/src/modules/auth/auth.module.ts +20 -0
  29. package/templates/backend/src/modules/auth/auth.service.ts +257 -0
  30. package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -0
  31. package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -0
  32. package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -0
  33. package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -0
  34. package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -0
  35. package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -0
  36. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -0
  37. package/templates/backend/src/modules/mail/mail.module.ts +9 -0
  38. package/templates/backend/src/modules/mail/mail.service.ts +141 -0
  39. package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -0
  40. package/templates/backend/src/modules/users/users.module.ts +14 -0
  41. package/templates/backend/src/modules/users/users.repository.ts +51 -0
  42. package/templates/backend/src/modules/users/users.service.ts +104 -0
  43. package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -0
  44. package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -0
  45. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -0
  46. package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -0
  47. package/templates/backend/test/app.e2e-spec.ts +25 -0
  48. package/templates/backend/test/jest-e2e.json +9 -0
  49. package/templates/backend/tsconfig.build.json +4 -0
  50. package/templates/backend/tsconfig.json +26 -0
  51. package/templates/frontend/.env.local.example +1 -0
  52. package/templates/frontend/components.json +20 -0
  53. package/templates/frontend/eslint.config.mjs +14 -0
  54. package/templates/frontend/next.config.ts +5 -0
  55. package/templates/frontend/package-lock.json +6722 -0
  56. package/templates/frontend/package.json +40 -0
  57. package/templates/frontend/postcss.config.mjs +7 -0
  58. package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -0
  59. package/templates/frontend/src/app/(auth)/layout.tsx +28 -0
  60. package/templates/frontend/src/app/(auth)/login/page.tsx +111 -0
  61. package/templates/frontend/src/app/(auth)/register/page.tsx +119 -0
  62. package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -0
  63. package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -0
  64. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +36 -0
  65. package/templates/frontend/src/app/(dashboard)/layout.tsx +59 -0
  66. package/templates/frontend/src/app/globals.css +81 -0
  67. package/templates/frontend/src/app/layout.tsx +26 -0
  68. package/templates/frontend/src/app/page.tsx +5 -0
  69. package/templates/frontend/src/app/setup/page.tsx +278 -0
  70. package/templates/frontend/src/components/ui/button.tsx +52 -0
  71. package/templates/frontend/src/components/ui/card.tsx +50 -0
  72. package/templates/frontend/src/components/ui/form.tsx +158 -0
  73. package/templates/frontend/src/components/ui/input.tsx +21 -0
  74. package/templates/frontend/src/components/ui/label.tsx +22 -0
  75. package/templates/frontend/src/components/ui/toast.tsx +109 -0
  76. package/templates/frontend/src/components/ui/toaster.tsx +30 -0
  77. package/templates/frontend/src/hooks/use-toast.ts +116 -0
  78. package/templates/frontend/src/lib/api/auth.ts +39 -0
  79. package/templates/frontend/src/lib/api/client.ts +66 -0
  80. package/templates/frontend/src/lib/hooks/use-auth.ts +1 -0
  81. package/templates/frontend/src/lib/utils.ts +6 -0
  82. package/templates/frontend/src/providers/auth-provider.tsx +60 -0
  83. package/templates/frontend/src/types/api.ts +12 -0
  84. package/templates/frontend/src/types/auth.ts +27 -0
  85. package/templates/frontend/tsconfig.json +23 -0
@@ -0,0 +1,158 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Get,
5
+ HttpCode,
6
+ HttpStatus,
7
+ Post,
8
+ Query,
9
+ Res,
10
+ UnauthorizedException,
11
+ UseGuards,
12
+ } from '@nestjs/common';
13
+ import { ConfigService } from '@nestjs/config';
14
+ import { ApiOperation, ApiTags } from '@nestjs/swagger';
15
+ import type { Response } from 'express';
16
+ import { CurrentUser } from '../../common/decorators/current-user.decorator';
17
+ import type { TokenPayload } from '../../common/decorators/current-user.decorator';
18
+ import { Cookie } from '../../common/decorators/cookie.decorator';
19
+ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
20
+ import { AuthService } from './auth.service';
21
+ import { ForgotPasswordDto } from './dto/forgot-password.dto';
22
+ import { LoginDto } from './dto/login.dto';
23
+ import { RefreshTokenDto } from './dto/refresh-token.dto';
24
+ import { RegisterDto } from './dto/register.dto';
25
+ import { ResetPasswordDto } from './dto/reset-password.dto';
26
+ import { VerifyEmailDto } from './dto/verify-email.dto';
27
+
28
+ @ApiTags('Auth')
29
+ @Controller('auth')
30
+ export class AuthController {
31
+ private readonly refreshCookiePath: string;
32
+
33
+ constructor(
34
+ private readonly authService: AuthService,
35
+ private readonly configService: ConfigService,
36
+ ) {
37
+ const prefix = this.configService.get<string>('API_PREFIX', 'api');
38
+ this.refreshCookiePath = `/${prefix}/auth/refresh`;
39
+ }
40
+
41
+ @Post('register')
42
+ @HttpCode(HttpStatus.CREATED)
43
+ @ApiOperation({ summary: 'Registrar nuevo usuario y workspace' })
44
+ register(@Body() dto: RegisterDto) {
45
+ return this.authService.register(dto);
46
+ }
47
+
48
+ @Get('verify-email')
49
+ @ApiOperation({ summary: 'Verificar email con token' })
50
+ verifyEmail(@Query() dto: VerifyEmailDto) {
51
+ return this.authService.verifyEmail(dto.token);
52
+ }
53
+
54
+ @Post('login')
55
+ @HttpCode(HttpStatus.OK)
56
+ @ApiOperation({ summary: 'Iniciar sesión' })
57
+ async login(
58
+ @Body() dto: LoginDto,
59
+ @Res({ passthrough: true }) res: Response,
60
+ ) {
61
+ const { user, accessToken, refreshToken } =
62
+ await this.authService.login(dto);
63
+ this.setTokenCookies(res, accessToken, refreshToken);
64
+ return user;
65
+ }
66
+
67
+ @Post('refresh')
68
+ @HttpCode(HttpStatus.OK)
69
+ @ApiOperation({ summary: 'Renovar access token usando el refresh token' })
70
+ async refresh(
71
+ @Cookie('refresh_token') cookieToken: string | undefined,
72
+ @Body() dto: RefreshTokenDto,
73
+ @Res({ passthrough: true }) res: Response,
74
+ ) {
75
+ const token = cookieToken ?? dto.refreshToken;
76
+ if (!token) {
77
+ throw new UnauthorizedException('Refresh token no encontrado.');
78
+ }
79
+ const tokens = await this.authService.refresh(token);
80
+ this.setTokenCookies(res, tokens.accessToken, tokens.refreshToken);
81
+ return { message: 'Token refreshed' };
82
+ }
83
+
84
+ @Post('logout')
85
+ @HttpCode(HttpStatus.OK)
86
+ @UseGuards(JwtAuthGuard)
87
+ @ApiOperation({ summary: 'Cerrar sesión' })
88
+ async logout(
89
+ @CurrentUser() user: TokenPayload,
90
+ @Res({ passthrough: true }) res: Response,
91
+ ) {
92
+ await this.authService.logout(user.userId);
93
+ this.clearTokenCookies(res);
94
+ return { message: 'Logged out' };
95
+ }
96
+
97
+ @Post('forgot-password')
98
+ @HttpCode(HttpStatus.OK)
99
+ @ApiOperation({ summary: 'Solicitar restablecimiento de contraseña' })
100
+ forgotPassword(@Body() dto: ForgotPasswordDto) {
101
+ return this.authService.forgotPassword(dto);
102
+ }
103
+
104
+ @Post('reset-password')
105
+ @HttpCode(HttpStatus.OK)
106
+ @ApiOperation({ summary: 'Restablecer contraseña con token del email' })
107
+ resetPassword(@Body() dto: ResetPasswordDto) {
108
+ return this.authService.resetPassword(dto);
109
+ }
110
+
111
+ @Get('me')
112
+ @UseGuards(JwtAuthGuard)
113
+ @ApiOperation({ summary: 'Obtener perfil del usuario autenticado' })
114
+ getMe(@CurrentUser() user: TokenPayload) {
115
+ return this.authService.getMe(user.userId);
116
+ }
117
+
118
+ // ─── Private helpers ────────────────────────────────────────────────────────
119
+
120
+ private setTokenCookies(
121
+ res: Response,
122
+ accessToken: string,
123
+ refreshToken: string,
124
+ ): void {
125
+ const isProd = this.configService.get<string>('NODE_ENV') === 'production';
126
+ const base = {
127
+ httpOnly: true,
128
+ secure: isProd,
129
+ sameSite: 'lax' as const,
130
+ };
131
+
132
+ res.cookie('access_token', accessToken, {
133
+ ...base,
134
+ maxAge: 15 * 60 * 1000,
135
+ });
136
+
137
+ res.cookie('refresh_token', refreshToken, {
138
+ ...base,
139
+ path: this.refreshCookiePath,
140
+ maxAge: 7 * 24 * 60 * 60 * 1000,
141
+ });
142
+ }
143
+
144
+ private clearTokenCookies(res: Response): void {
145
+ const isProd = this.configService.get<string>('NODE_ENV') === 'production';
146
+ const base = {
147
+ httpOnly: true,
148
+ secure: isProd,
149
+ sameSite: 'lax' as const,
150
+ };
151
+
152
+ res.clearCookie('access_token', base);
153
+ res.clearCookie('refresh_token', {
154
+ ...base,
155
+ path: this.refreshCookiePath,
156
+ });
157
+ }
158
+ }
@@ -0,0 +1,20 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { PassportModule } from '@nestjs/passport';
4
+ import { UsersModule } from '../users/users.module';
5
+ import { WorkspacesModule } from '../workspaces/workspaces.module';
6
+ import { AuthController } from './auth.controller';
7
+ import { AuthService } from './auth.service';
8
+ import { JwtStrategy } from './strategies/jwt.strategy';
9
+
10
+ @Module({
11
+ imports: [
12
+ PassportModule,
13
+ JwtModule.register({}),
14
+ UsersModule,
15
+ WorkspacesModule,
16
+ ],
17
+ controllers: [AuthController],
18
+ providers: [AuthService, JwtStrategy],
19
+ })
20
+ export class AuthModule {}
@@ -0,0 +1,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
+ // ─── Register ──────────────────────────────────────────────────────────────
37
+
38
+ async register(dto: RegisterDto): Promise<{ message: string }> {
39
+ // 1. Create workspace with placeholder ownerId (user doesn't exist yet)
40
+ const workspace = await this.workspacesService.create('pending', dto.name);
41
+ const workspaceId = String(workspace._id);
42
+
43
+ // 2. Create user linked to the workspace
44
+ const user = await this.usersService.create({
45
+ name: dto.name,
46
+ email: dto.email,
47
+ password: dto.password,
48
+ workspaceId,
49
+ });
50
+ const userId = String(user._id);
51
+
52
+ // 3. Set the real ownerId on the workspace
53
+ await this.workspacesService.update(workspaceId, { ownerId: userId });
54
+
55
+ // 4. Generate email verification token (expires in 24h)
56
+ const token = this.generateSecureToken();
57
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
58
+ await this.usersService.setEmailVerificationToken(userId, token, expiresAt);
59
+
60
+ // 5. Send verification email
61
+ await this.mailService.sendVerificationEmail(dto.email, dto.name, token);
62
+
63
+ return { message: 'Verification email sent' };
64
+ }
65
+
66
+ // ─── Verify Email ───────────────────────────────────────────────────────────
67
+
68
+ async verifyEmail(token: string): Promise<{ message: string }> {
69
+ const user = await this.usersService.findByVerificationToken(token);
70
+ if (!user) {
71
+ throw new BadRequestException('Token inválido o expirado.');
72
+ }
73
+ if (
74
+ user.emailVerificationTokenExpiresAt &&
75
+ user.emailVerificationTokenExpiresAt < new Date()
76
+ ) {
77
+ throw new BadRequestException(
78
+ 'El enlace de verificación expiró. Solicitá uno nuevo.',
79
+ );
80
+ }
81
+ await this.usersService.markEmailVerified(String(user._id));
82
+ return { message: 'Email verified' };
83
+ }
84
+
85
+ // ─── Login ──────────────────────────────────────────────────────────────────
86
+
87
+ async login(
88
+ dto: LoginDto,
89
+ ): Promise<{ user: UserDocument } & TokenPair> {
90
+ const user = await this.usersService.findByEmail(dto.email);
91
+ if (!user?.password) {
92
+ throw new UnauthorizedException('Email o contraseña incorrectos.');
93
+ }
94
+
95
+ const valid = await bcrypt.compare(dto.password, user.password);
96
+ if (!valid) {
97
+ throw new UnauthorizedException('Email o contraseña incorrectos.');
98
+ }
99
+
100
+ if (!user.emailVerified) {
101
+ throw new UnauthorizedException(
102
+ 'Por favor verificá tu email antes de ingresar.',
103
+ );
104
+ }
105
+
106
+ const userId = String(user._id);
107
+ const tokens = await this.generateTokens(
108
+ userId,
109
+ user.email,
110
+ user.workspaceId,
111
+ user.role,
112
+ );
113
+
114
+ await Promise.all([
115
+ this.usersService.updateRefreshToken(userId, tokens.refreshToken),
116
+ this.usersService.updateLastLoginAt(userId),
117
+ ]);
118
+
119
+ // Re-fetch without secrets for the response
120
+ const cleanUser = await this.usersService.findById(userId);
121
+ if (!cleanUser) {
122
+ throw new InternalServerErrorException('Error al obtener el usuario.');
123
+ }
124
+
125
+ return { user: cleanUser, ...tokens };
126
+ }
127
+
128
+ // ─── Refresh Token ──────────────────────────────────────────────────────────
129
+
130
+ async refresh(token: string): Promise<TokenPair> {
131
+ let payload: { sub: string };
132
+ try {
133
+ payload = await this.jwtService.verifyAsync<{ sub: string }>(token, {
134
+ secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
135
+ });
136
+ } catch {
137
+ throw new UnauthorizedException('Refresh token inválido.');
138
+ }
139
+
140
+ const user = await this.usersService.validateRefreshToken(
141
+ payload.sub,
142
+ token,
143
+ );
144
+ if (!user) {
145
+ throw new UnauthorizedException('Refresh token inválido.');
146
+ }
147
+
148
+ const userId = String(user._id);
149
+ const tokens = await this.generateTokens(
150
+ userId,
151
+ user.email,
152
+ user.workspaceId,
153
+ user.role,
154
+ );
155
+
156
+ await this.usersService.updateRefreshToken(userId, tokens.refreshToken);
157
+ return tokens;
158
+ }
159
+
160
+ // ─── Logout ─────────────────────────────────────────────────────────────────
161
+
162
+ async logout(userId: string): Promise<void> {
163
+ await this.usersService.updateRefreshToken(userId, null);
164
+ }
165
+
166
+ // ─── Forgot Password ────────────────────────────────────────────────────────
167
+
168
+ async forgotPassword(
169
+ dto: ForgotPasswordDto,
170
+ ): Promise<{ message: string }> {
171
+ const user = await this.usersService.findByEmail(dto.email);
172
+
173
+ // Always return the same response to avoid leaking email existence
174
+ if (user?.emailVerified) {
175
+ const token = this.generateSecureToken();
176
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000);
177
+ await this.usersService.setPasswordResetToken(
178
+ String(user._id),
179
+ token,
180
+ expiresAt,
181
+ );
182
+ await this.mailService.sendPasswordResetEmail(
183
+ dto.email,
184
+ user.name,
185
+ token,
186
+ );
187
+ }
188
+
189
+ return { message: 'Reset email sent' };
190
+ }
191
+
192
+ // ─── Reset Password ─────────────────────────────────────────────────────────
193
+
194
+ async resetPassword(dto: ResetPasswordDto): Promise<{ message: string }> {
195
+ const user = await this.usersService.findByPasswordResetToken(dto.token);
196
+ if (!user) {
197
+ throw new BadRequestException('Token inválido o expirado.');
198
+ }
199
+ if (
200
+ user.passwordResetTokenExpiresAt &&
201
+ user.passwordResetTokenExpiresAt < new Date()
202
+ ) {
203
+ throw new BadRequestException(
204
+ 'El enlace de restablecimiento expiró. Solicitá uno nuevo.',
205
+ );
206
+ }
207
+
208
+ await this.usersService.updatePassword(String(user._id), dto.newPassword);
209
+ return { message: 'Password reset successful' };
210
+ }
211
+
212
+ // ─── Get Me ─────────────────────────────────────────────────────────────────
213
+
214
+ async getMe(userId: string): Promise<UserDocument> {
215
+ const user = await this.usersService.findById(userId);
216
+ if (!user) {
217
+ throw new UnauthorizedException('Usuario no encontrado.');
218
+ }
219
+ return user;
220
+ }
221
+
222
+ // ─── Private helpers ────────────────────────────────────────────────────────
223
+
224
+ private generateSecureToken(): string {
225
+ return crypto.randomBytes(32).toString('hex');
226
+ }
227
+
228
+ private async generateTokens(
229
+ userId: string,
230
+ email: string,
231
+ workspaceId: string,
232
+ role: string,
233
+ ): Promise<TokenPair> {
234
+ const accessPayload = { sub: userId, email, workspaceId, role };
235
+ const refreshPayload = { sub: userId };
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
+ }
@@ -0,0 +1,9 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsEmail, IsNotEmpty } from 'class-validator';
3
+
4
+ export class ForgotPasswordDto {
5
+ @ApiProperty({ example: 'juan@example.com' })
6
+ @IsEmail({}, { message: 'El email no tiene un formato válido' })
7
+ @IsNotEmpty({ message: 'El email es obligatorio' })
8
+ email!: string;
9
+ }
@@ -0,0 +1,14 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
3
+
4
+ export class LoginDto {
5
+ @ApiProperty({ example: 'juan@example.com' })
6
+ @IsEmail({}, { message: 'El email no tiene un formato válido' })
7
+ @IsNotEmpty({ message: 'El email es obligatorio' })
8
+ email!: string;
9
+
10
+ @ApiProperty()
11
+ @IsString({ message: 'La contraseña debe ser texto' })
12
+ @IsNotEmpty({ message: 'La contraseña es obligatoria' })
13
+ password!: string;
14
+ }
@@ -0,0 +1,12 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsOptional, IsString } from 'class-validator';
3
+
4
+ export class RefreshTokenDto {
5
+ @ApiPropertyOptional({
6
+ description:
7
+ 'Refresh token (opcional — se lee de la cookie refresh_token si no se provee)',
8
+ })
9
+ @IsString({ message: 'El token debe ser texto' })
10
+ @IsOptional()
11
+ refreshToken?: string;
12
+ }
@@ -0,0 +1,26 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import {
3
+ IsEmail,
4
+ IsNotEmpty,
5
+ IsString,
6
+ MaxLength,
7
+ MinLength,
8
+ } from 'class-validator';
9
+
10
+ export class RegisterDto {
11
+ @ApiProperty({ example: 'Juan Pérez', maxLength: 80 })
12
+ @IsString({ message: 'El nombre debe ser texto' })
13
+ @IsNotEmpty({ message: 'El nombre es obligatorio' })
14
+ @MaxLength(80, { message: 'El nombre no puede superar 80 caracteres' })
15
+ name!: string;
16
+
17
+ @ApiProperty({ example: 'juan@example.com' })
18
+ @IsEmail({}, { message: 'El email no tiene un formato válido' })
19
+ @IsNotEmpty({ message: 'El email es obligatorio' })
20
+ email!: string;
21
+
22
+ @ApiProperty({ minLength: 8 })
23
+ @IsString({ message: 'La contraseña debe ser texto' })
24
+ @MinLength(8, { message: 'La contraseña debe tener al menos 8 caracteres' })
25
+ password!: string;
26
+ }
@@ -0,0 +1,16 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsNotEmpty, IsString, MinLength } from 'class-validator';
3
+
4
+ export class ResetPasswordDto {
5
+ @ApiProperty({ description: 'Token de reset recibido por email' })
6
+ @IsString({ message: 'El token debe ser texto' })
7
+ @IsNotEmpty({ message: 'El token es obligatorio' })
8
+ token!: string;
9
+
10
+ @ApiProperty({ minLength: 8 })
11
+ @IsString({ message: 'La nueva contraseña debe ser texto' })
12
+ @MinLength(8, {
13
+ message: 'La nueva contraseña debe tener al menos 8 caracteres',
14
+ })
15
+ newPassword!: string;
16
+ }
@@ -0,0 +1,9 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsNotEmpty, IsString } from 'class-validator';
3
+
4
+ export class VerifyEmailDto {
5
+ @ApiProperty({ description: 'Token de verificación recibido por email' })
6
+ @IsString({ message: 'El token debe ser texto' })
7
+ @IsNotEmpty({ message: 'El token es obligatorio' })
8
+ token!: string;
9
+ }
@@ -0,0 +1,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
+ });
33
+ }
34
+
35
+ validate(payload: JwtPayload): TokenUser {
36
+ return {
37
+ userId: payload.sub,
38
+ email: payload.email,
39
+ workspaceId: payload.workspaceId,
40
+ role: payload.role,
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,9 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { MailService } from './mail.service';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [MailService],
7
+ exports: [MailService],
8
+ })
9
+ export class MailModule {}