ar-saas 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator.js +2 -6
- package/package.json +1 -1
- package/templates/backend/.env.example +1 -1
- package/templates/backend/README.md +6 -6
- package/templates/backend/package.json +5 -2
- package/templates/backend/src/app.module.ts +68 -40
- package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +27 -45
- package/templates/backend/src/main.ts +50 -51
- package/templates/backend/src/modules/auth/auth.controller.ts +162 -158
- package/templates/backend/src/modules/auth/auth.service.ts +236 -257
- package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +45 -43
- package/templates/backend/src/modules/users/users.controller.ts +28 -0
- package/templates/backend/src/modules/users/users.module.ts +16 -14
- package/templates/backend/src/modules/users/users.repository.ts +57 -51
- package/templates/backend/src/modules/users/users.service.ts +130 -104
- package/templates/backend/src/modules/workspaces/workspaces.repository.ts +38 -34
- package/templates/backend/src/modules/workspaces/workspaces.service.ts +51 -42
- package/templates/frontend/README.md +2 -2
- package/templates/frontend/package.json +2 -6
- package/templates/frontend/pnpm-workspace.yaml +2 -2
- package/templates/frontend/src/app/(auth)/layout.tsx +29 -28
- package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
- package/templates/frontend/src/app/(dashboard)/profile/page.tsx +241 -226
- package/templates/frontend/src/app/(dashboard)/settings/page.tsx +155 -156
- package/templates/frontend/src/app/(dashboard)/team/page.tsx +179 -178
- package/templates/frontend/src/app/layout.tsx +29 -26
- package/templates/frontend/src/app/page.tsx +1 -1
- package/templates/frontend/src/app/setup/page.tsx +1 -1
- package/templates/frontend/src/components/dashboard/header.tsx +5 -3
- package/templates/frontend/src/config/site.ts +1 -1
|
@@ -1,51 +1,57 @@
|
|
|
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
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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: email.toLowerCase().trim() })
|
|
15
|
+
.exec();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findByEmailWithPassword(email: string): Promise<UserDocument | null> {
|
|
19
|
+
return this.userModel
|
|
20
|
+
.findOne({ email: email.toLowerCase().trim() })
|
|
21
|
+
.select('+password')
|
|
22
|
+
.exec();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async findById(id: string): Promise<UserDocument | null> {
|
|
26
|
+
return this.userModel.findById(id).exec();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findByIdWithSecrets(id: string): Promise<UserDocument | null> {
|
|
30
|
+
return this.userModel.findById(id).select('+refreshToken').exec();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async findByVerificationToken(tokenHash: string): Promise<UserDocument | null> {
|
|
34
|
+
return this.userModel
|
|
35
|
+
.findOne({ emailVerificationToken: tokenHash })
|
|
36
|
+
.select('+emailVerificationToken')
|
|
37
|
+
.exec();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async findByPasswordResetToken(tokenHash: string): Promise<UserDocument | null> {
|
|
41
|
+
return this.userModel
|
|
42
|
+
.findOne({ passwordResetToken: tokenHash })
|
|
43
|
+
.select('+passwordResetToken')
|
|
44
|
+
.exec();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async create(data: Partial<User>): Promise<UserDocument> {
|
|
48
|
+
const user = new this.userModel(data);
|
|
49
|
+
return user.save();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async update(id: string, data: Partial<User>): Promise<UserDocument | null> {
|
|
53
|
+
return this.userModel
|
|
54
|
+
.findByIdAndUpdate(id, { $set: data }, { returnDocument: 'after' })
|
|
55
|
+
.exec();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,104 +1,130 @@
|
|
|
1
|
-
import { ConflictException, Injectable } from '@nestjs/common';
|
|
2
|
-
import * as bcrypt from 'bcryptjs';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await this.usersRepository.update(userId, {
|
|
103
|
-
|
|
104
|
-
|
|
1
|
+
import { ConflictException, Injectable } from '@nestjs/common';
|
|
2
|
+
import * as bcrypt from 'bcryptjs';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import { User, UserDocument, UserRole } from './schemas/user.schema';
|
|
5
|
+
import { UsersRepository } from './users.repository';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class UsersService {
|
|
9
|
+
constructor(private readonly usersRepository: UsersRepository) {}
|
|
10
|
+
|
|
11
|
+
async create(data: {
|
|
12
|
+
name: string;
|
|
13
|
+
email: string;
|
|
14
|
+
password: string;
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
role?: UserRole;
|
|
17
|
+
}): Promise<UserDocument> {
|
|
18
|
+
const existing = await this.usersRepository.findByEmail(data.email);
|
|
19
|
+
if (existing) {
|
|
20
|
+
throw new ConflictException('Este email ya está registrado.');
|
|
21
|
+
}
|
|
22
|
+
const hashedPassword = await bcrypt.hash(data.password, 12);
|
|
23
|
+
return this.usersRepository.create({ ...data, password: hashedPassword });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async findByEmail(email: string): Promise<UserDocument | null> {
|
|
27
|
+
return this.usersRepository.findByEmail(email);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findByEmailWithPassword(email: string): Promise<UserDocument | null> {
|
|
31
|
+
return this.usersRepository.findByEmailWithPassword(email);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findById(id: string): Promise<UserDocument | null> {
|
|
35
|
+
return this.usersRepository.findById(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async updateRefreshToken(
|
|
39
|
+
userId: string,
|
|
40
|
+
refreshToken: string | null,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const hashed = refreshToken
|
|
43
|
+
? await bcrypt.hash(refreshToken, 10)
|
|
44
|
+
: null;
|
|
45
|
+
await this.usersRepository.update(userId, { refreshToken: hashed });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async validateRefreshToken(
|
|
49
|
+
userId: string,
|
|
50
|
+
token: string,
|
|
51
|
+
): Promise<UserDocument | null> {
|
|
52
|
+
const user = await this.usersRepository.findByIdWithSecrets(userId);
|
|
53
|
+
if (!user?.refreshToken) return null;
|
|
54
|
+
const valid = await bcrypt.compare(token, user.refreshToken);
|
|
55
|
+
return valid ? user : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async setEmailVerificationToken(
|
|
59
|
+
userId: string,
|
|
60
|
+
token: string,
|
|
61
|
+
expiresAt: Date,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const tokenHash = this.hashToken(token);
|
|
64
|
+
await this.usersRepository.update(userId, {
|
|
65
|
+
emailVerificationToken: tokenHash,
|
|
66
|
+
emailVerificationTokenExpiresAt: expiresAt,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async findByVerificationToken(token: string): Promise<UserDocument | null> {
|
|
71
|
+
const tokenHash = this.hashToken(token);
|
|
72
|
+
return this.usersRepository.findByVerificationToken(tokenHash);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async markEmailVerified(userId: string): Promise<void> {
|
|
76
|
+
await this.usersRepository.update(userId, {
|
|
77
|
+
emailVerified: true,
|
|
78
|
+
emailVerificationToken: null,
|
|
79
|
+
emailVerificationTokenExpiresAt: null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async setPasswordResetToken(
|
|
84
|
+
userId: string,
|
|
85
|
+
token: string,
|
|
86
|
+
expiresAt: Date,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const tokenHash = this.hashToken(token);
|
|
89
|
+
await this.usersRepository.update(userId, {
|
|
90
|
+
passwordResetToken: tokenHash,
|
|
91
|
+
passwordResetTokenExpiresAt: expiresAt,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async findByPasswordResetToken(token: string): Promise<UserDocument | null> {
|
|
96
|
+
const tokenHash = this.hashToken(token);
|
|
97
|
+
return this.usersRepository.findByPasswordResetToken(tokenHash);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async updatePassword(userId: string, password: string): Promise<void> {
|
|
101
|
+
const hashed = await bcrypt.hash(password, 12);
|
|
102
|
+
await this.usersRepository.update(userId, {
|
|
103
|
+
password: hashed,
|
|
104
|
+
passwordResetToken: null,
|
|
105
|
+
passwordResetTokenExpiresAt: null,
|
|
106
|
+
refreshToken: null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async updateLastLoginAt(userId: string): Promise<void> {
|
|
111
|
+
await this.usersRepository.update(userId, { lastLoginAt: new Date() });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async updateMe(
|
|
115
|
+
userId: string,
|
|
116
|
+
data: { name?: string; email?: string },
|
|
117
|
+
): Promise<UserDocument | null> {
|
|
118
|
+
if (data.email) {
|
|
119
|
+
const existing = await this.usersRepository.findByEmail(data.email);
|
|
120
|
+
if (existing && String(existing._id) !== userId) {
|
|
121
|
+
throw new ConflictException('Este email ya está registrado.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return this.usersRepository.update(userId, data);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private hashToken(token: string): string {
|
|
128
|
+
return crypto.createHash('sha256').update(token).digest('hex');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -1,34 +1,38 @@
|
|
|
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
|
+
|
|
35
|
+
async delete(id: string): Promise<WorkspaceDocument | null> {
|
|
36
|
+
return this.workspaceModel.findByIdAndDelete(id).exec();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,42 +1,51 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import { Workspace, WorkspaceDocument } from './schemas/workspace.schema';
|
|
4
|
+
import { WorkspacesRepository } from './workspaces.repository';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class WorkspacesService {
|
|
8
|
+
constructor(
|
|
9
|
+
private readonly workspacesRepository: WorkspacesRepository,
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async create(ownerId: string, name: string): Promise<WorkspaceDocument> {
|
|
13
|
+
const slug = await this.generateUniqueSlug(name);
|
|
14
|
+
return this.workspacesRepository.create({ name, slug, ownerId });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async update(
|
|
18
|
+
id: string,
|
|
19
|
+
data: Partial<Workspace>,
|
|
20
|
+
): Promise<WorkspaceDocument | null> {
|
|
21
|
+
return this.workspacesRepository.update(id, data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async delete(id: string): Promise<WorkspaceDocument | null> {
|
|
25
|
+
return this.workspacesRepository.delete(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async findById(id: string): Promise<WorkspaceDocument | null> {
|
|
29
|
+
return this.workspacesRepository.findById(id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async generateUniqueSlug(name: string): Promise<string> {
|
|
33
|
+
const base = name
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.normalize('NFD')
|
|
36
|
+
.replace(/\p{M}/gu, '')
|
|
37
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
38
|
+
.replace(/^-+|-+$/g, '');
|
|
39
|
+
|
|
40
|
+
let slug = base || 'workspace';
|
|
41
|
+
let counter = 1;
|
|
42
|
+
const MAX_ATTEMPTS = 20;
|
|
43
|
+
while (counter <= MAX_ATTEMPTS && (await this.workspacesRepository.findBySlug(slug))) {
|
|
44
|
+
slug = `${base}-${counter++}`;
|
|
45
|
+
}
|
|
46
|
+
if (counter > MAX_ATTEMPTS) {
|
|
47
|
+
slug = `${base}-${crypto.randomBytes(4).toString('hex')}`;
|
|
48
|
+
}
|
|
49
|
+
return slug;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ar-saas — Frontend
|
|
2
2
|
|
|
3
3
|
Frontend del template SaaS para startups argentinas. Stack: Next.js 15, TypeScript, Tailwind CSS 4, shadcn/ui.
|
|
4
4
|
|
|
@@ -98,7 +98,7 @@ src/
|
|
|
98
98
|
│ ├── (legal)/ # Términos y privacidad
|
|
99
99
|
│ ├── setup/ # Pantalla de setup inicial (se muestra al arrancar por primera vez)
|
|
100
100
|
│ ├── layout.tsx # Layout raíz con AuthProvider
|
|
101
|
-
│ └── page.tsx # Redirige a /
|
|
101
|
+
│ └── page.tsx # Redirige a /login
|
|
102
102
|
├── components/
|
|
103
103
|
│ ├── landing/ # Componentes de la landing page (navbar, hero, features, pricing)
|
|
104
104
|
│ ├── dashboard/ # Sidebar, header y cards del dashboard
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "ar-saas-frontend",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"lint": "next lint"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@hookform/resolvers": "^3.9.1",
|
|
13
12
|
"@radix-ui/react-accordion": "^1.2.13",
|
|
14
13
|
"@radix-ui/react-avatar": "^1.1.12",
|
|
15
14
|
"@radix-ui/react-checkbox": "^1.3.4",
|
|
@@ -24,19 +23,16 @@
|
|
|
24
23
|
"axios": "^1.7.9",
|
|
25
24
|
"class-variance-authority": "^0.7.1",
|
|
26
25
|
"clsx": "^2.1.1",
|
|
27
|
-
"js-cookie": "^3.0.5",
|
|
28
26
|
"lucide-react": "^0.477.0",
|
|
29
27
|
"next": "^15.3.3",
|
|
30
28
|
"react": "^19.1.0",
|
|
31
29
|
"react-dom": "^19.1.0",
|
|
32
30
|
"react-hook-form": "^7.54.2",
|
|
33
|
-
"tailwind-merge": "^2.6.0"
|
|
34
|
-
"zod": "^3.24.1"
|
|
31
|
+
"tailwind-merge": "^2.6.0"
|
|
35
32
|
},
|
|
36
33
|
"devDependencies": {
|
|
37
34
|
"@eslint/eslintrc": "^3.2.0",
|
|
38
35
|
"@tailwindcss/postcss": "^4.1.0",
|
|
39
|
-
"@types/js-cookie": "^3.0.6",
|
|
40
36
|
"@types/node": "^22.10.7",
|
|
41
37
|
"@types/react": "^19.1.0",
|
|
42
38
|
"@types/react-dom": "^19.1.0",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
allowBuilds:
|
|
2
|
-
sharp:
|
|
3
|
-
unrs-resolver:
|
|
2
|
+
sharp: true
|
|
3
|
+
unrs-resolver: true
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react'
|
|
4
|
-
import { useRouter } from 'next/navigation'
|
|
5
|
-
import { useAuth } from '@/lib/hooks/use-auth'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useAuth } from '@/lib/hooks/use-auth'
|
|
6
|
+
import { siteConfig } from '@/config/site'
|
|
7
|
+
|
|
8
|
+
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
|
9
|
+
const { isAuthenticated, isLoading } = useAuth()
|
|
10
|
+
const router = useRouter()
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!isLoading && isAuthenticated) {
|
|
14
|
+
router.replace('/dashboard')
|
|
15
|
+
}
|
|
16
|
+
}, [isAuthenticated, isLoading, router])
|
|
17
|
+
|
|
18
|
+
if (isLoading) return null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-muted/40 px-4">
|
|
22
|
+
<div className="mb-8 text-center">
|
|
23
|
+
<h1 className="text-2xl font-bold tracking-tight">{siteConfig.name}</h1>
|
|
24
|
+
<p className="text-sm text-muted-foreground mt-1">{siteConfig.tagline}</p>
|
|
25
|
+
</div>
|
|
26
|
+
<div className="w-full max-w-md">{children}</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|