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,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
+ }
@@ -0,0 +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);
@@ -0,0 +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 {}
@@ -0,0 +1,51 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { InjectModel } from '@nestjs/mongoose';
3
+ import { Model } from 'mongoose';
4
+ import { User, UserDocument } from './schemas/user.schema';
5
+
6
+ @Injectable()
7
+ export class UsersRepository {
8
+ constructor(
9
+ @InjectModel(User.name) private readonly userModel: Model<UserDocument>,
10
+ ) {}
11
+
12
+ async findByEmail(email: string): Promise<UserDocument | null> {
13
+ return this.userModel
14
+ .findOne({ email })
15
+ .select('+password +refreshToken')
16
+ .exec();
17
+ }
18
+
19
+ async findById(id: string): Promise<UserDocument | null> {
20
+ return this.userModel.findById(id).exec();
21
+ }
22
+
23
+ async findByIdWithSecrets(id: string): Promise<UserDocument | null> {
24
+ return this.userModel.findById(id).select('+refreshToken').exec();
25
+ }
26
+
27
+ async findByVerificationToken(token: string): Promise<UserDocument | null> {
28
+ return this.userModel
29
+ .findOne({ emailVerificationToken: token })
30
+ .select('+emailVerificationToken')
31
+ .exec();
32
+ }
33
+
34
+ async findByPasswordResetToken(token: string): Promise<UserDocument | null> {
35
+ return this.userModel
36
+ .findOne({ passwordResetToken: token })
37
+ .select('+passwordResetToken')
38
+ .exec();
39
+ }
40
+
41
+ async create(data: Partial<User>): Promise<UserDocument> {
42
+ const user = new this.userModel(data);
43
+ return user.save();
44
+ }
45
+
46
+ async update(id: string, data: Partial<User>): Promise<UserDocument | null> {
47
+ return this.userModel
48
+ .findByIdAndUpdate(id, { $set: data }, { returnDocument: 'after' })
49
+ .exec();
50
+ }
51
+ }
@@ -0,0 +1,104 @@
1
+ import { ConflictException, Injectable } from '@nestjs/common';
2
+ import * as bcrypt from 'bcryptjs';
3
+ import { User, UserDocument, UserRole } from './schemas/user.schema';
4
+ import { UsersRepository } from './users.repository';
5
+
6
+ @Injectable()
7
+ export class UsersService {
8
+ constructor(private readonly usersRepository: UsersRepository) {}
9
+
10
+ async create(data: {
11
+ name: string;
12
+ email: string;
13
+ password: string;
14
+ workspaceId: string;
15
+ role?: UserRole;
16
+ }): Promise<UserDocument> {
17
+ const existing = await this.usersRepository.findByEmail(data.email);
18
+ if (existing) {
19
+ throw new ConflictException('Este email ya está registrado.');
20
+ }
21
+ const hashedPassword = await bcrypt.hash(data.password, 12);
22
+ return this.usersRepository.create({ ...data, password: hashedPassword });
23
+ }
24
+
25
+ async findByEmail(email: string): Promise<UserDocument | null> {
26
+ return this.usersRepository.findByEmail(email);
27
+ }
28
+
29
+ async findById(id: string): Promise<UserDocument | null> {
30
+ return this.usersRepository.findById(id);
31
+ }
32
+
33
+ async updateRefreshToken(
34
+ userId: string,
35
+ refreshToken: string | null,
36
+ ): Promise<void> {
37
+ const hashed = refreshToken
38
+ ? await bcrypt.hash(refreshToken, 10)
39
+ : null;
40
+ await this.usersRepository.update(userId, { refreshToken: hashed });
41
+ }
42
+
43
+ async validateRefreshToken(
44
+ userId: string,
45
+ token: string,
46
+ ): Promise<UserDocument | null> {
47
+ const user = await this.usersRepository.findByIdWithSecrets(userId);
48
+ if (!user?.refreshToken) return null;
49
+ const valid = await bcrypt.compare(token, user.refreshToken);
50
+ return valid ? user : null;
51
+ }
52
+
53
+ async setEmailVerificationToken(
54
+ userId: string,
55
+ token: string,
56
+ expiresAt: Date,
57
+ ): Promise<void> {
58
+ await this.usersRepository.update(userId, {
59
+ emailVerificationToken: token,
60
+ emailVerificationTokenExpiresAt: expiresAt,
61
+ });
62
+ }
63
+
64
+ async findByVerificationToken(token: string): Promise<UserDocument | null> {
65
+ return this.usersRepository.findByVerificationToken(token);
66
+ }
67
+
68
+ async markEmailVerified(userId: string): Promise<void> {
69
+ await this.usersRepository.update(userId, {
70
+ emailVerified: true,
71
+ emailVerificationToken: null,
72
+ emailVerificationTokenExpiresAt: null,
73
+ });
74
+ }
75
+
76
+ async setPasswordResetToken(
77
+ userId: string,
78
+ token: string,
79
+ expiresAt: Date,
80
+ ): Promise<void> {
81
+ await this.usersRepository.update(userId, {
82
+ passwordResetToken: token,
83
+ passwordResetTokenExpiresAt: expiresAt,
84
+ });
85
+ }
86
+
87
+ async findByPasswordResetToken(token: string): Promise<UserDocument | null> {
88
+ return this.usersRepository.findByPasswordResetToken(token);
89
+ }
90
+
91
+ async updatePassword(userId: string, password: string): Promise<void> {
92
+ const hashed = await bcrypt.hash(password, 12);
93
+ await this.usersRepository.update(userId, {
94
+ password: hashed,
95
+ passwordResetToken: null,
96
+ passwordResetTokenExpiresAt: null,
97
+ refreshToken: null,
98
+ });
99
+ }
100
+
101
+ async updateLastLoginAt(userId: string): Promise<void> {
102
+ await this.usersRepository.update(userId, { lastLoginAt: new Date() });
103
+ }
104
+ }
@@ -0,0 +1,26 @@
1
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
+ import { HydratedDocument } from 'mongoose';
3
+
4
+ export type WorkspaceDocument = HydratedDocument<Workspace>;
5
+
6
+ @Schema({ collection: 'workspaces', timestamps: true })
7
+ export class Workspace {
8
+ @Prop({ required: true, trim: true })
9
+ name!: string;
10
+
11
+ @Prop({ required: true, unique: true, lowercase: true, trim: true })
12
+ slug!: string;
13
+
14
+ @Prop({ required: true })
15
+ ownerId!: string;
16
+
17
+ @Prop({ type: String, enum: ['active', 'suspended'], default: 'active' })
18
+ status!: 'active' | 'suspended';
19
+
20
+ createdAt!: Date;
21
+ updatedAt!: Date;
22
+ }
23
+
24
+ export const WorkspaceSchema = SchemaFactory.createForClass(Workspace);
25
+
26
+ WorkspaceSchema.index({ ownerId: 1 });
@@ -0,0 +1,16 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { MongooseModule } from '@nestjs/mongoose';
3
+ import { Workspace, WorkspaceSchema } from './schemas/workspace.schema';
4
+ import { WorkspacesRepository } from './workspaces.repository';
5
+ import { WorkspacesService } from './workspaces.service';
6
+
7
+ @Module({
8
+ imports: [
9
+ MongooseModule.forFeature([
10
+ { name: Workspace.name, schema: WorkspaceSchema },
11
+ ]),
12
+ ],
13
+ providers: [WorkspacesService, WorkspacesRepository],
14
+ exports: [WorkspacesService],
15
+ })
16
+ export class WorkspacesModule {}
@@ -0,0 +1,34 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { InjectModel } from '@nestjs/mongoose';
3
+ import { Model } from 'mongoose';
4
+ import { Workspace, WorkspaceDocument } from './schemas/workspace.schema';
5
+
6
+ @Injectable()
7
+ export class WorkspacesRepository {
8
+ constructor(
9
+ @InjectModel(Workspace.name)
10
+ private readonly workspaceModel: Model<WorkspaceDocument>,
11
+ ) {}
12
+
13
+ async findById(id: string): Promise<WorkspaceDocument | null> {
14
+ return this.workspaceModel.findById(id).exec();
15
+ }
16
+
17
+ async findBySlug(slug: string): Promise<WorkspaceDocument | null> {
18
+ return this.workspaceModel.findOne({ slug }).exec();
19
+ }
20
+
21
+ async create(data: Partial<Workspace>): Promise<WorkspaceDocument> {
22
+ const workspace = new this.workspaceModel(data);
23
+ return workspace.save();
24
+ }
25
+
26
+ async update(
27
+ id: string,
28
+ data: Partial<Workspace>,
29
+ ): Promise<WorkspaceDocument | null> {
30
+ return this.workspaceModel
31
+ .findByIdAndUpdate(id, { $set: data }, { returnDocument: 'after' })
32
+ .exec();
33
+ }
34
+ }
@@ -0,0 +1,42 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { Workspace, WorkspaceDocument } from './schemas/workspace.schema';
3
+ import { WorkspacesRepository } from './workspaces.repository';
4
+
5
+ @Injectable()
6
+ export class WorkspacesService {
7
+ constructor(
8
+ private readonly workspacesRepository: WorkspacesRepository,
9
+ ) {}
10
+
11
+ async create(ownerId: string, name: string): Promise<WorkspaceDocument> {
12
+ const slug = await this.generateUniqueSlug(name);
13
+ return this.workspacesRepository.create({ name, slug, ownerId });
14
+ }
15
+
16
+ async update(
17
+ id: string,
18
+ data: Partial<Workspace>,
19
+ ): Promise<WorkspaceDocument | null> {
20
+ return this.workspacesRepository.update(id, data);
21
+ }
22
+
23
+ async findById(id: string): Promise<WorkspaceDocument | null> {
24
+ return this.workspacesRepository.findById(id);
25
+ }
26
+
27
+ private async generateUniqueSlug(name: string): Promise<string> {
28
+ const base = name
29
+ .toLowerCase()
30
+ .normalize('NFD')
31
+ .replace(/\p{M}/gu, '')
32
+ .replace(/[^a-z0-9]+/g, '-')
33
+ .replace(/^-+|-+$/g, '');
34
+
35
+ let slug = base || 'workspace';
36
+ let counter = 1;
37
+ while (await this.workspacesRepository.findBySlug(slug)) {
38
+ slug = `${base}-${counter++}`;
39
+ }
40
+ return slug;
41
+ }
42
+ }
@@ -0,0 +1,25 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { INestApplication } from '@nestjs/common';
3
+ import request from 'supertest';
4
+ import { App } from 'supertest/types';
5
+ import { AppModule } from './../src/app.module';
6
+
7
+ describe('AppController (e2e)', () => {
8
+ let app: INestApplication<App>;
9
+
10
+ beforeEach(async () => {
11
+ const moduleFixture: TestingModule = await Test.createTestingModule({
12
+ imports: [AppModule],
13
+ }).compile();
14
+
15
+ app = moduleFixture.createNestApplication();
16
+ await app.init();
17
+ });
18
+
19
+ it('/ (GET)', () => {
20
+ return request(app.getHttpServer())
21
+ .get('/')
22
+ .expect(200)
23
+ .expect('Hello World!');
24
+ });
25
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "moduleFileExtensions": ["js", "json", "ts"],
3
+ "rootDir": ".",
4
+ "testEnvironment": "node",
5
+ "testRegex": ".e2e-spec.ts$",
6
+ "transform": {
7
+ "^.+\\.(t|j)s$": "ts-jest"
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "nodenext",
4
+ "moduleResolution": "nodenext",
5
+ "resolvePackageJsonExports": true,
6
+ "esModuleInterop": true,
7
+ "isolatedModules": true,
8
+ "declaration": true,
9
+ "removeComments": true,
10
+ "emitDecoratorMetadata": true,
11
+ "experimentalDecorators": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "target": "ES2023",
14
+ "sourceMap": true,
15
+ "outDir": "./dist",
16
+ "baseUrl": "./",
17
+ "incremental": true,
18
+ "skipLibCheck": true,
19
+ "strict": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "noImplicitAny": true,
22
+ "strictNullChecks": true,
23
+ "strictBindCallApply": true,
24
+ "noFallthroughCasesInSwitch": true
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ NEXT_PUBLIC_API_URL=http://localhost:3000
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -0,0 +1,14 @@
1
+ import { dirname } from 'path'
2
+ import { fileURLToPath } from 'url'
3
+ import { FlatCompat } from '@eslint/eslintrc'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = dirname(__filename)
7
+
8
+ const compat = new FlatCompat({ baseDirectory: __dirname })
9
+
10
+ const eslintConfig = [
11
+ ...compat.extends('next/core-web-vitals', 'next/typescript'),
12
+ ]
13
+
14
+ export default eslintConfig
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from 'next'
2
+
3
+ const nextConfig: NextConfig = {}
4
+
5
+ export default nextConfig