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.
Files changed (30) hide show
  1. package/dist/generator.js +2 -6
  2. package/package.json +1 -1
  3. package/templates/backend/.env.example +1 -1
  4. package/templates/backend/README.md +6 -6
  5. package/templates/backend/package.json +5 -2
  6. package/templates/backend/src/app.module.ts +68 -40
  7. package/templates/backend/src/common/interceptors/workspace-tenant.interceptor.ts +27 -45
  8. package/templates/backend/src/main.ts +50 -51
  9. package/templates/backend/src/modules/auth/auth.controller.ts +162 -158
  10. package/templates/backend/src/modules/auth/auth.service.ts +236 -257
  11. package/templates/backend/src/modules/auth/strategies/jwt.strategy.ts +45 -43
  12. package/templates/backend/src/modules/users/users.controller.ts +28 -0
  13. package/templates/backend/src/modules/users/users.module.ts +16 -14
  14. package/templates/backend/src/modules/users/users.repository.ts +57 -51
  15. package/templates/backend/src/modules/users/users.service.ts +130 -104
  16. package/templates/backend/src/modules/workspaces/workspaces.repository.ts +38 -34
  17. package/templates/backend/src/modules/workspaces/workspaces.service.ts +51 -42
  18. package/templates/frontend/README.md +2 -2
  19. package/templates/frontend/package.json +2 -6
  20. package/templates/frontend/pnpm-workspace.yaml +2 -2
  21. package/templates/frontend/src/app/(auth)/layout.tsx +29 -28
  22. package/templates/frontend/src/app/(dashboard)/billing/page.tsx +111 -111
  23. package/templates/frontend/src/app/(dashboard)/profile/page.tsx +241 -226
  24. package/templates/frontend/src/app/(dashboard)/settings/page.tsx +155 -156
  25. package/templates/frontend/src/app/(dashboard)/team/page.tsx +179 -178
  26. package/templates/frontend/src/app/layout.tsx +29 -26
  27. package/templates/frontend/src/app/page.tsx +1 -1
  28. package/templates/frontend/src/app/setup/page.tsx +1 -1
  29. package/templates/frontend/src/components/dashboard/header.tsx +5 -3
  30. 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
- .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: 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 { 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 * 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 { 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 * 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
- # create-saas-ar — Frontend
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 /setup
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": "create-saas-ar-frontend",
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: set this to true or false
3
- unrs-resolver: set this to true or false
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
- export default function AuthLayout({ children }: { children: React.ReactNode }) {
8
- const { isAuthenticated, isLoading } = useAuth()
9
- const router = useRouter()
10
-
11
- useEffect(() => {
12
- if (!isLoading && isAuthenticated) {
13
- router.replace('/dashboard')
14
- }
15
- }, [isAuthenticated, isLoading, router])
16
-
17
- if (isLoading) return null
18
-
19
- return (
20
- <div className="min-h-screen flex flex-col items-center justify-center bg-muted/40 px-4">
21
- <div className="mb-8 text-center">
22
- <h1 className="text-2xl font-bold tracking-tight">create-saas-ar</h1>
23
- <p className="text-sm text-muted-foreground mt-1">Tu SaaS listo para Argentina</p>
24
- </div>
25
- <div className="w-full max-w-md">{children}</div>
26
- </div>
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
+ }