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.
- package/LICENSE +21 -21
- package/README.md +338 -314
- package/dist/cli.js +19 -0
- package/dist/generator.js +166 -55
- package/package.json +52 -50
- package/templates/backend/.env.example +67 -67
- package/templates/backend/.prettierrc +4 -4
- package/templates/backend/README.md +249 -168
- package/templates/backend/eslint.config.mjs +35 -35
- package/templates/backend/nest-cli.json +8 -8
- package/templates/backend/package-lock.json +10979 -10979
- package/templates/backend/package.json +88 -88
- package/templates/backend/src/app.controller.spec.ts +24 -24
- package/templates/backend/src/app.controller.ts +15 -15
- package/templates/backend/src/app.module.ts +40 -40
- package/templates/backend/src/app.service.ts +11 -11
- package/templates/backend/src/common/base/base.repository.ts +221 -221
- package/templates/backend/src/common/base/base.schema.ts +24 -24
- package/templates/backend/src/common/decorators/cookie.decorator.ts +9 -9
- package/templates/backend/src/common/decorators/current-user.decorator.ts +20 -20
- package/templates/backend/src/common/decorators/workspace-id.decorator.ts +14 -14
- package/templates/backend/src/common/filters/global-exception.filter.ts +61 -61
- package/templates/backend/src/common/guards/jwt-auth.guard.ts +5 -5
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +45 -45
- package/templates/backend/src/main.ts +51 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +158 -158
- package/templates/backend/src/modules/auth/auth.module.ts +20 -20
- package/templates/backend/src/modules/auth/auth.service.ts +257 -257
- package/templates/backend/src/modules/auth/dto/forgot-password.dto.ts +9 -9
- package/templates/backend/src/modules/auth/dto/login.dto.ts +14 -14
- package/templates/backend/src/modules/auth/dto/refresh-token.dto.ts +12 -12
- package/templates/backend/src/modules/auth/dto/register.dto.ts +26 -26
- package/templates/backend/src/modules/auth/dto/reset-password.dto.ts +16 -16
- package/templates/backend/src/modules/auth/dto/verify-email.dto.ts +9 -9
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +43 -43
- package/templates/backend/src/modules/mail/mail.module.ts +9 -9
- package/templates/backend/src/modules/mail/mail.service.ts +141 -141
- package/templates/backend/src/modules/users/schemas/user.schema.ts +54 -54
- package/templates/backend/src/modules/users/users.module.ts +14 -14
- package/templates/backend/src/modules/users/users.repository.ts +51 -51
- package/templates/backend/src/modules/users/users.service.ts +104 -104
- package/templates/backend/src/modules/workspaces/schemas/workspace.schema.ts +26 -26
- package/templates/backend/src/modules/workspaces/workspaces.module.ts +16 -16
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +34 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +42 -42
- package/templates/backend/test/app.e2e-spec.ts +25 -25
- package/templates/backend/test/jest-e2e.json +9 -9
- package/templates/backend/tsconfig.build.json +4 -4
- package/templates/backend/tsconfig.json +26 -26
- package/templates/frontend/.env.local.example +1 -1
- package/templates/frontend/README.md +152 -0
- package/templates/frontend/components.json +20 -20
- package/templates/frontend/eslint.config.mjs +14 -14
- package/templates/frontend/next.config.ts +5 -5
- package/templates/frontend/package-lock.json +6722 -6722
- package/templates/frontend/package.json +48 -48
- package/templates/frontend/pnpm-lock.yaml +5012 -5012
- package/templates/frontend/pnpm-workspace.yaml +3 -3
- package/templates/frontend/postcss.config.mjs +7 -7
- package/templates/frontend/src/app/(auth)/forgot-password/page.tsx +84 -84
- package/templates/frontend/src/app/(auth)/layout.tsx +28 -28
- package/templates/frontend/src/app/(auth)/login/page.tsx +111 -111
- package/templates/frontend/src/app/(auth)/register/page.tsx +161 -161
- package/templates/frontend/src/app/(auth)/reset-password/page.tsx +120 -120
- package/templates/frontend/src/app/(auth)/verify-email/page.tsx +78 -78
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/dashboard/page.tsx +105 -105
- package/templates/frontend/src/app/(dashboard)/layout.tsx +38 -38
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +226 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +156 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +178 -178
- package/templates/frontend/src/app/(legal)/privacy/page.tsx +127 -127
- package/templates/frontend/src/app/(legal)/terms/page.tsx +118 -118
- package/templates/frontend/src/app/globals.css +81 -81
- package/templates/frontend/src/app/layout.tsx +26 -26
- package/templates/frontend/src/app/page.tsx +5 -45
- package/templates/frontend/src/app/setup/page.tsx +371 -275
- package/templates/frontend/src/components/dashboard/header.tsx +89 -89
- package/templates/frontend/src/components/dashboard/sidebar.tsx +71 -71
- package/templates/frontend/src/components/dashboard/stat-card.tsx +34 -34
- package/templates/frontend/src/components/landing/faq.tsx +39 -39
- package/templates/frontend/src/components/landing/features.tsx +54 -54
- package/templates/frontend/src/components/landing/footer.tsx +76 -76
- package/templates/frontend/src/components/landing/hero.tsx +72 -72
- package/templates/frontend/src/components/landing/navbar.tsx +78 -78
- package/templates/frontend/src/components/landing/pricing.tsx +90 -90
- package/templates/frontend/src/components/ui/accordion.tsx +52 -52
- package/templates/frontend/src/components/ui/avatar.tsx +46 -46
- package/templates/frontend/src/components/ui/badge.tsx +30 -30
- package/templates/frontend/src/components/ui/button.tsx +52 -52
- package/templates/frontend/src/components/ui/card.tsx +50 -50
- package/templates/frontend/src/components/ui/checkbox.tsx +27 -27
- package/templates/frontend/src/components/ui/dialog.tsx +100 -100
- package/templates/frontend/src/components/ui/dropdown-menu.tsx +173 -173
- package/templates/frontend/src/components/ui/form.tsx +158 -158
- package/templates/frontend/src/components/ui/input.tsx +21 -21
- package/templates/frontend/src/components/ui/label.tsx +22 -22
- package/templates/frontend/src/components/ui/separator.tsx +25 -25
- package/templates/frontend/src/components/ui/skeleton.tsx +7 -7
- package/templates/frontend/src/components/ui/switch.tsx +28 -28
- package/templates/frontend/src/components/ui/tabs.tsx +54 -54
- package/templates/frontend/src/components/ui/textarea.tsx +20 -20
- package/templates/frontend/src/components/ui/toast.tsx +109 -109
- package/templates/frontend/src/components/ui/toaster.tsx +30 -30
- package/templates/frontend/src/config/site.ts +197 -197
- package/templates/frontend/src/hooks/use-toast.ts +116 -116
- package/templates/frontend/src/lib/api/auth.ts +39 -39
- package/templates/frontend/src/lib/api/client.ts +66 -66
- package/templates/frontend/src/lib/hooks/use-auth.ts +1 -1
- package/templates/frontend/src/lib/utils.ts +6 -6
- package/templates/frontend/src/providers/auth-provider.tsx +60 -60
- package/templates/frontend/src/types/api.ts +12 -12
- package/templates/frontend/src/types/auth.ts +27 -27
- package/templates/frontend/tsconfig.json +23 -23
|
@@ -1,51 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,104 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,26 +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 });
|
|
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 });
|
|
@@ -1,16 +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 {}
|
|
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 {}
|
|
@@ -1,34 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,42 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,25 +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
|
-
});
|
|
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
|
+
});
|
|
@@ -1,9 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "./tsconfig.json",
|
|
3
|
-
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
|
4
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
|
4
|
+
}
|