ar-saas 0.3.1 → 0.3.2

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 (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +338 -314
  3. package/dist/cli.js +19 -0
  4. package/dist/generator.js +166 -55
  5. package/package.json +52 -50
  6. package/templates/backend/.env.example +67 -67
  7. package/templates/backend/.prettierrc +4 -4
  8. package/templates/backend/README.md +249 -168
  9. package/templates/backend/eslint.config.mjs +35 -35
  10. package/templates/backend/nest-cli.json +8 -8
  11. package/templates/backend/package-lock.json +10979 -10979
  12. package/templates/backend/package.json +88 -88
  13. package/templates/backend/src/app.controller.spec.ts +24 -24
  14. package/templates/backend/src/app.controller.ts +15 -15
  15. package/templates/backend/src/app.module.ts +40 -40
  16. package/templates/backend/src/app.service.ts +11 -11
  17. package/templates/backend/src/common/base/base.repository.ts +221 -221
  18. package/templates/backend/src/common/base/base.schema.ts +24 -24
  19. package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
  20. package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
  21. package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
  22. package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
  23. package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
  24. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
  25. package/templates/backend/src/main.ts +51 -51
  26. package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
  27. package/templates/backend/src/modules/auth/auth.module.ts +20 -20
  28. package/templates/backend/src/modules/auth/auth.service.ts +257 -257
  29. package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
  30. package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
  31. package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
  32. package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
  33. package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
  34. package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
  35. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
  36. package/templates/backend/src/modules/mail/mail.module.ts +9 -9
  37. package/templates/backend/src/modules/mail/mail.service.ts +141 -141
  38. package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
  39. package/templates/backend/src/modules/users/users.module.ts +14 -14
  40. package/templates/backend/src/modules/users/users.repository.ts +51 -51
  41. package/templates/backend/src/modules/users/users.service.ts +104 -104
  42. package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
  43. package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
  44. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
  45. package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
  46. package/templates/backend/test/app.e2e-spec.ts +25 -25
  47. package/templates/backend/test/jest-e2e.json +9 -9
  48. package/templates/backend/tsconfig.build.json +4 -4
  49. package/templates/backend/tsconfig.json +26 -26
  50. package/templates/frontend/.env.local.example +1 -1
  51. package/templates/frontend/README.md +152 -0
  52. package/templates/frontend/components.json +20 -20
  53. package/templates/frontend/eslint.config.mjs +14 -14
  54. package/templates/frontend/next.config.ts +5 -5
  55. package/templates/frontend/package-lock.json +6722 -6722
  56. package/templates/frontend/package.json +48 -48
  57. package/templates/frontend/pnpm-lock.yaml +5012 -5012
  58. package/templates/frontend/pnpm-workspace.yaml +3 -3
  59. package/templates/frontend/postcss.config.mjs +7 -7
  60. package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
  61. package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
  62. package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
  63. package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
  64. package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
  65. package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
  66. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  67. package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
  68. package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
  69. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
  70. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
  71. package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
  72. package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
  73. package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
  74. package/templates/frontend/src/app/globals.css +81 -81
  75. package/templates/frontend/src/app/layout.tsx +26 -26
  76. package/templates/frontend/src/app/page.tsx +5 -45
  77. package/templates/frontend/src/app/setup/page.tsx +371 -275
  78. package/templates/frontend/src/components/dashboard/header.tsx +89 -89
  79. package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
  80. package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
  81. package/templates/frontend/src/components/landing/faq.tsx +39 -39
  82. package/templates/frontend/src/components/landing/features.tsx +54 -54
  83. package/templates/frontend/src/components/landing/footer.tsx +76 -76
  84. package/templates/frontend/src/components/landing/hero.tsx +72 -72
  85. package/templates/frontend/src/components/landing/navbar.tsx +78 -78
  86. package/templates/frontend/src/components/landing/pricing.tsx +90 -90
  87. package/templates/frontend/src/components/ui/accordion.tsx +52 -52
  88. package/templates/frontend/src/components/ui/avatar.tsx +46 -46
  89. package/templates/frontend/src/components/ui/badge.tsx +30 -30
  90. package/templates/frontend/src/components/ui/button.tsx +52 -52
  91. package/templates/frontend/src/components/ui/card.tsx +50 -50
  92. package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
  93. package/templates/frontend/src/components/ui/dialog.tsx +100 -100
  94. package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
  95. package/templates/frontend/src/components/ui/form.tsx +158 -158
  96. package/templates/frontend/src/components/ui/input.tsx +21 -21
  97. package/templates/frontend/src/components/ui/label.tsx +22 -22
  98. package/templates/frontend/src/components/ui/separator.tsx +25 -25
  99. package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
  100. package/templates/frontend/src/components/ui/switch.tsx +28 -28
  101. package/templates/frontend/src/components/ui/tabs.tsx +54 -54
  102. package/templates/frontend/src/components/ui/textarea.tsx +20 -20
  103. package/templates/frontend/src/components/ui/toast.tsx +109 -109
  104. package/templates/frontend/src/components/ui/toaster.tsx +30 -30
  105. package/templates/frontend/src/config/site.ts +197 -197
  106. package/templates/frontend/src/hooks/use-toast.ts +116 -116
  107. package/templates/frontend/src/lib/api/auth.ts +39 -39
  108. package/templates/frontend/src/lib/api/client.ts +66 -66
  109. package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
  110. package/templates/frontend/src/lib/utils.ts +6 -6
  111. package/templates/frontend/src/providers/auth-provider.tsx +60 -60
  112. package/templates/frontend/src/types/api.ts +12 -12
  113. package/templates/frontend/src/types/auth.ts +27 -27
  114. package/templates/frontend/tsconfig.json +23 -23
@@ -1,26 +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
- }
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
+ }
@@ -1,16 +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
- }
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
+ }
@@ -1,9 +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
- }
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
+ }
@@ -1,43 +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
- }
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
+ }
@@ -1,9 +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 {}
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 {}
@@ -1,141 +1,141 @@
1
- import { Injectable, Logger } from '@nestjs/common';
2
- import { ConfigService } from '@nestjs/config';
3
- import { Resend } from 'resend';
4
-
5
- export interface SendMailOptions {
6
- to: string | string[];
7
- subject: string;
8
- html: string;
9
- from?: string;
10
- replyTo?: string;
11
- attachments?: Array<{
12
- filename: string;
13
- content: Buffer | string;
14
- contentType?: string;
15
- }>;
16
- }
17
-
18
- @Injectable()
19
- export class MailService {
20
- private readonly resend: Resend;
21
- private readonly logger = new Logger(MailService.name);
22
- private readonly fromEmail: string;
23
- private readonly fromName: string;
24
-
25
- constructor(private readonly configService: ConfigService) {
26
- this.resend = new Resend(
27
- this.configService.getOrThrow<string>('RESEND_API_KEY'),
28
- );
29
- this.fromName = this.configService.get<string>('RESEND_FROM_NAME', 'SaaS AR');
30
- this.fromEmail = this.configService.getOrThrow<string>('RESEND_FROM_EMAIL');
31
- }
32
-
33
- async send(options: SendMailOptions): Promise<{ id: string }> {
34
- try {
35
- const { data, error } = await this.resend.emails.send({
36
- from: options.from ?? `${this.fromName} <${this.fromEmail}>`,
37
- to: Array.isArray(options.to) ? options.to : [options.to],
38
- subject: options.subject,
39
- html: options.html,
40
- replyTo: options.replyTo,
41
- attachments: options.attachments?.map((att) => ({
42
- filename: att.filename,
43
- content: att.content,
44
- })),
45
- });
46
-
47
- if (error) {
48
- this.logger.error(`Error al enviar email a ${options.to}: ${error.message}`);
49
- return { id: '' };
50
- }
51
-
52
- this.logger.log(`Email enviado a ${options.to} [id=${data?.id}]`);
53
- return { id: data!.id };
54
- } catch (err) {
55
- this.logger.error(
56
- `Excepción al enviar email a ${options.to}`,
57
- err instanceof Error ? err.stack : String(err),
58
- );
59
- return { id: '' };
60
- }
61
- }
62
-
63
- async sendVerificationEmail(
64
- to: string,
65
- name: string,
66
- token: string,
67
- ): Promise<{ id: string }> {
68
- const appUrl = this.configService.getOrThrow<string>('APP_URL');
69
- const link = `${appUrl}/auth/verify-email?token=${token}`;
70
-
71
- return this.send({
72
- to,
73
- subject: 'Verificá tu email',
74
- html: `
75
- <!DOCTYPE html>
76
- <html>
77
- <body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
78
- <h2>Hola ${name},</h2>
79
- <p>Gracias por registrarte. Hacé click en el siguiente botón para verificar tu email:</p>
80
- <p style="margin:32px 0">
81
- <a href="${link}"
82
- style="background:#000;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:bold">
83
- Verificar email
84
- </a>
85
- </p>
86
- <p style="color:#666;font-size:14px">Este link expira en 24 horas.</p>
87
- <p style="color:#666;font-size:14px">Si no creaste esta cuenta, ignorá este mensaje.</p>
88
- </body>
89
- </html>
90
- `,
91
- });
92
- }
93
-
94
- async sendPasswordResetEmail(
95
- to: string,
96
- name: string,
97
- token: string,
98
- ): Promise<{ id: string }> {
99
- const appUrl = this.configService.getOrThrow<string>('APP_URL');
100
- const link = `${appUrl}/auth/reset-password?token=${token}`;
101
-
102
- return this.send({
103
- to,
104
- subject: 'Restablecé tu contraseña',
105
- html: `
106
- <!DOCTYPE html>
107
- <html>
108
- <body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
109
- <h2>Hola ${name},</h2>
110
- <p>Recibimos una solicitud para restablecer tu contraseña.</p>
111
- <p style="margin:32px 0">
112
- <a href="${link}"
113
- style="background:#000;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:bold">
114
- Restablecer contraseña
115
- </a>
116
- </p>
117
- <p style="color:#666;font-size:14px">Este link expira en 1 hora.</p>
118
- <p style="color:#666;font-size:14px">Si no solicitaste esto, ignorá este mensaje.</p>
119
- </body>
120
- </html>
121
- `,
122
- });
123
- }
124
-
125
- async sendWelcomeEmail(to: string, name: string): Promise<{ id: string }> {
126
- return this.send({
127
- to,
128
- subject: `¡Bienvenido/a a ${this.fromName}!`,
129
- html: `
130
- <!DOCTYPE html>
131
- <html>
132
- <body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
133
- <h2>¡Hola ${name}!</h2>
134
- <p>Tu cuenta fue verificada exitosamente. Ya podés iniciar sesión y empezar a usar la plataforma.</p>
135
- <p style="color:#666;font-size:14px">Si tenés alguna duda, respondé este email.</p>
136
- </body>
137
- </html>
138
- `,
139
- });
140
- }
141
- }
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { Resend } from 'resend';
4
+
5
+ export interface SendMailOptions {
6
+ to: string | string[];
7
+ subject: string;
8
+ html: string;
9
+ from?: string;
10
+ replyTo?: string;
11
+ attachments?: Array<{
12
+ filename: string;
13
+ content: Buffer | string;
14
+ contentType?: string;
15
+ }>;
16
+ }
17
+
18
+ @Injectable()
19
+ export class MailService {
20
+ private readonly resend: Resend;
21
+ private readonly logger = new Logger(MailService.name);
22
+ private readonly fromEmail: string;
23
+ private readonly fromName: string;
24
+
25
+ constructor(private readonly configService: ConfigService) {
26
+ this.resend = new Resend(
27
+ this.configService.getOrThrow<string>('RESEND_API_KEY'),
28
+ );
29
+ this.fromName = this.configService.get<string>('RESEND_FROM_NAME', 'SaaS AR');
30
+ this.fromEmail = this.configService.getOrThrow<string>('RESEND_FROM_EMAIL');
31
+ }
32
+
33
+ async send(options: SendMailOptions): Promise<{ id: string }> {
34
+ try {
35
+ const { data, error } = await this.resend.emails.send({
36
+ from: options.from ?? `${this.fromName} <${this.fromEmail}>`,
37
+ to: Array.isArray(options.to) ? options.to : [options.to],
38
+ subject: options.subject,
39
+ html: options.html,
40
+ replyTo: options.replyTo,
41
+ attachments: options.attachments?.map((att) => ({
42
+ filename: att.filename,
43
+ content: att.content,
44
+ })),
45
+ });
46
+
47
+ if (error) {
48
+ this.logger.error(`Error al enviar email a ${options.to}: ${error.message}`);
49
+ return { id: '' };
50
+ }
51
+
52
+ this.logger.log(`Email enviado a ${options.to} [id=${data?.id}]`);
53
+ return { id: data!.id };
54
+ } catch (err) {
55
+ this.logger.error(
56
+ `Excepción al enviar email a ${options.to}`,
57
+ err instanceof Error ? err.stack : String(err),
58
+ );
59
+ return { id: '' };
60
+ }
61
+ }
62
+
63
+ async sendVerificationEmail(
64
+ to: string,
65
+ name: string,
66
+ token: string,
67
+ ): Promise<{ id: string }> {
68
+ const appUrl = this.configService.getOrThrow<string>('APP_URL');
69
+ const link = `${appUrl}/auth/verify-email?token=${token}`;
70
+
71
+ return this.send({
72
+ to,
73
+ subject: 'Verificá tu email',
74
+ html: `
75
+ <!DOCTYPE html>
76
+ <html>
77
+ <body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
78
+ <h2>Hola ${name},</h2>
79
+ <p>Gracias por registrarte. Hacé click en el siguiente botón para verificar tu email:</p>
80
+ <p style="margin:32px 0">
81
+ <a href="${link}"
82
+ style="background:#000;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:bold">
83
+ Verificar email
84
+ </a>
85
+ </p>
86
+ <p style="color:#666;font-size:14px">Este link expira en 24 horas.</p>
87
+ <p style="color:#666;font-size:14px">Si no creaste esta cuenta, ignorá este mensaje.</p>
88
+ </body>
89
+ </html>
90
+ `,
91
+ });
92
+ }
93
+
94
+ async sendPasswordResetEmail(
95
+ to: string,
96
+ name: string,
97
+ token: string,
98
+ ): Promise<{ id: string }> {
99
+ const appUrl = this.configService.getOrThrow<string>('APP_URL');
100
+ const link = `${appUrl}/auth/reset-password?token=${token}`;
101
+
102
+ return this.send({
103
+ to,
104
+ subject: 'Restablecé tu contraseña',
105
+ html: `
106
+ <!DOCTYPE html>
107
+ <html>
108
+ <body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
109
+ <h2>Hola ${name},</h2>
110
+ <p>Recibimos una solicitud para restablecer tu contraseña.</p>
111
+ <p style="margin:32px 0">
112
+ <a href="${link}"
113
+ style="background:#000;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;font-weight:bold">
114
+ Restablecer contraseña
115
+ </a>
116
+ </p>
117
+ <p style="color:#666;font-size:14px">Este link expira en 1 hora.</p>
118
+ <p style="color:#666;font-size:14px">Si no solicitaste esto, ignorá este mensaje.</p>
119
+ </body>
120
+ </html>
121
+ `,
122
+ });
123
+ }
124
+
125
+ async sendWelcomeEmail(to: string, name: string): Promise<{ id: string }> {
126
+ return this.send({
127
+ to,
128
+ subject: `¡Bienvenido/a a ${this.fromName}!`,
129
+ html: `
130
+ <!DOCTYPE html>
131
+ <html>
132
+ <body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:24px">
133
+ <h2>¡Hola ${name}!</h2>
134
+ <p>Tu cuenta fue verificada exitosamente. Ya podés iniciar sesión y empezar a usar la plataforma.</p>
135
+ <p style="color:#666;font-size:14px">Si tenés alguna duda, respondé este email.</p>
136
+ </body>
137
+ </html>
138
+ `,
139
+ });
140
+ }
141
+ }
@@ -1,54 +1,54 @@
1
- import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
- import { HydratedDocument } from 'mongoose';
3
-
4
- export type UserDocument = HydratedDocument<User>;
5
-
6
- export enum UserRole {
7
- OWNER = 'owner',
8
- ADMIN = 'admin',
9
- MEMBER = 'member',
10
- }
11
-
12
- @Schema({ collection: 'users', timestamps: true })
13
- export class User {
14
- @Prop({ required: true, trim: true })
15
- name!: string;
16
-
17
- @Prop({ required: true, unique: true, lowercase: true, trim: true })
18
- email!: string;
19
-
20
- @Prop({ type: String, default: null, select: false })
21
- password!: string | null;
22
-
23
- @Prop({ required: true })
24
- workspaceId!: string;
25
-
26
- @Prop({ default: false })
27
- emailVerified!: boolean;
28
-
29
- @Prop({ type: String, default: null, select: false })
30
- emailVerificationToken!: string | null;
31
-
32
- @Prop({ type: Date, default: null })
33
- emailVerificationTokenExpiresAt!: Date | null;
34
-
35
- @Prop({ type: String, default: null, select: false })
36
- passwordResetToken!: string | null;
37
-
38
- @Prop({ type: Date, default: null })
39
- passwordResetTokenExpiresAt!: Date | null;
40
-
41
- @Prop({ type: String, default: null, select: false })
42
- refreshToken!: string | null;
43
-
44
- @Prop({ type: String, enum: Object.values(UserRole), default: UserRole.OWNER })
45
- role!: UserRole;
46
-
47
- @Prop({ type: Date, default: null })
48
- lastLoginAt!: Date | null;
49
-
50
- createdAt!: Date;
51
- updatedAt!: Date;
52
- }
53
-
54
- export const UserSchema = SchemaFactory.createForClass(User);
1
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
+ import { HydratedDocument } from 'mongoose';
3
+
4
+ export type UserDocument = HydratedDocument<User>;
5
+
6
+ export enum UserRole {
7
+ OWNER = 'owner',
8
+ ADMIN = 'admin',
9
+ MEMBER = 'member',
10
+ }
11
+
12
+ @Schema({ collection: 'users', timestamps: true })
13
+ export class User {
14
+ @Prop({ required: true, trim: true })
15
+ name!: string;
16
+
17
+ @Prop({ required: true, unique: true, lowercase: true, trim: true })
18
+ email!: string;
19
+
20
+ @Prop({ type: String, default: null, select: false })
21
+ password!: string | null;
22
+
23
+ @Prop({ required: true })
24
+ workspaceId!: string;
25
+
26
+ @Prop({ default: false })
27
+ emailVerified!: boolean;
28
+
29
+ @Prop({ type: String, default: null, select: false })
30
+ emailVerificationToken!: string | null;
31
+
32
+ @Prop({ type: Date, default: null })
33
+ emailVerificationTokenExpiresAt!: Date | null;
34
+
35
+ @Prop({ type: String, default: null, select: false })
36
+ passwordResetToken!: string | null;
37
+
38
+ @Prop({ type: Date, default: null })
39
+ passwordResetTokenExpiresAt!: Date | null;
40
+
41
+ @Prop({ type: String, default: null, select: false })
42
+ refreshToken!: string | null;
43
+
44
+ @Prop({ type: String, enum: Object.values(UserRole), default: UserRole.OWNER })
45
+ role!: UserRole;
46
+
47
+ @Prop({ type: Date, default: null })
48
+ lastLoginAt!: Date | null;
49
+
50
+ createdAt!: Date;
51
+ updatedAt!: Date;
52
+ }
53
+
54
+ export const UserSchema = SchemaFactory.createForClass(User);
@@ -1,14 +1,14 @@
1
- import { Module } from '@nestjs/common';
2
- import { MongooseModule } from '@nestjs/mongoose';
3
- import { User, UserSchema } from './schemas/user.schema';
4
- import { UsersRepository } from './users.repository';
5
- import { UsersService } from './users.service';
6
-
7
- @Module({
8
- imports: [
9
- MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
10
- ],
11
- providers: [UsersService, UsersRepository],
12
- exports: [UsersService],
13
- })
14
- export class UsersModule {}
1
+ import { Module } from '@nestjs/common';
2
+ import { MongooseModule } from '@nestjs/mongoose';
3
+ import { User, UserSchema } from './schemas/user.schema';
4
+ import { UsersRepository } from './users.repository';
5
+ import { UsersService } from './users.service';
6
+
7
+ @Module({
8
+ imports: [
9
+ MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
10
+ ],
11
+ providers: [UsersService, UsersRepository],
12
+ exports: [UsersService],
13
+ })
14
+ export class UsersModule {}