ar-saas 0.3.2 → 0.4.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.
- package/dist/generator.js +2 -6
- package/package.json +1 -1
- package/templates/backend/.env.example +1 -1
- 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/package.json +2 -5
- 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,178 +1,179 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { useForm } from 'react-hook-form'
|
|
5
|
-
import { UserPlus, MoreHorizontal, Crown, User } from 'lucide-react'
|
|
6
|
-
import { useAuth } from '@/lib/hooks/use-auth'
|
|
7
|
-
import { Button } from '@/components/ui/button'
|
|
8
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
|
-
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
10
|
-
import { Badge } from '@/components/ui/badge'
|
|
11
|
-
import { Input } from '@/components/ui/input'
|
|
12
|
-
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
13
|
-
import {
|
|
14
|
-
Dialog,
|
|
15
|
-
DialogContent,
|
|
16
|
-
DialogDescription,
|
|
17
|
-
DialogFooter,
|
|
18
|
-
DialogHeader,
|
|
19
|
-
DialogTitle,
|
|
20
|
-
DialogTrigger,
|
|
21
|
-
} from '@/components/ui/dialog'
|
|
22
|
-
import {
|
|
23
|
-
DropdownMenu,
|
|
24
|
-
DropdownMenuContent,
|
|
25
|
-
DropdownMenuItem,
|
|
26
|
-
DropdownMenuTrigger,
|
|
27
|
-
} from '@/components/ui/dropdown-menu'
|
|
28
|
-
|
|
29
|
-
function getInitials(name: string) {
|
|
30
|
-
return name
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div className="max-w-2xl space-y-6">
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{member.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
</
|
|
152
|
-
</
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useForm } from 'react-hook-form'
|
|
5
|
+
import { UserPlus, MoreHorizontal, Crown, User } from 'lucide-react'
|
|
6
|
+
import { useAuth } from '@/lib/hooks/use-auth'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
9
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
10
|
+
import { Badge } from '@/components/ui/badge'
|
|
11
|
+
import { Input } from '@/components/ui/input'
|
|
12
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
13
|
+
import {
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogDescription,
|
|
17
|
+
DialogFooter,
|
|
18
|
+
DialogHeader,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
DialogTrigger,
|
|
21
|
+
} from '@/components/ui/dialog'
|
|
22
|
+
import {
|
|
23
|
+
DropdownMenu,
|
|
24
|
+
DropdownMenuContent,
|
|
25
|
+
DropdownMenuItem,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from '@/components/ui/dropdown-menu'
|
|
28
|
+
|
|
29
|
+
function getInitials(name: string): string {
|
|
30
|
+
return name
|
|
31
|
+
.trim()
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map((n) => n[0])
|
|
35
|
+
.slice(0, 2)
|
|
36
|
+
.join('')
|
|
37
|
+
.toUpperCase() || 'U'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface InviteForm {
|
|
41
|
+
email: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function TeamPage() {
|
|
45
|
+
const { user } = useAuth()
|
|
46
|
+
const [inviteOpen, setInviteOpen] = useState(false)
|
|
47
|
+
|
|
48
|
+
const members = [
|
|
49
|
+
{ name: user?.name ?? 'Vos', email: user?.email ?? '', role: 'owner' as const },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const form = useForm<InviteForm>({ defaultValues: { email: '' } })
|
|
53
|
+
|
|
54
|
+
function onInvite(data: InviteForm) {
|
|
55
|
+
setInviteOpen(false)
|
|
56
|
+
form.reset()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="max-w-2xl space-y-6">
|
|
61
|
+
<div className="rounded-lg border border-amber-200 bg-amber-50 p-3">
|
|
62
|
+
<p className="text-xs text-amber-800 font-medium">Próximamente</p>
|
|
63
|
+
<p className="text-xs text-amber-700 mt-0.5">La gestión de equipo estará disponible en la próxima actualización.</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<Card>
|
|
67
|
+
<CardHeader>
|
|
68
|
+
<div className="flex items-start justify-between">
|
|
69
|
+
<div>
|
|
70
|
+
<CardTitle>Miembros del equipo</CardTitle>
|
|
71
|
+
<CardDescription>
|
|
72
|
+
Gestioná quién tiene acceso a tu workspace.
|
|
73
|
+
</CardDescription>
|
|
74
|
+
</div>
|
|
75
|
+
<Dialog open={inviteOpen} onOpenChange={setInviteOpen}>
|
|
76
|
+
<DialogTrigger asChild>
|
|
77
|
+
<Button size="sm" className="gap-2" disabled>
|
|
78
|
+
<UserPlus className="size-4" />
|
|
79
|
+
Invitar
|
|
80
|
+
</Button>
|
|
81
|
+
</DialogTrigger>
|
|
82
|
+
<DialogContent>
|
|
83
|
+
<DialogHeader>
|
|
84
|
+
<DialogTitle>Invitar miembro</DialogTitle>
|
|
85
|
+
<DialogDescription>
|
|
86
|
+
Ingresá el email de la persona que querés agregar al workspace.
|
|
87
|
+
</DialogDescription>
|
|
88
|
+
</DialogHeader>
|
|
89
|
+
<Form {...form}>
|
|
90
|
+
<form onSubmit={form.handleSubmit(onInvite)} className="space-y-4">
|
|
91
|
+
<FormField
|
|
92
|
+
control={form.control}
|
|
93
|
+
name="email"
|
|
94
|
+
rules={{
|
|
95
|
+
required: 'El email es requerido',
|
|
96
|
+
pattern: { value: /\S+@\S+\.\S+/, message: 'Email inválido' },
|
|
97
|
+
}}
|
|
98
|
+
render={({ field }) => (
|
|
99
|
+
<FormItem>
|
|
100
|
+
<FormLabel>Email</FormLabel>
|
|
101
|
+
<FormControl>
|
|
102
|
+
<Input type="email" placeholder="colaborador@empresa.com" {...field} />
|
|
103
|
+
</FormControl>
|
|
104
|
+
<FormMessage />
|
|
105
|
+
</FormItem>
|
|
106
|
+
)}
|
|
107
|
+
/>
|
|
108
|
+
<DialogFooter>
|
|
109
|
+
<Button type="button" variant="ghost" onClick={() => setInviteOpen(false)}>
|
|
110
|
+
Cancelar
|
|
111
|
+
</Button>
|
|
112
|
+
<Button type="submit" disabled>
|
|
113
|
+
Próximamente
|
|
114
|
+
</Button>
|
|
115
|
+
</DialogFooter>
|
|
116
|
+
</form>
|
|
117
|
+
</Form>
|
|
118
|
+
</DialogContent>
|
|
119
|
+
</Dialog>
|
|
120
|
+
</div>
|
|
121
|
+
</CardHeader>
|
|
122
|
+
<CardContent>
|
|
123
|
+
<div className="space-y-3">
|
|
124
|
+
{members.map((member) => (
|
|
125
|
+
<div
|
|
126
|
+
key={member.email}
|
|
127
|
+
className="flex items-center justify-between rounded-lg border p-3"
|
|
128
|
+
>
|
|
129
|
+
<div className="flex items-center gap-3">
|
|
130
|
+
<Avatar className="size-8">
|
|
131
|
+
<AvatarFallback className="text-xs">{getInitials(member.name)}</AvatarFallback>
|
|
132
|
+
</Avatar>
|
|
133
|
+
<div>
|
|
134
|
+
<p className="text-sm font-medium">{member.name}</p>
|
|
135
|
+
<p className="text-xs text-muted-foreground">{member.email}</p>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
139
|
+
<Badge variant={member.role === 'owner' ? 'default' : 'secondary'} className="gap-1 text-xs">
|
|
140
|
+
{member.role === 'owner' ? (
|
|
141
|
+
<><Crown className="size-3" /> Owner</>
|
|
142
|
+
) : (
|
|
143
|
+
<><User className="size-3" /> Miembro</>
|
|
144
|
+
)}
|
|
145
|
+
</Badge>
|
|
146
|
+
{member.role !== 'owner' && (
|
|
147
|
+
<DropdownMenu>
|
|
148
|
+
<DropdownMenuTrigger asChild>
|
|
149
|
+
<Button variant="ghost" size="sm" className="size-7 p-0">
|
|
150
|
+
<MoreHorizontal className="size-4" />
|
|
151
|
+
</Button>
|
|
152
|
+
</DropdownMenuTrigger>
|
|
153
|
+
<DropdownMenuContent align="end">
|
|
154
|
+
<DropdownMenuItem className="text-destructive focus:text-destructive">
|
|
155
|
+
Eliminar miembro
|
|
156
|
+
</DropdownMenuItem>
|
|
157
|
+
</DropdownMenuContent>
|
|
158
|
+
</DropdownMenu>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</CardContent>
|
|
165
|
+
</Card>
|
|
166
|
+
|
|
167
|
+
{/* Pending invitations placeholder */}
|
|
168
|
+
<Card>
|
|
169
|
+
<CardHeader>
|
|
170
|
+
<CardTitle>Invitaciones pendientes</CardTitle>
|
|
171
|
+
<CardDescription>Invitaciones enviadas que aún no fueron aceptadas.</CardDescription>
|
|
172
|
+
</CardHeader>
|
|
173
|
+
<CardContent>
|
|
174
|
+
<p className="text-sm text-muted-foreground">No hay invitaciones pendientes.</p>
|
|
175
|
+
</CardContent>
|
|
176
|
+
</Card>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
import type { Metadata } from 'next'
|
|
2
|
-
import { Geist, Geist_Mono } from 'next/font/google'
|
|
3
|
-
import './globals.css'
|
|
4
|
-
import { AuthProvider } from '@/providers/auth-provider'
|
|
5
|
-
import { Toaster } from '@/components/ui/toaster'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Geist, Geist_Mono } from 'next/font/google'
|
|
3
|
+
import './globals.css'
|
|
4
|
+
import { AuthProvider } from '@/providers/auth-provider'
|
|
5
|
+
import { Toaster } from '@/components/ui/toaster'
|
|
6
|
+
import { siteConfig } from '@/config/site'
|
|
7
|
+
|
|
8
|
+
const geistSans = Geist({ variable: '--font-geist-sans', subsets: ['latin'] })
|
|
9
|
+
const geistMono = Geist_Mono({ variable: '--font-geist-mono', subsets: ['latin'] })
|
|
10
|
+
|
|
11
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
12
|
+
return {
|
|
13
|
+
title: siteConfig.name,
|
|
14
|
+
description: siteConfig.description,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="es">
|
|
21
|
+
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
|
22
|
+
<AuthProvider>
|
|
23
|
+
{children}
|
|
24
|
+
<Toaster />
|
|
25
|
+
</AuthProvider>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -139,7 +139,7 @@ export default function SetupPage() {
|
|
|
139
139
|
<div className="mb-10">
|
|
140
140
|
<div className="inline-flex items-center gap-2 bg-zinc-900 text-white text-xs font-mono px-3 py-1.5 rounded-full mb-4">
|
|
141
141
|
<span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
|
|
142
|
-
|
|
142
|
+
ar-saas
|
|
143
143
|
</div>
|
|
144
144
|
<h1 className="text-3xl font-bold text-zinc-900 mb-2">
|
|
145
145
|
Tu proyecto está listo
|
|
@@ -22,13 +22,15 @@ const breadcrumbMap: Record<string, string> = {
|
|
|
22
22
|
'/team': 'Equipo',
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function getInitials(name: string) {
|
|
25
|
+
function getInitials(name: string): string {
|
|
26
26
|
return name
|
|
27
|
-
.
|
|
27
|
+
.trim()
|
|
28
|
+
.split(/\s+/)
|
|
29
|
+
.filter(Boolean)
|
|
28
30
|
.map((n) => n[0])
|
|
29
31
|
.slice(0, 2)
|
|
30
32
|
.join('')
|
|
31
|
-
.toUpperCase()
|
|
33
|
+
.toUpperCase() || 'U'
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function DashboardHeader() {
|
|
@@ -183,7 +183,7 @@ export const siteConfig = {
|
|
|
183
183
|
github: '',
|
|
184
184
|
linkedin: '',
|
|
185
185
|
},
|
|
186
|
-
copyright:
|
|
186
|
+
copyright: `© ${new Date().getFullYear()} __SITE_NAME__. Todos los derechos reservados.`,
|
|
187
187
|
},
|
|
188
188
|
|
|
189
189
|
// ── Legal ──────────────────────────────────────────────────────────────────
|