@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.
- package/dist/api/admin/signup-stats.d.ts +21 -0
- package/dist/api/admin/signup-stats.d.ts.map +1 -0
- package/dist/api/admin/signup-stats.js +75 -0
- package/dist/api/admin/users-by-source.d.ts +22 -0
- package/dist/api/admin/users-by-source.d.ts.map +1 -0
- package/dist/api/admin/users-by-source.js +56 -0
- package/dist/api/public/signup.d.ts +10 -0
- package/dist/api/public/signup.d.ts.map +1 -0
- package/dist/api/public/signup.js +71 -0
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +26 -0
- package/dist/components/AccountButton.d.ts.map +1 -1
- package/dist/index.d.ts +17 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -16
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/web/admin/signup-stats.d.ts +2 -0
- package/dist/web/admin/signup-stats.d.ts.map +1 -0
- package/dist/web/admin/signup-stats.js +50 -0
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +5 -1
- package/dist/web/admin/users/[id].js +1 -1
- package/dist/web/admin/users-by-signup-source.d.ts +2 -0
- package/dist/web/admin/users-by-signup-source.d.ts.map +1 -0
- package/dist/web/admin/users-by-signup-source.js +79 -0
- package/dist/web/auth/profile.js +18 -18
- package/dist/web/public/SignUpPage.d.ts.map +1 -1
- package/dist/web/public/SignUpPage.js +15 -23
- package/package.json +4 -4
- package/src/api/admin/signup-stats.ts +109 -0
- package/src/api/admin/users-by-source.ts +87 -0
- package/src/api/public/signup.ts +106 -0
- package/src/auth.build.config.ts +27 -0
- package/src/components/AccountButton.tsx +0 -1
- package/src/index.ts +17 -16
- package/src/server.ts +1 -0
- package/src/web/admin/signup-stats.tsx +304 -0
- package/src/web/admin/user-detail.tsx +17 -2
- package/src/web/admin/users/[id].tsx +1 -1
- package/src/web/admin/users-by-signup-source.tsx +262 -0
- package/src/web/admin/users.tsx +1 -1
- package/src/web/auth/profile.tsx +6 -6
- package/src/web/public/SignUpPage.tsx +16 -25
- package/supabase/migrations/20251112000000_user_init.sql +18 -1
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +10 -2
- package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +2 -1
- 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
|
+
}
|
package/dist/web/auth/profile.js
CHANGED
|
@@ -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
|
|
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 (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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":"
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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.
|
|
41
|
-
"react-dom": "^19.
|
|
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.
|
|
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
|
+
}
|
package/src/auth.build.config.ts
CHANGED
|
@@ -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",
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
// Client Components uniquement
|
|
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
|
|
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
|
|
15
|
-
export { NotificationButton } from "./components/NotificationButton
|
|
16
|
-
export { ThemeSwitcherButton } from "./components/ThemeSwitcherButton
|
|
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
|
|
20
|
-
export { Doc as AuthModuleDoc } from "./components/Doc
|
|
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
|
|
23
|
+
export { default as authBuildConfig } from "./auth.build.config";
|
package/src/server.ts
CHANGED