@lastbrain/module-auth 0.1.22 → 1.0.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 (49) 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/components/AccountButton.d.ts.map +1 -1
  13. package/dist/index.d.ts +17 -16
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +17 -16
  16. package/dist/server.d.ts +1 -0
  17. package/dist/server.d.ts.map +1 -1
  18. package/dist/server.js +1 -0
  19. package/dist/web/admin/signup-stats.d.ts +2 -0
  20. package/dist/web/admin/signup-stats.d.ts.map +1 -0
  21. package/dist/web/admin/signup-stats.js +50 -0
  22. package/dist/web/admin/user-detail.d.ts.map +1 -1
  23. package/dist/web/admin/user-detail.js +5 -1
  24. package/dist/web/admin/users/[id].js +1 -1
  25. package/dist/web/admin/users-by-signup-source.d.ts +2 -0
  26. package/dist/web/admin/users-by-signup-source.d.ts.map +1 -0
  27. package/dist/web/admin/users-by-signup-source.js +79 -0
  28. package/dist/web/auth/profile.js +18 -18
  29. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  30. package/dist/web/public/SignUpPage.js +15 -23
  31. package/package.json +4 -4
  32. package/src/api/admin/signup-stats.ts +109 -0
  33. package/src/api/admin/users-by-source.ts +87 -0
  34. package/src/api/public/signup.ts +106 -0
  35. package/src/auth.build.config.ts +27 -0
  36. package/src/components/AccountButton.tsx +0 -1
  37. package/src/index.ts +17 -16
  38. package/src/server.ts +1 -0
  39. package/src/web/admin/signup-stats.tsx +304 -0
  40. package/src/web/admin/user-detail.tsx +17 -2
  41. package/src/web/admin/users/[id].tsx +1 -1
  42. package/src/web/admin/users-by-signup-source.tsx +262 -0
  43. package/src/web/admin/users.tsx +1 -1
  44. package/src/web/auth/profile.tsx +6 -6
  45. package/src/web/public/SignUpPage.tsx +16 -25
  46. package/supabase/migrations/20251112000000_user_init.sql +18 -1
  47. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +10 -2
  48. package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +2 -1
  49. package/supabase/migrations-down/20251204000000_add_signup_source.sql +12 -0
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { Card, CardBody, CardHeader, Chip, Input, Pagination, Select, SelectItem, Spinner, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow, } from "@lastbrain/ui";
5
+ import { Search, Users } from "lucide-react";
6
+ export function UsersBySignupSourcePage() {
7
+ const [users, setUsers] = useState([]);
8
+ const [loading, setLoading] = useState(true);
9
+ const [error, setError] = useState(null);
10
+ const [pagination, setPagination] = useState({
11
+ page: 1,
12
+ limit: 25,
13
+ total: 0,
14
+ totalPages: 0,
15
+ });
16
+ const [source, setSource] = useState(""); // '' for all, 'lastbrain', 'recipe'
17
+ const [searchQuery, setSearchQuery] = useState("");
18
+ useEffect(() => {
19
+ fetchUsers(pagination.page, source, searchQuery);
20
+ }, []);
21
+ const fetchUsers = async (page, selectedSource, query) => {
22
+ try {
23
+ setLoading(true);
24
+ let url = `/api/admin/users-by-source?page=${page}&limit=25`;
25
+ if (selectedSource) {
26
+ url += `&source=${selectedSource}`;
27
+ }
28
+ const response = await fetch(url);
29
+ if (!response.ok) {
30
+ throw new Error("Erreur lors du chargement des utilisateurs");
31
+ }
32
+ const result = await response.json();
33
+ // Filter by search query if provided
34
+ let filteredUsers = result.data;
35
+ if (query) {
36
+ filteredUsers = filteredUsers.filter((user) => user.email.toLowerCase().includes(query.toLowerCase()) ||
37
+ user.name.toLowerCase().includes(query.toLowerCase()));
38
+ }
39
+ setUsers(filteredUsers);
40
+ setPagination(result.pagination);
41
+ }
42
+ catch (err) {
43
+ setError(err instanceof Error ? err.message : "Erreur lors du chargement");
44
+ }
45
+ finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+ const handleSourceChange = (value) => {
50
+ setSource(value);
51
+ fetchUsers(1, value, searchQuery);
52
+ };
53
+ const handleSearch = (e) => {
54
+ const query = e.target.value;
55
+ setSearchQuery(query);
56
+ fetchUsers(1, source, query);
57
+ };
58
+ const handlePageChange = (page) => {
59
+ fetchUsers(page, source, searchQuery);
60
+ };
61
+ const getSourceColor = (src) => {
62
+ return src.toLowerCase() === "recipe" ? "success" : "secondary";
63
+ };
64
+ if (error) {
65
+ return (_jsx("div", { className: "p-6", children: _jsx(Card, { className: "border border-danger-200 bg-danger-50/50", children: _jsx(CardBody, { children: _jsx("p", { className: "text-danger-600", children: error }) }) }) }));
66
+ }
67
+ return (_jsxs("div", { className: "space-y-6 p-6", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(Users, { size: 28, className: "text-primary-600" }), _jsx("h1", { className: "text-3xl font-bold", children: "Utilisateurs par source d'inscription" })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(Input, { placeholder: "Rechercher par email ou nom...", value: searchQuery, onChange: handleSearch, startContent: _jsx(Search, { size: 16 }), isClearable: true, onClear: () => {
68
+ setSearchQuery("");
69
+ fetchUsers(1, source, "");
70
+ } }), _jsxs(Select, { label: "Filtrer par source", selectedKeys: [source], onChange: (e) => handleSourceChange(e.target.value), children: [_jsx(SelectItem, { children: "Toutes les sources" }, ""), _jsx(SelectItem, { children: "LastBrain" }, "lastbrain"), _jsx(SelectItem, { children: "Recipe" }, "recipe")] })] }), _jsxs("div", { className: "text-sm text-default-600", children: ["Affichage de", " ", _jsxs("span", { className: "font-semibold", children: [(pagination.page - 1) * pagination.limit + 1, "-", Math.min(pagination.page * pagination.limit, pagination.total)] }), " ", "sur ", _jsx("span", { className: "font-semibold", children: pagination.total }), " ", "utilisateurs"] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Liste des utilisateurs" }) }), _jsx(CardBody, { children: loading ? (_jsx("div", { className: "flex justify-center py-8", children: _jsx(Spinner, { size: "lg", label: "Chargement..." }) })) : (_jsxs(_Fragment, { children: [_jsxs(Table, { "aria-label": "Tableau des utilisateurs", children: [_jsxs(TableHeader, { children: [_jsx(TableColumn, { children: "Nom" }), _jsx(TableColumn, { children: "Email" }), _jsx(TableColumn, { children: "Source" }), _jsx(TableColumn, { children: "Date d'inscription" })] }), _jsx(TableBody, { children: users.length > 0 ? (users.map((user) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx("span", { className: "font-medium", children: user.name }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-sm text-default-600", children: user.email }) }), _jsx(TableCell, { children: _jsx(Chip, { size: "sm", color: getSourceColor(user.signup_source), variant: "flat", children: user.signup_source.toLowerCase() === "recipe"
71
+ ? "🍳 Recipe"
72
+ : "🧠 LastBrain" }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-sm text-default-500", children: new Date(user.created_at).toLocaleDateString("fr-FR", {
73
+ year: "2-digit",
74
+ month: "2-digit",
75
+ day: "2-digit",
76
+ hour: "2-digit",
77
+ minute: "2-digit",
78
+ }) }) })] }, user.id)))) : (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center py-8", children: _jsx("p", { className: "text-default-500", children: "Aucun utilisateur trouv\u00E9" }) }) })) })] }), pagination.totalPages > 1 && (_jsx("div", { className: "flex justify-center mt-6", children: _jsx(Pagination, { total: pagination.totalPages, page: pagination.page, onChange: handlePageChange, showControls: true }) }))] })) })] })] }));
79
+ }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import { Card, CardBody, CardHeader, Input, Textarea, Button, Spinner, Divider, addToast, AvatarUploader, } from "@lastbrain/ui";
5
5
  import { Save, User } from "lucide-react";
6
- import { uploadFile, deleteFilesWithPrefix } from "../../api/storage.js";
6
+ import { uploadFile, deleteFilesWithPrefix } from "../../api/storage";
7
7
  import { supabaseBrowserClient } from "@lastbrain/core";
8
8
  export function ProfilePage() {
9
9
  const [profile, setProfile] = useState({});
@@ -174,21 +174,21 @@ export function ProfilePage() {
174
174
  if (isLoading) {
175
175
  return (_jsx("div", { className: "flex justify-center items-center min-h-[400px]", children: _jsx(Spinner, { size: "lg", label: "Loading profile..." }) }));
176
176
  }
177
- return (_jsxs("div", { className: "pt-12 pb-12 max-w-4xl mx-auto px-4", children: [_jsxs("div", { className: "flex items-center gap-2 mb-8", children: [_jsx(User, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "Edit Profile" })] }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs("div", { className: "space-y-6", children: [_jsx("div", { className: "flex justify-center", children: _jsx(AvatarUploader, { userId: currentUser?.id, bucket: "avatar", shape: "circle", onUpload: handleAvatarUpload, onDelete: handleAvatarDelete, initialAvatarPath: currentUser?.user_metadata?.avatar ||
178
- profile.avatar_url ||
179
- null, initialAvatarSizes: (() => {
180
- const sizes = currentUser?.user_metadata
181
- ?.avatar_sizes;
182
- if (!sizes)
183
- return null;
184
- return {
185
- small: sizes.small ?? null,
186
- medium: sizes.medium ?? null,
187
- large: sizes.large ?? null,
188
- };
189
- })(), onUploaded: (urls) => {
190
- setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
191
- }, onDeleted: () => {
192
- setProfile((prev) => ({ ...prev, avatar_url: "" }));
193
- } }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Personal Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "First Name", placeholder: "Enter your first name", value: profile.first_name || "", onChange: (e) => handleChange("first_name", e.target.value) }), _jsx(Input, { label: "Last Name", placeholder: "Enter your last name", value: profile.last_name || "", onChange: (e) => handleChange("last_name", e.target.value) }), _jsx(Input, { label: "Phone", placeholder: "Enter your phone number", type: "tel", value: profile.phone || "", onChange: (e) => handleChange("phone", e.target.value), className: "md:col-span-2" }), _jsx(Textarea, { label: "Bio", placeholder: "Tell us about yourself", value: profile.bio || "", onChange: (e) => handleChange("bio", e.target.value), minRows: 3, className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Professional Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "Company", placeholder: "Enter your company name", value: profile.company || "", onChange: (e) => handleChange("company", e.target.value) }), _jsx(Input, { label: "Website", placeholder: "https://example.com", type: "url", value: profile.website || "", onChange: (e) => handleChange("website", e.target.value) }), _jsx(Input, { label: "Location", placeholder: "City, Country", value: profile.location || "", onChange: (e) => handleChange("location", e.target.value), className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Preferences" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "Language", placeholder: "en, fr, es...", value: profile.language || "", onChange: (e) => handleChange("language", e.target.value) }), _jsx(Input, { label: "Timezone", placeholder: "Europe/Paris, America/New_York...", value: profile.timezone || "", onChange: (e) => handleChange("timezone", e.target.value) })] }) })] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { type: "button", variant: "flat", onPress: () => fetchProfile(), isDisabled: isSaving, children: "Cancel" }), _jsx(Button, { type: "submit", color: "primary", isLoading: isSaving, startContent: !isSaving && _jsx(Save, { className: "w-4 h-4" }), children: isSaving ? "Saving..." : "Save Changes" })] })] }) })] }));
177
+ return (_jsx("div", { className: "pt-12 pb-12 max-w-4xl mx-auto px-4", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs("div", { className: "space-y-6", children: [_jsx("div", { className: "flex justify-center", children: _jsx(AvatarUploader, { userId: currentUser?.id, bucket: "avatar", shape: "circle", onUpload: handleAvatarUpload, onDelete: handleAvatarDelete, initialAvatarPath: currentUser?.user_metadata?.avatar ||
178
+ profile.avatar_url ||
179
+ null, initialAvatarSizes: (() => {
180
+ const sizes = currentUser?.user_metadata
181
+ ?.avatar_sizes;
182
+ if (!sizes)
183
+ return null;
184
+ return {
185
+ small: sizes.small ?? null,
186
+ medium: sizes.medium ?? null,
187
+ large: sizes.large ?? null,
188
+ };
189
+ })(), onUploaded: (urls) => {
190
+ setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
191
+ }, onDeleted: () => {
192
+ setProfile((prev) => ({ ...prev, avatar_url: "" }));
193
+ } }) }), _jsxs("div", { className: "flex items-center gap-2 mb-4", children: [_jsx(User, { className: "w-8 h-8" }), _jsx("h1", { className: "text-3xl font-bold", children: "Edit Profile" })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Personal Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "First Name", placeholder: "Enter your first name", value: profile.first_name || "", onChange: (e) => handleChange("first_name", e.target.value) }), _jsx(Input, { label: "Last Name", placeholder: "Enter your last name", value: profile.last_name || "", onChange: (e) => handleChange("last_name", e.target.value) }), _jsx(Input, { label: "Phone", placeholder: "Enter your phone number", type: "tel", value: profile.phone || "", onChange: (e) => handleChange("phone", e.target.value), className: "md:col-span-2" }), _jsx(Textarea, { label: "Bio", placeholder: "Tell us about yourself", value: profile.bio || "", onChange: (e) => handleChange("bio", e.target.value), minRows: 3, className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Professional Information" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "Company", placeholder: "Enter your company name", value: profile.company || "", onChange: (e) => handleChange("company", e.target.value) }), _jsx(Input, { label: "Website", placeholder: "https://example.com", type: "url", value: profile.website || "", onChange: (e) => handleChange("website", e.target.value) }), _jsx(Input, { label: "Location", placeholder: "City, Country", value: profile.location || "", onChange: (e) => handleChange("location", e.target.value), className: "md:col-span-2" })] }) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx("h3", { className: "text-lg font-semibold", children: "Preferences" }) }), _jsx(Divider, {}), _jsx(CardBody, { children: _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [_jsx(Input, { label: "Language", placeholder: "en, fr, es...", value: profile.language || "", onChange: (e) => handleChange("language", e.target.value) }), _jsx(Input, { label: "Timezone", placeholder: "Europe/Paris, America/New_York...", value: profile.timezone || "", onChange: (e) => handleChange("timezone", e.target.value) })] }) })] }), _jsxs("div", { className: "flex justify-end gap-3", children: [_jsx(Button, { type: "button", variant: "flat", onPress: () => fetchProfile(), isDisabled: isSaving, children: "Cancel" }), _jsx(Button, { type: "submit", color: "primary", isLoading: isSaving, startContent: !isSaving && _jsx(Save, { className: "w-4 h-4" }), children: isSaving ? "Saving..." : "Save Changes" })] })] }) }) }));
194
194
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SignUpPage.d.ts","sourceRoot":"","sources":["../../../src/web/public/SignUpPage.tsx"],"names":[],"mappings":"AA8RA,wBAAgB,UAAU,4CAMzB"}
1
+ {"version":3,"file":"SignUpPage.d.ts","sourceRoot":"","sources":["../../../src/web/public/SignUpPage.tsx"],"names":[],"mappings":"AAqRA,wBAAgB,UAAU,4CAMzB"}
@@ -4,7 +4,6 @@ import { Button, Card, CardBody, Input, Link, Chip, addToast, } from "@lastbrain
4
4
  import { useRouter, useSearchParams } from "next/navigation";
5
5
  import { Suspense, useState } from "react";
6
6
  import { Mail, Lock, User, ArrowRight, Sparkles, CheckCircle2, } from "lucide-react";
7
- import { supabaseBrowserClient } from "@lastbrain/core";
8
7
  function SignUpForm() {
9
8
  const router = useRouter();
10
9
  const searchParams = useSearchParams();
@@ -16,7 +15,7 @@ function SignUpForm() {
16
15
  const [error, setError] = useState(null);
17
16
  const [success, setSuccess] = useState(null);
18
17
  // Récupérer le paramètre redirect
19
- const redirectUrl = searchParams.get("redirect");
18
+ const redirectUrl = searchParams?.get("redirect") || "";
20
19
  const handleSubmit = async (event) => {
21
20
  event.preventDefault();
22
21
  setError(null);
@@ -31,35 +30,28 @@ function SignUpForm() {
31
30
  }
32
31
  setLoading(true);
33
32
  try {
34
- const { data, error: signUpError } = await supabaseBrowserClient.auth.signUp({
35
- email,
36
- password,
37
- options: {
38
- emailRedirectTo: `${window.location.origin}/api/auth/callback${redirectUrl ? `?next=${encodeURIComponent(redirectUrl)}` : ""}`,
39
- data: {
40
- full_name: fullName,
41
- },
33
+ // Appeler la nouvelle route API signup (signupSource sera déterminé côté serveur via VERCEL_URL)
34
+ const response = await fetch("/api/auth/signup", {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
42
38
  },
39
+ body: JSON.stringify({
40
+ email,
41
+ password,
42
+ fullName,
43
+ }),
43
44
  });
44
- if (signUpError) {
45
- setError(signUpError.message);
45
+ const result = await response.json();
46
+ if (!response.ok) {
47
+ setError(result.error || "Erreur lors de l'inscription");
46
48
  return;
47
49
  }
48
50
  // Si la confirmation par email est requise
49
- if (data.user && !data.session) {
51
+ if (result.data.user && !result.data.session) {
50
52
  setSuccess("Compte créé avec succès ! Veuillez vérifier votre email pour confirmer votre compte.");
51
53
  return;
52
54
  }
53
- // Si l'utilisateur est directement connecté (confirmation email désactivée)
54
- if (data.session) {
55
- if (redirectUrl) {
56
- window.location.href = redirectUrl;
57
- }
58
- else {
59
- window.location.href = "/auth/dashboard";
60
- }
61
- return;
62
- }
63
55
  setSuccess("Compte créé. Vous pouvez désormais vous connecter.");
64
56
  setTimeout(() => {
65
57
  if (redirectUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lastbrain/module-auth",
3
- "version": "0.1.22",
3
+ "version": "1.0.1",
4
4
  "description": "Module d'authentification complet pour LastBrain avec Supabase",
5
5
  "private": false,
6
6
  "type": "module",
@@ -37,14 +37,14 @@
37
37
  "@lastbrain/module-ai": "^0.1.0",
38
38
  "@supabase/supabase-js": "^2.86.0",
39
39
  "lucide-react": "^0.554.0",
40
- "react": "^19.0.0",
41
- "react-dom": "^19.0.0"
40
+ "react": "^19.2.1",
41
+ "react-dom": "^19.2.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "next": ">=15.0.0"
45
45
  },
46
46
  "devDependencies": {
47
- "next": "^16.0.4",
47
+ "next": "^16.0.7",
48
48
  "typescript": "^5.4.0"
49
49
  },
50
50
  "exports": {
@@ -0,0 +1,109 @@
1
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ interface SignupStats {
5
+ total: number;
6
+ bySource: {
7
+ lastbrain: number;
8
+ recipe: number;
9
+ };
10
+ byDate: Array<{
11
+ date: string;
12
+ lastbrain: number;
13
+ recipe: number;
14
+ total: number;
15
+ }>;
16
+ }
17
+
18
+ export async function GET(_request: NextRequest) {
19
+ try {
20
+ const supabase = await getSupabaseServiceClient();
21
+
22
+ // Get total signups by source
23
+ const { data: signupData, error: signupError } = await supabase
24
+ .from("user_profil")
25
+ .select("signup_source", { count: "exact" })
26
+ .order("created_at", { ascending: false });
27
+
28
+ if (signupError) {
29
+ return NextResponse.json({ error: signupError.message }, { status: 400 });
30
+ }
31
+
32
+ // Get signups by date and source (last 30 days)
33
+ const thirtyDaysAgo = new Date();
34
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
35
+
36
+ const { data: dateData, error: dateError } = await supabase
37
+ .from("user_profil")
38
+ .select("created_at, signup_source")
39
+ .gte("created_at", thirtyDaysAgo.toISOString())
40
+ .order("created_at", { ascending: false });
41
+
42
+ if (dateError) {
43
+ return NextResponse.json({ error: dateError.message }, { status: 400 });
44
+ }
45
+
46
+ // Process stats
47
+ const stats = processSignupStats(signupData, dateData);
48
+
49
+ return NextResponse.json({ data: stats }, { status: 200 });
50
+ } catch (error) {
51
+ console.error("Error fetching signup stats:", error);
52
+ return NextResponse.json(
53
+ { error: "Erreur interne du serveur" },
54
+ { status: 500 }
55
+ );
56
+ }
57
+ }
58
+
59
+ function processSignupStats(
60
+ allData: Array<{ signup_source: string | null }>,
61
+ dateData: Array<{ created_at: string; signup_source: string | null }>
62
+ ): SignupStats {
63
+ // Count by source
64
+ const bySource = {
65
+ lastbrain: 0,
66
+ recipe: 0,
67
+ };
68
+
69
+ for (const record of allData) {
70
+ const source = (record.signup_source || "lastbrain").toLowerCase();
71
+ if (source === "lastbrain") bySource.lastbrain++;
72
+ else if (source === "recipe") bySource.recipe++;
73
+ }
74
+
75
+ // Count by date
76
+ const byDate: Record<
77
+ string,
78
+ { lastbrain: number; recipe: number; total: number }
79
+ > = {};
80
+
81
+ for (const record of dateData) {
82
+ const date = new Date(record.created_at).toISOString().split("T")[0];
83
+ if (!byDate[date]) {
84
+ byDate[date] = { lastbrain: 0, recipe: 0, total: 0 };
85
+ }
86
+
87
+ const source = (record.signup_source || "lastbrain").toLowerCase();
88
+ if (source === "lastbrain") {
89
+ byDate[date].lastbrain++;
90
+ } else if (source === "recipe") {
91
+ byDate[date].recipe++;
92
+ }
93
+ byDate[date].total++;
94
+ }
95
+
96
+ // Sort dates
97
+ const sortedByDate = Object.entries(byDate)
98
+ .sort(([dateA], [dateB]) => dateB.localeCompare(dateA))
99
+ .map(([date, stats]) => ({
100
+ date,
101
+ ...stats,
102
+ }));
103
+
104
+ return {
105
+ total: allData.length,
106
+ bySource,
107
+ byDate: sortedByDate,
108
+ };
109
+ }
@@ -0,0 +1,87 @@
1
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ interface UserSignupData {
5
+ id: string;
6
+ email: string;
7
+ created_at: string;
8
+ signup_source: string | null;
9
+ first_name: string | null;
10
+ last_name: string | null;
11
+ }
12
+
13
+ export async function GET(request: NextRequest) {
14
+ try {
15
+ const supabase = await getSupabaseServiceClient();
16
+
17
+ // Get query params for filtering
18
+ const url = new URL(request.url);
19
+ const source = url.searchParams.get("source"); // 'lastbrain' or 'recipe'
20
+ const page = parseInt(url.searchParams.get("page") || "1");
21
+ const limit = parseInt(url.searchParams.get("limit") || "50");
22
+
23
+ const offset = (page - 1) * limit;
24
+
25
+ let query = supabase.from("user_profil").select(
26
+ `
27
+ id,
28
+ owner_id,
29
+ first_name,
30
+ last_name,
31
+ signup_source,
32
+ created_at
33
+ `,
34
+ { count: "exact" }
35
+ );
36
+
37
+ // Filter by source if specified
38
+ if (source) {
39
+ query = query.eq("signup_source", source);
40
+ }
41
+
42
+ const { data, count, error } = await query
43
+ .order("created_at", { ascending: false })
44
+ .range(offset, offset + limit - 1);
45
+
46
+ if (error) {
47
+ return NextResponse.json({ error: error.message }, { status: 400 });
48
+ }
49
+
50
+ // Fetch email from auth.users for each profile
51
+ const usersWithEmails: UserSignupData[] = await Promise.all(
52
+ (data || []).map(async (profile: any) => {
53
+ const { data: authUser } = await supabase.auth.admin.getUserById(
54
+ profile.owner_id
55
+ );
56
+
57
+ return {
58
+ id: profile.id,
59
+ email: authUser?.user?.email || "Unknown",
60
+ created_at: profile.created_at,
61
+ signup_source: profile.signup_source || "lastbrain",
62
+ first_name: profile.first_name,
63
+ last_name: profile.last_name,
64
+ };
65
+ })
66
+ );
67
+
68
+ return NextResponse.json(
69
+ {
70
+ data: usersWithEmails,
71
+ pagination: {
72
+ page,
73
+ limit,
74
+ total: count || 0,
75
+ totalPages: Math.ceil((count || 0) / limit),
76
+ },
77
+ },
78
+ { status: 200 }
79
+ );
80
+ } catch (error) {
81
+ console.error("Error fetching users by signup source:", error);
82
+ return NextResponse.json(
83
+ { error: "Erreur interne du serveur" },
84
+ { status: 500 }
85
+ );
86
+ }
87
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ getSupabaseServerClient,
3
+ getSupabaseServiceClient,
4
+ } from "@lastbrain/core/server";
5
+ import { NextRequest, NextResponse } from "next/server";
6
+
7
+ interface SignUpRequest {
8
+ email: string;
9
+ password: string;
10
+ fullName: string;
11
+ signupSource?: string; // 'lastbrain' or 'recipe'
12
+ }
13
+
14
+ export async function POST(request: NextRequest) {
15
+ try {
16
+ const body: SignUpRequest = await request.json();
17
+ const defaultSource = process.env.APP_NAME || "undefined";
18
+ console.log("🚀 ~ POST ~ defaultSource:", defaultSource);
19
+ const { email, password, fullName, signupSource = defaultSource } = body;
20
+
21
+ // Validate required fields
22
+ if (!email || !password) {
23
+ return NextResponse.json(
24
+ { error: "Email et mot de passe requis." },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ // Get Supabase client for authentication
30
+ const supabase = await getSupabaseServerClient();
31
+
32
+ // Sign up the user
33
+ const { data: authData, error: signUpError } = await supabase.auth.signUp({
34
+ email,
35
+ password,
36
+ options: {
37
+ emailRedirectTo: `${request.nextUrl.origin}/api/auth/callback`,
38
+ data: {
39
+ full_name: fullName,
40
+ signup_source: signupSource,
41
+ },
42
+ },
43
+ });
44
+
45
+ if (signUpError) {
46
+ return NextResponse.json({ error: signUpError.message }, { status: 400 });
47
+ }
48
+
49
+ if (!authData.user) {
50
+ return NextResponse.json(
51
+ { error: "Erreur lors de la création du compte" },
52
+ { status: 500 }
53
+ );
54
+ }
55
+
56
+ // Create user profile with signup_source
57
+ const serviceClient = await getSupabaseServiceClient();
58
+
59
+ // Check if profile already exists
60
+ const { data: existingProfile } = await serviceClient
61
+ .from("user_profil")
62
+ .select("owner_id")
63
+ .eq("owner_id", authData.user.id)
64
+ .single();
65
+
66
+ // Only create profile if it doesn't exist
67
+ if (!existingProfile) {
68
+ const { error: profileError } = await serviceClient
69
+ .from("user_profil")
70
+ .insert({
71
+ owner_id: authData.user.id,
72
+ first_name: fullName?.split(" ")[0] || "",
73
+ last_name: fullName?.split(" ").slice(1).join(" ") || "",
74
+ signup_source: signupSource,
75
+ preferences: {},
76
+ });
77
+
78
+ if (profileError) {
79
+ console.error("Error creating user profile:", profileError);
80
+ return NextResponse.json(
81
+ {
82
+ error: "Compte créé mais profil non configuré",
83
+ message: profileError.message,
84
+ },
85
+ { status: 500 }
86
+ );
87
+ }
88
+ }
89
+
90
+ return NextResponse.json(
91
+ {
92
+ data: {
93
+ user: authData.user,
94
+ message: "Compte créé avec succès",
95
+ },
96
+ },
97
+ { status: 201 }
98
+ );
99
+ } catch (error) {
100
+ console.error("Signup error:", error);
101
+ return NextResponse.json(
102
+ { error: "Erreur interne du serveur" },
103
+ { status: 500 }
104
+ );
105
+ }
106
+ }
@@ -48,6 +48,11 @@ const authBuildConfig: ModuleBuildConfig = {
48
48
  path: "/users/[id]",
49
49
  componentExport: "UserPage",
50
50
  },
51
+ {
52
+ section: "admin",
53
+ path: "/signup-stats",
54
+ componentExport: "SignupStatsPage",
55
+ },
51
56
  ],
52
57
  apis: [
53
58
  {
@@ -57,6 +62,13 @@ const authBuildConfig: ModuleBuildConfig = {
57
62
  entryPoint: "api/public/signin",
58
63
  authRequired: false,
59
64
  },
65
+ {
66
+ method: "POST",
67
+ path: "/api/auth/signup",
68
+ handlerExport: "POST",
69
+ entryPoint: "api/public/signup",
70
+ authRequired: false,
71
+ },
60
72
  {
61
73
  method: "GET",
62
74
  path: "/api/auth/profile",
@@ -106,6 +118,13 @@ const authBuildConfig: ModuleBuildConfig = {
106
118
  entryPoint: "api/admin/users/[id]/notifications",
107
119
  authRequired: true,
108
120
  },
121
+ {
122
+ method: "GET",
123
+ path: "/api/admin/signup-stats",
124
+ handlerExport: "GET",
125
+ entryPoint: "api/admin/signup-stats",
126
+ authRequired: true,
127
+ },
109
128
  ],
110
129
  migrations: {
111
130
  enabled: true,
@@ -162,6 +181,14 @@ const authBuildConfig: ModuleBuildConfig = {
162
181
  shortcut: "cmd+shift+u",
163
182
  shortcutDisplay: "⌘⇧U",
164
183
  },
184
+ {
185
+ title: "Statistiques d'inscriptions",
186
+ description: "Suivez les inscriptions par source",
187
+ icon: "UserStar",
188
+ path: "/admin/auth/signup-stats",
189
+ order: 2,
190
+ },
191
+
165
192
  {
166
193
  title: "Notifications",
167
194
  description: "Vos notifications",
@@ -10,7 +10,6 @@ import {
10
10
  } from "@lastbrain/ui";
11
11
  import * as LucideIcons from "lucide-react";
12
12
  import type { User } from "@supabase/supabase-js";
13
- import { use } from "react";
14
13
 
15
14
  interface AccountButtonProps {
16
15
  item: {
package/src/index.ts CHANGED
@@ -1,22 +1,23 @@
1
1
  // Client Components uniquement
2
- export { SignInPage } from "./web/public/SignInPage.js";
3
- export { SignUpPage } from "./web/public/SignUpPage.js";
4
- export { ResetPassword } from "./web/public/ResetPassword.js";
5
- export { DashboardPage } from "./web/auth/dashboard.js";
6
- export { FolderPage } from "./web/auth/folder.js";
7
- export { ProfilePage } from "./web/auth/profile.js";
8
- export { ReglagePage } from "./web/auth/reglage.js";
9
- export { AdminUsersPage } from "./web/admin/users.js";
10
- export { default as UserPage } from "./web/admin/users/[id].js";
11
- export { UserDetailPage } from "./web/admin/user-detail.js";
2
+ export { SignInPage } from "./web/public/SignInPage";
3
+ export { SignUpPage } from "./web/public/SignUpPage";
4
+ export { ResetPassword } from "./web/public/ResetPassword";
5
+ export { DashboardPage } from "./web/auth/dashboard";
6
+ export { FolderPage } from "./web/auth/folder";
7
+ export { ProfilePage } from "./web/auth/profile";
8
+ export { ReglagePage } from "./web/auth/reglage";
9
+ export { AdminUsersPage } from "./web/admin/users";
10
+ export { default as UserPage } from "./web/admin/users/[id]";
11
+ export { UserDetailPage } from "./web/admin/user-detail";
12
+ export { SignupStatsPage } from "./web/admin/signup-stats";
12
13
 
13
14
  // Header Components
14
- export { AccountButton } from "./components/AccountButton.js";
15
- export { NotificationButton } from "./components/NotificationButton.js";
16
- export { ThemeSwitcherButton } from "./components/ThemeSwitcherButton.js";
15
+ export { AccountButton } from "./components/AccountButton";
16
+ export { NotificationButton } from "./components/NotificationButton";
17
+ export { ThemeSwitcherButton } from "./components/ThemeSwitcherButton";
17
18
 
18
19
  // Documentation
19
- export { Doc } from "./components/Doc.js";
20
- export { Doc as AuthModuleDoc } from "./components/Doc.js";
20
+ export { Doc } from "./components/Doc";
21
+ export { Doc as AuthModuleDoc } from "./components/Doc";
21
22
  // Configuration de build (utilisée par les scripts)
22
- export { default as authBuildConfig } from "./auth.build.config.js";
23
+ export { default as authBuildConfig } from "./auth.build.config";
package/src/server.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  // Server-only exports (Route Handlers, Server Actions, etc.)
2
2
  export { POST as signInApi } from "./api/public/signin.js";
3
+ export { POST as signUpApi } from "./api/public/signup.js";