@lastbrain/module-auth 0.1.22 → 0.1.23

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 (42) hide show
  1. package/dist/api/admin/signup-stats.d.ts +21 -0
  2. package/dist/api/admin/signup-stats.d.ts.map +1 -0
  3. package/dist/api/admin/signup-stats.js +75 -0
  4. package/dist/api/admin/users-by-source.d.ts +22 -0
  5. package/dist/api/admin/users-by-source.d.ts.map +1 -0
  6. package/dist/api/admin/users-by-source.js +56 -0
  7. package/dist/api/public/signup.d.ts +10 -0
  8. package/dist/api/public/signup.d.ts.map +1 -0
  9. package/dist/api/public/signup.js +71 -0
  10. package/dist/auth.build.config.d.ts.map +1 -1
  11. package/dist/auth.build.config.js +26 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -0
  15. package/dist/server.d.ts +1 -0
  16. package/dist/server.d.ts.map +1 -1
  17. package/dist/server.js +1 -0
  18. package/dist/web/admin/signup-stats.d.ts +2 -0
  19. package/dist/web/admin/signup-stats.d.ts.map +1 -0
  20. package/dist/web/admin/signup-stats.js +50 -0
  21. package/dist/web/admin/user-detail.d.ts.map +1 -1
  22. package/dist/web/admin/user-detail.js +5 -1
  23. package/dist/web/admin/users-by-signup-source.d.ts +2 -0
  24. package/dist/web/admin/users-by-signup-source.d.ts.map +1 -0
  25. package/dist/web/admin/users-by-signup-source.js +79 -0
  26. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  27. package/dist/web/public/SignUpPage.js +15 -23
  28. package/package.json +4 -4
  29. package/src/api/admin/signup-stats.ts +109 -0
  30. package/src/api/admin/users-by-source.ts +87 -0
  31. package/src/api/public/signup.ts +106 -0
  32. package/src/auth.build.config.ts +27 -0
  33. package/src/index.ts +1 -0
  34. package/src/server.ts +1 -0
  35. package/src/web/admin/signup-stats.tsx +304 -0
  36. package/src/web/admin/user-detail.tsx +16 -0
  37. package/src/web/admin/users-by-signup-source.tsx +262 -0
  38. package/src/web/public/SignUpPage.tsx +16 -25
  39. package/supabase/migrations/20251112000000_user_init.sql +18 -1
  40. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +10 -2
  41. package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +2 -1
  42. package/supabase/migrations-down/20251204000000_add_signup_source.sql +12 -0
@@ -0,0 +1,304 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ Card,
6
+ CardBody,
7
+ CardHeader,
8
+ Chip,
9
+ Spinner,
10
+ Tab,
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableColumn,
15
+ TableHeader,
16
+ TableRow,
17
+ Tabs,
18
+ } from "@lastbrain/ui";
19
+ import { BarChart3, TrendingUp } from "lucide-react";
20
+
21
+ interface SignupStats {
22
+ total: number;
23
+ bySource: {
24
+ lastbrain: number;
25
+ recipe: number;
26
+ };
27
+ byDate: Array<{
28
+ date: string;
29
+ lastbrain: number;
30
+ recipe: number;
31
+ total: number;
32
+ }>;
33
+ }
34
+
35
+ export function SignupStatsPage() {
36
+ const [stats, setStats] = useState<SignupStats | null>(null);
37
+ const [loading, setLoading] = useState(true);
38
+ const [error, setError] = useState<string | null>(null);
39
+
40
+ useEffect(() => {
41
+ fetchStats();
42
+ }, []);
43
+
44
+ const fetchStats = async () => {
45
+ try {
46
+ setLoading(true);
47
+ const response = await fetch("/api/admin/signup-stats");
48
+
49
+ if (!response.ok) {
50
+ throw new Error("Erreur lors du chargement des statistiques");
51
+ }
52
+
53
+ const result = await response.json();
54
+ setStats(result.data);
55
+ } catch (err) {
56
+ setError(
57
+ err instanceof Error ? err.message : "Erreur lors du chargement"
58
+ );
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ };
63
+
64
+ if (loading) {
65
+ return (
66
+ <div className="flex justify-center items-center min-h-screen">
67
+ <Spinner size="lg" label="Chargement des statistiques..." />
68
+ </div>
69
+ );
70
+ }
71
+
72
+ if (error || !stats) {
73
+ return (
74
+ <div className="p-6">
75
+ <Card className="border border-danger-200 bg-danger-50/50">
76
+ <CardBody>
77
+ <p className="text-danger-600">{error || "Erreur de chargement"}</p>
78
+ </CardBody>
79
+ </Card>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ const lastbrainPercentage = (
85
+ (stats.bySource.lastbrain / stats.total) *
86
+ 100
87
+ ).toFixed(1);
88
+ const recipePercentage = (
89
+ (stats.bySource.recipe / stats.total) *
90
+ 100
91
+ ).toFixed(1);
92
+
93
+ return (
94
+ <div className="space-y-6 px-2 md:p-6">
95
+ {/* Header */}
96
+ <div className="flex items-center gap-2 mb-8">
97
+ <BarChart3 size={28} className="text-primary-600" />
98
+ <h1 className="text-3xl font-bold">Statistiques d'inscriptions</h1>
99
+ </div>
100
+
101
+ {/* Key Metrics Cards */}
102
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-12">
103
+ {/* Total Signups */}
104
+ <Card className="bg-content2 border border-primary-200 dark:border-primary-800">
105
+ <CardBody className="gap-4">
106
+ <div className="flex items-center justify-between">
107
+ <span className="text-sm font-medium text-primary-600 dark:text-primary-400">
108
+ Total d'inscriptions
109
+ </span>
110
+ <TrendingUp size={20} className="text-primary-600" />
111
+ </div>
112
+ <p className="text-4xl font-bold text-primary-700 dark:text-primary-300">
113
+ {stats.total}
114
+ </p>
115
+ </CardBody>
116
+ </Card>
117
+
118
+ {/* LastBrain Signups */}
119
+ <Card className="bg-content2 border border-secondary-200 dark:border-secondary-800">
120
+ <CardBody className="gap-4">
121
+ <div className="flex items-center justify-between">
122
+ <span className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
123
+ Inscriptions LastBrain
124
+ </span>
125
+ </div>
126
+ <div className="flex items-end gap-2">
127
+ <p className="text-4xl font-bold text-secondary-700 dark:text-secondary-300">
128
+ {stats.bySource.lastbrain}
129
+ </p>
130
+ <Chip size="sm" color="secondary" variant="flat">
131
+ {lastbrainPercentage}%
132
+ </Chip>
133
+ </div>
134
+ </CardBody>
135
+ </Card>
136
+
137
+ {/* Recipe Signups */}
138
+ <Card className="bg-content2 border border-success-200 dark:border-success-800">
139
+ <CardBody className="gap-4">
140
+ <div className="flex items-center justify-between">
141
+ <span className="text-sm font-medium text-success-600 dark:text-success-400">
142
+ Inscriptions Recipe
143
+ </span>
144
+ </div>
145
+ <div className="flex items-end gap-2">
146
+ <p className="text-4xl font-bold text-success-700 dark:text-success-300">
147
+ {stats.bySource.recipe}
148
+ </p>
149
+ <Chip size="sm" color="success" variant="flat">
150
+ {recipePercentage}%
151
+ </Chip>
152
+ </div>
153
+ </CardBody>
154
+ </Card>
155
+ </div>
156
+
157
+ {/* Tabs for different views */}
158
+ <Tabs
159
+ aria-label="Vues des statistiques"
160
+ color="primary"
161
+ variant="bordered"
162
+ >
163
+ {/* By Source Tab */}
164
+ <Tab key="by-source" title="Par Source">
165
+ <Card className="mt-6">
166
+ <CardHeader>
167
+ <h3 className="text-lg font-semibold">Résumé par source</h3>
168
+ </CardHeader>
169
+ <CardBody>
170
+ <div className="space-y-4">
171
+ {/* LastBrain */}
172
+ <div className="p-4 rounded-lg border border-secondary-800 dark:border-secondary-800">
173
+ <div className="flex items-center justify-between mb-2">
174
+ <span className="font-medium text-secondary-700 dark:text-secondary-300">
175
+ LastBrain
176
+ </span>
177
+ <Chip size="sm" color="secondary" variant="flat">
178
+ {stats.bySource.lastbrain} inscriptions
179
+ </Chip>
180
+ </div>
181
+ <div className="w-full bg-default-200 rounded-full h-2 dark:bg-default-700">
182
+ <div
183
+ className="bg-secondary-500 h-2 rounded-full"
184
+ style={{
185
+ width: `${(stats.bySource.lastbrain / stats.total) * 100}%`,
186
+ }}
187
+ />
188
+ </div>
189
+ <p className="text-xs text-default-500 mt-2">
190
+ {lastbrainPercentage}% du total
191
+ </p>
192
+ </div>
193
+
194
+ {/* Recipe */}
195
+ <div className="p-4 rounded-lg border border-success-200 dark:border-success-800">
196
+ <div className="flex items-center justify-between mb-2">
197
+ <span className="font-medium text-success-700 dark:text-success-300">
198
+ Recipe
199
+ </span>
200
+ <Chip size="sm" color="success" variant="flat">
201
+ {stats.bySource.recipe} inscriptions
202
+ </Chip>
203
+ </div>
204
+ <div className="w-full bg-default-200 rounded-full h-2 dark:bg-default-700">
205
+ <div
206
+ className="bg-success-500 h-2 rounded-full"
207
+ style={{
208
+ width: `${(stats.bySource.recipe / stats.total) * 100}%`,
209
+ }}
210
+ />
211
+ </div>
212
+ <p className="text-xs text-default-500 mt-2">
213
+ {recipePercentage}% du total
214
+ </p>
215
+ </div>
216
+ </div>
217
+ </CardBody>
218
+ </Card>
219
+ </Tab>
220
+
221
+ {/* By Date Tab */}
222
+ <Tab key="by-date" title="Par Date (30 derniers jours)">
223
+ <Card className="mt-6">
224
+ <CardHeader>
225
+ <h3 className="text-lg font-semibold">Inscriptions par date</h3>
226
+ <p className="text-sm text-default-500">
227
+ Derniers 30 jours avec répartition par source
228
+ </p>
229
+ </CardHeader>
230
+ <CardBody>
231
+ <Table
232
+ aria-label="Tableau des inscriptions par date"
233
+ className="rounded-lg"
234
+ >
235
+ <TableHeader>
236
+ <TableColumn>Date</TableColumn>
237
+ <TableColumn className="text-right">LastBrain</TableColumn>
238
+ <TableColumn className="text-right">Recipe</TableColumn>
239
+ <TableColumn className="text-right">Total</TableColumn>
240
+ </TableHeader>
241
+ <TableBody>
242
+ {stats.byDate.map((row) => (
243
+ <TableRow key={row.date}>
244
+ <TableCell>
245
+ <span className="font-medium">
246
+ {new Date(row.date).toLocaleDateString("fr-FR", {
247
+ weekday: "short",
248
+ year: "2-digit",
249
+ month: "2-digit",
250
+ day: "2-digit",
251
+ })}
252
+ </span>
253
+ </TableCell>
254
+ <TableCell className="text-right">
255
+ <Chip
256
+ size="sm"
257
+ color={row.lastbrain > 0 ? "secondary" : "default"}
258
+ variant="flat"
259
+ >
260
+ {row.lastbrain}
261
+ </Chip>
262
+ </TableCell>
263
+ <TableCell className="text-right">
264
+ <Chip
265
+ size="sm"
266
+ color={row.recipe > 0 ? "success" : "default"}
267
+ variant="flat"
268
+ >
269
+ {row.recipe}
270
+ </Chip>
271
+ </TableCell>
272
+ <TableCell className="text-right">
273
+ <span className="font-semibold text-primary-600 dark:text-primary-400">
274
+ {row.total}
275
+ </span>
276
+ </TableCell>
277
+ </TableRow>
278
+ ))}
279
+ </TableBody>
280
+ </Table>
281
+
282
+ {stats.byDate.length === 0 && (
283
+ <div className="text-center py-8 text-default-500">
284
+ <p>Aucune donnée disponible pour les 30 derniers jours</p>
285
+ </div>
286
+ )}
287
+ </CardBody>
288
+ </Card>
289
+ </Tab>
290
+ </Tabs>
291
+
292
+ {/* Info Card */}
293
+ <Card className="border-l-4 border-l-primary-500 bg-primary-50/30 dark:bg-primary-950/20">
294
+ <CardBody className="py-3">
295
+ <p className="text-sm text-primary-700 dark:text-primary-300">
296
+ <strong>💡 Info:</strong> Ces statistiques vous permettent de suivre
297
+ les inscriptions en fonction de la source (LastBrain ou Recipe) pour
298
+ mieux comprendre d'où proviennent vos utilisateurs.
299
+ </p>
300
+ </CardBody>
301
+ </Card>
302
+ </div>
303
+ );
304
+ }
@@ -58,6 +58,7 @@ interface UserProfile {
58
58
  avatar_url?: string;
59
59
  created_at?: string;
60
60
  updated_at?: string;
61
+ signup_source?: string;
61
62
  };
62
63
  }
63
64
 
@@ -223,6 +224,21 @@ export function UserDetailPage({
223
224
  >
224
225
  {isAdmin ? "Administrateur" : "Utilisateur"}
225
226
  </Chip>
227
+ {userProfile.profile?.signup_source && (
228
+ <Chip
229
+ variant="flat"
230
+ color={
231
+ userProfile.profile.signup_source.toLowerCase() === "recipe"
232
+ ? "success"
233
+ : "secondary"
234
+ }
235
+ size="sm"
236
+ >
237
+ {userProfile.profile.signup_source.toLowerCase() === "recipe"
238
+ ? "Recipe"
239
+ : "LastBrain"}
240
+ </Chip>
241
+ )}
226
242
  <Snippet color="default" size="sm" symbol="#">
227
243
  {userId}
228
244
  </Snippet>
@@ -0,0 +1,262 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ Card,
6
+ CardBody,
7
+ CardHeader,
8
+ Chip,
9
+ Input,
10
+ Pagination,
11
+ Select,
12
+ SelectItem,
13
+ Spinner,
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableColumn,
18
+ TableHeader,
19
+ TableRow,
20
+ } from "@lastbrain/ui";
21
+ import { Search, Users } from "lucide-react";
22
+
23
+ interface User {
24
+ id: string;
25
+ owner_id: string;
26
+ email: string;
27
+ name: string;
28
+ signup_source: string;
29
+ created_at: string;
30
+ }
31
+
32
+ interface PaginationData {
33
+ page: number;
34
+ limit: number;
35
+ total: number;
36
+ totalPages: number;
37
+ }
38
+
39
+ export function UsersBySignupSourcePage() {
40
+ const [users, setUsers] = useState<User[]>([]);
41
+ const [loading, setLoading] = useState(true);
42
+ const [error, setError] = useState<string | null>(null);
43
+ const [pagination, setPagination] = useState<PaginationData>({
44
+ page: 1,
45
+ limit: 25,
46
+ total: 0,
47
+ totalPages: 0,
48
+ });
49
+ const [source, setSource] = useState<string>(""); // '' for all, 'lastbrain', 'recipe'
50
+ const [searchQuery, setSearchQuery] = useState("");
51
+
52
+ useEffect(() => {
53
+ fetchUsers(pagination.page, source, searchQuery);
54
+ }, []);
55
+
56
+ const fetchUsers = async (
57
+ page: number,
58
+ selectedSource: string,
59
+ query: string
60
+ ) => {
61
+ try {
62
+ setLoading(true);
63
+
64
+ let url = `/api/admin/users-by-source?page=${page}&limit=25`;
65
+ if (selectedSource) {
66
+ url += `&source=${selectedSource}`;
67
+ }
68
+
69
+ const response = await fetch(url);
70
+
71
+ if (!response.ok) {
72
+ throw new Error("Erreur lors du chargement des utilisateurs");
73
+ }
74
+
75
+ const result = await response.json();
76
+
77
+ // Filter by search query if provided
78
+ let filteredUsers = result.data;
79
+ if (query) {
80
+ filteredUsers = filteredUsers.filter(
81
+ (user: User) =>
82
+ user.email.toLowerCase().includes(query.toLowerCase()) ||
83
+ user.name.toLowerCase().includes(query.toLowerCase())
84
+ );
85
+ }
86
+
87
+ setUsers(filteredUsers);
88
+ setPagination(result.pagination);
89
+ } catch (err) {
90
+ setError(
91
+ err instanceof Error ? err.message : "Erreur lors du chargement"
92
+ );
93
+ } finally {
94
+ setLoading(false);
95
+ }
96
+ };
97
+
98
+ const handleSourceChange = (value: string) => {
99
+ setSource(value);
100
+ fetchUsers(1, value, searchQuery);
101
+ };
102
+
103
+ const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
104
+ const query = e.target.value;
105
+ setSearchQuery(query);
106
+ fetchUsers(1, source, query);
107
+ };
108
+
109
+ const handlePageChange = (page: number) => {
110
+ fetchUsers(page, source, searchQuery);
111
+ };
112
+
113
+ const getSourceColor = (src: string) => {
114
+ return src.toLowerCase() === "recipe" ? "success" : "secondary";
115
+ };
116
+
117
+ if (error) {
118
+ return (
119
+ <div className="p-6">
120
+ <Card className="border border-danger-200 bg-danger-50/50">
121
+ <CardBody>
122
+ <p className="text-danger-600">{error}</p>
123
+ </CardBody>
124
+ </Card>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <div className="space-y-6 p-6">
131
+ {/* Header */}
132
+ <div className="flex items-center gap-2 mb-8">
133
+ <Users size={28} className="text-primary-600" />
134
+ <h1 className="text-3xl font-bold">
135
+ Utilisateurs par source d'inscription
136
+ </h1>
137
+ </div>
138
+
139
+ {/* Filters */}
140
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
141
+ <Input
142
+ placeholder="Rechercher par email ou nom..."
143
+ value={searchQuery}
144
+ onChange={handleSearch}
145
+ startContent={<Search size={16} />}
146
+ isClearable
147
+ onClear={() => {
148
+ setSearchQuery("");
149
+ fetchUsers(1, source, "");
150
+ }}
151
+ />
152
+
153
+ <Select
154
+ label="Filtrer par source"
155
+ selectedKeys={[source]}
156
+ onChange={(e) => handleSourceChange(e.target.value)}
157
+ >
158
+ <SelectItem key="">Toutes les sources</SelectItem>
159
+ <SelectItem key="lastbrain">LastBrain</SelectItem>
160
+ <SelectItem key="recipe">Recipe</SelectItem>
161
+ </Select>
162
+ </div>
163
+
164
+ {/* Results info */}
165
+ <div className="text-sm text-default-600">
166
+ Affichage de{" "}
167
+ <span className="font-semibold">
168
+ {(pagination.page - 1) * pagination.limit + 1}-
169
+ {Math.min(pagination.page * pagination.limit, pagination.total)}
170
+ </span>{" "}
171
+ sur <span className="font-semibold">{pagination.total}</span>{" "}
172
+ utilisateurs
173
+ </div>
174
+
175
+ {/* Users Table */}
176
+ <Card>
177
+ <CardHeader>
178
+ <h3 className="text-lg font-semibold">Liste des utilisateurs</h3>
179
+ </CardHeader>
180
+ <CardBody>
181
+ {loading ? (
182
+ <div className="flex justify-center py-8">
183
+ <Spinner size="lg" label="Chargement..." />
184
+ </div>
185
+ ) : (
186
+ <>
187
+ <Table aria-label="Tableau des utilisateurs">
188
+ <TableHeader>
189
+ <TableColumn>Nom</TableColumn>
190
+ <TableColumn>Email</TableColumn>
191
+ <TableColumn>Source</TableColumn>
192
+ <TableColumn>Date d'inscription</TableColumn>
193
+ </TableHeader>
194
+ <TableBody>
195
+ {users.length > 0 ? (
196
+ users.map((user) => (
197
+ <TableRow key={user.id}>
198
+ <TableCell>
199
+ <span className="font-medium">{user.name}</span>
200
+ </TableCell>
201
+ <TableCell>
202
+ <span className="text-sm text-default-600">
203
+ {user.email}
204
+ </span>
205
+ </TableCell>
206
+ <TableCell>
207
+ <Chip
208
+ size="sm"
209
+ color={getSourceColor(user.signup_source)}
210
+ variant="flat"
211
+ >
212
+ {user.signup_source.toLowerCase() === "recipe"
213
+ ? "🍳 Recipe"
214
+ : "🧠 LastBrain"}
215
+ </Chip>
216
+ </TableCell>
217
+ <TableCell>
218
+ <span className="text-sm text-default-500">
219
+ {new Date(user.created_at).toLocaleDateString(
220
+ "fr-FR",
221
+ {
222
+ year: "2-digit",
223
+ month: "2-digit",
224
+ day: "2-digit",
225
+ hour: "2-digit",
226
+ minute: "2-digit",
227
+ }
228
+ )}
229
+ </span>
230
+ </TableCell>
231
+ </TableRow>
232
+ ))
233
+ ) : (
234
+ <TableRow>
235
+ <TableCell colSpan={4} className="text-center py-8">
236
+ <p className="text-default-500">
237
+ Aucun utilisateur trouvé
238
+ </p>
239
+ </TableCell>
240
+ </TableRow>
241
+ )}
242
+ </TableBody>
243
+ </Table>
244
+
245
+ {/* Pagination */}
246
+ {pagination.totalPages > 1 && (
247
+ <div className="flex justify-center mt-6">
248
+ <Pagination
249
+ total={pagination.totalPages}
250
+ page={pagination.page}
251
+ onChange={handlePageChange}
252
+ showControls
253
+ />
254
+ </div>
255
+ )}
256
+ </>
257
+ )}
258
+ </CardBody>
259
+ </Card>
260
+ </div>
261
+ );
262
+ }
@@ -33,7 +33,7 @@ function SignUpForm() {
33
33
  const [success, setSuccess] = useState<string | null>(null);
34
34
 
35
35
  // Récupérer le paramètre redirect
36
- const redirectUrl = searchParams.get("redirect");
36
+ const redirectUrl = searchParams?.get("redirect") || "";
37
37
 
38
38
  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
39
39
  event.preventDefault();
@@ -53,43 +53,34 @@ function SignUpForm() {
53
53
  setLoading(true);
54
54
 
55
55
  try {
56
- const { data, error: signUpError } =
57
- await supabaseBrowserClient.auth.signUp({
56
+ // Appeler la nouvelle route API signup (signupSource sera déterminé côté serveur via VERCEL_URL)
57
+ const response = await fetch("/api/auth/signup", {
58
+ method: "POST",
59
+ headers: {
60
+ "Content-Type": "application/json",
61
+ },
62
+ body: JSON.stringify({
58
63
  email,
59
64
  password,
60
- options: {
61
- emailRedirectTo: `${window.location.origin}/api/auth/callback${
62
- redirectUrl ? `?next=${encodeURIComponent(redirectUrl)}` : ""
63
- }`,
64
- data: {
65
- full_name: fullName,
66
- },
67
- },
68
- });
65
+ fullName,
66
+ }),
67
+ });
68
+
69
+ const result = await response.json();
69
70
 
70
- if (signUpError) {
71
- setError(signUpError.message);
71
+ if (!response.ok) {
72
+ setError(result.error || "Erreur lors de l'inscription");
72
73
  return;
73
74
  }
74
75
 
75
76
  // Si la confirmation par email est requise
76
- if (data.user && !data.session) {
77
+ if (result.data.user && !result.data.session) {
77
78
  setSuccess(
78
79
  "Compte créé avec succès ! Veuillez vérifier votre email pour confirmer votre compte."
79
80
  );
80
81
  return;
81
82
  }
82
83
 
83
- // Si l'utilisateur est directement connecté (confirmation email désactivée)
84
- if (data.session) {
85
- if (redirectUrl) {
86
- window.location.href = redirectUrl;
87
- } else {
88
- window.location.href = "/auth/dashboard";
89
- }
90
- return;
91
- }
92
-
93
84
  setSuccess("Compte créé. Vous pouvez désormais vous connecter.");
94
85
  setTimeout(() => {
95
86
  if (redirectUrl) {
@@ -177,4 +177,21 @@ CREATE TRIGGER set_user_notifications_updated_at
177
177
  -- =====================================================
178
178
  -- Enable Realtime for user_notifications
179
179
  -- =====================================================
180
- ALTER PUBLICATION supabase_realtime ADD TABLE public.user_notifications;
180
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.user_notifications;
181
+
182
+ -- Add signup_source to user_profil table
183
+ -- Track where users signed up from (e.g., 'lastbrain', 'recipe', etc.)
184
+
185
+ -- Add the column
186
+ ALTER TABLE public.user_profil
187
+ ADD COLUMN IF NOT EXISTS signup_source TEXT DEFAULT 'lastbrain';
188
+
189
+
190
+
191
+ -- Create an index for stats queries
192
+ CREATE INDEX IF NOT EXISTS idx_user_profil_signup_source
193
+ ON public.user_profil(signup_source);
194
+
195
+ -- Create an index for date range queries
196
+ CREATE INDEX IF NOT EXISTS idx_user_profil_created_at_source
197
+ ON public.user_profil(signup_source, created_at DESC);