@lastbrain/module-auth 0.1.1 → 0.1.3

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 (41) hide show
  1. package/README.md +504 -0
  2. package/dist/api/admin/users.d.ts +36 -0
  3. package/dist/api/admin/users.d.ts.map +1 -0
  4. package/dist/api/admin/users.js +90 -0
  5. package/dist/api/auth/me.d.ts +17 -0
  6. package/dist/api/auth/me.d.ts.map +1 -0
  7. package/dist/api/auth/me.js +34 -0
  8. package/dist/api/auth/profile.d.ts +32 -0
  9. package/dist/api/auth/profile.d.ts.map +1 -0
  10. package/dist/api/auth/profile.js +108 -0
  11. package/dist/api/storage.d.ts +13 -0
  12. package/dist/api/storage.d.ts.map +1 -0
  13. package/dist/api/storage.js +47 -0
  14. package/dist/auth.build.config.d.ts.map +1 -1
  15. package/dist/auth.build.config.js +21 -0
  16. package/dist/web/admin/users.d.ts.map +1 -1
  17. package/dist/web/admin/users.js +87 -2
  18. package/dist/web/auth/dashboard.d.ts +1 -1
  19. package/dist/web/auth/dashboard.d.ts.map +1 -1
  20. package/dist/web/auth/dashboard.js +42 -2
  21. package/dist/web/auth/profile.d.ts.map +1 -1
  22. package/dist/web/auth/profile.js +152 -2
  23. package/dist/web/auth/reglage.d.ts.map +1 -1
  24. package/dist/web/auth/reglage.js +98 -2
  25. package/package.json +5 -4
  26. package/src/api/admin/users.ts +124 -0
  27. package/src/api/auth/me.ts +48 -0
  28. package/src/api/auth/profile.ts +156 -0
  29. package/src/api/public/signin.ts +31 -0
  30. package/src/api/storage.ts +63 -0
  31. package/src/auth.build.config.ts +137 -0
  32. package/src/components/Doc.tsx +310 -0
  33. package/src/index.ts +12 -0
  34. package/src/server.ts +2 -0
  35. package/src/web/admin/users.tsx +266 -0
  36. package/src/web/auth/dashboard.tsx +202 -0
  37. package/src/web/auth/profile.tsx +381 -0
  38. package/src/web/auth/reglage.tsx +304 -0
  39. package/src/web/public/ResetPassword.tsx +3 -0
  40. package/src/web/public/SignInPage.tsx +255 -0
  41. package/src/web/public/SignUpPage.tsx +293 -0
@@ -0,0 +1,304 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ Card,
6
+ CardBody,
7
+ CardHeader,
8
+ Switch,
9
+ Button,
10
+ Spinner,
11
+ Divider,
12
+ Select,
13
+ SelectItem,
14
+ addToast,
15
+ } from "@lastbrain/ui";
16
+ import { Settings, Save } from "lucide-react";
17
+
18
+ interface UserPreferences {
19
+ email_notifications?: boolean;
20
+ push_notifications?: boolean;
21
+ marketing_emails?: boolean;
22
+ theme?: string;
23
+ language?: string;
24
+ timezone?: string;
25
+ }
26
+
27
+ interface ProfileData {
28
+ language?: string;
29
+ timezone?: string;
30
+ preferences?: UserPreferences;
31
+ }
32
+
33
+ export function ReglagePage() {
34
+ const [preferences, setPreferences] = useState<UserPreferences>({
35
+ email_notifications: true,
36
+ push_notifications: false,
37
+ marketing_emails: false,
38
+ theme: "system",
39
+ language: "en",
40
+ timezone: "UTC",
41
+ });
42
+ const [isLoading, setIsLoading] = useState(true);
43
+ const [isSaving, setIsSaving] = useState(false);
44
+
45
+ useEffect(() => {
46
+ fetchSettings();
47
+ }, []);
48
+
49
+ const fetchSettings = async () => {
50
+ try {
51
+ setIsLoading(true);
52
+ const response = await fetch("/api/auth/profile");
53
+
54
+ if (!response.ok) {
55
+ throw new Error("Failed to fetch settings");
56
+ }
57
+
58
+ const result = await response.json();
59
+ if (result.data) {
60
+ const profile: ProfileData = result.data;
61
+ setPreferences((prev) => ({
62
+ ...prev,
63
+ language: profile.language || prev.language,
64
+ timezone: profile.timezone || prev.timezone,
65
+ ...(profile.preferences || {}),
66
+ }));
67
+ }
68
+ } catch (err) {
69
+ console.error("Error loading settings:", err);
70
+ addToast({
71
+ title: "Error",
72
+ description: "Failed to load settings",
73
+ color: "danger",
74
+ });
75
+ } finally {
76
+ setIsLoading(false);
77
+ }
78
+ };
79
+
80
+ const handleSave = async () => {
81
+ setIsSaving(true);
82
+
83
+ try {
84
+ const response = await fetch("/api/auth/profile", {
85
+ method: "PUT",
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ },
89
+ body: JSON.stringify({
90
+ language: preferences.language,
91
+ timezone: preferences.timezone,
92
+ preferences: {
93
+ email_notifications: preferences.email_notifications,
94
+ push_notifications: preferences.push_notifications,
95
+ marketing_emails: preferences.marketing_emails,
96
+ theme: preferences.theme,
97
+ },
98
+ }),
99
+ });
100
+
101
+ if (!response.ok) {
102
+ throw new Error("Failed to update settings");
103
+ }
104
+
105
+ addToast({
106
+ title: "Success",
107
+ description: "Settings updated successfully",
108
+ color: "success",
109
+ });
110
+ } catch (err) {
111
+ console.error("Error updating settings:", err);
112
+ addToast({
113
+ title: "Error",
114
+ description: "Failed to update settings",
115
+ color: "danger",
116
+ });
117
+ } finally {
118
+ setIsSaving(false);
119
+ }
120
+ };
121
+
122
+ const handleToggle = (key: keyof UserPreferences, value: boolean) => {
123
+ setPreferences((prev) => ({ ...prev, [key]: value }));
124
+ };
125
+
126
+ const handleSelect = (key: keyof UserPreferences, value: string) => {
127
+ setPreferences((prev) => ({ ...prev, [key]: value }));
128
+ };
129
+
130
+ if (isLoading) {
131
+ return (
132
+ <div className="flex justify-center items-center min-h-[400px]">
133
+ <Spinner size="lg" label="Loading settings..." />
134
+ </div>
135
+ );
136
+ }
137
+
138
+ return (
139
+ <div className="pt-12 pb-12 max-w-4xl mx-auto px-4">
140
+ <div className="flex items-center gap-2 mb-8">
141
+ <Settings className="w-8 h-8" />
142
+ <h1 className="text-3xl font-bold">Account Settings</h1>
143
+ </div>
144
+
145
+ <div className="space-y-6">
146
+ {/* Notifications */}
147
+ <Card>
148
+ <CardHeader>
149
+ <h3 className="text-lg font-semibold">Notifications</h3>
150
+ </CardHeader>
151
+ <Divider />
152
+ <CardBody>
153
+ <div className="space-y-4">
154
+ <div className="flex justify-between items-center">
155
+ <div>
156
+ <p className="font-medium">Email Notifications</p>
157
+ <p className="text-small text-default-500">
158
+ Receive email notifications for important updates
159
+ </p>
160
+ </div>
161
+ <Switch
162
+ isSelected={preferences.email_notifications}
163
+ onValueChange={(value) =>
164
+ handleToggle("email_notifications", value)
165
+ }
166
+ />
167
+ </div>
168
+ <Divider />
169
+ <div className="flex justify-between items-center">
170
+ <div>
171
+ <p className="font-medium">Push Notifications</p>
172
+ <p className="text-small text-default-500">
173
+ Receive push notifications in your browser
174
+ </p>
175
+ </div>
176
+ <Switch
177
+ isSelected={preferences.push_notifications}
178
+ onValueChange={(value) =>
179
+ handleToggle("push_notifications", value)
180
+ }
181
+ />
182
+ </div>
183
+ <Divider />
184
+ <div className="flex justify-between items-center">
185
+ <div>
186
+ <p className="font-medium">Marketing Emails</p>
187
+ <p className="text-small text-default-500">
188
+ Receive emails about new features and updates
189
+ </p>
190
+ </div>
191
+ <Switch
192
+ isSelected={preferences.marketing_emails}
193
+ onValueChange={(value) =>
194
+ handleToggle("marketing_emails", value)
195
+ }
196
+ />
197
+ </div>
198
+ </div>
199
+ </CardBody>
200
+ </Card>
201
+
202
+ {/* Appearance */}
203
+ <Card>
204
+ <CardHeader>
205
+ <h3 className="text-lg font-semibold">Appearance</h3>
206
+ </CardHeader>
207
+ <Divider />
208
+ <CardBody>
209
+ <Select
210
+ label="Theme"
211
+ placeholder="Select a theme"
212
+ selectedKeys={preferences.theme ? [preferences.theme] : []}
213
+ onChange={(e) => handleSelect("theme", e.target.value)}
214
+ >
215
+ <SelectItem key="light">
216
+ Light
217
+ </SelectItem>
218
+ <SelectItem key="dark">
219
+ Dark
220
+ </SelectItem>
221
+ <SelectItem key="system">
222
+ System
223
+ </SelectItem>
224
+ </Select>
225
+ </CardBody>
226
+ </Card>
227
+
228
+ {/* Language & Region */}
229
+ <Card>
230
+ <CardHeader>
231
+ <h3 className="text-lg font-semibold">Language & Region</h3>
232
+ </CardHeader>
233
+ <Divider />
234
+ <CardBody>
235
+ <div className="grid gap-4 md:grid-cols-2">
236
+ <Select
237
+ label="Language"
238
+ placeholder="Select a language"
239
+ selectedKeys={preferences.language ? [preferences.language] : []}
240
+ onChange={(e) => handleSelect("language", e.target.value)}
241
+ >
242
+ <SelectItem key="en">
243
+ English
244
+ </SelectItem>
245
+ <SelectItem key="fr">
246
+ Français
247
+ </SelectItem>
248
+ <SelectItem key="es">
249
+ Español
250
+ </SelectItem>
251
+ <SelectItem key="de">
252
+ Deutsch
253
+ </SelectItem>
254
+ </Select>
255
+ <Select
256
+ label="Timezone"
257
+ placeholder="Select a timezone"
258
+ selectedKeys={preferences.timezone ? [preferences.timezone] : []}
259
+ onChange={(e) => handleSelect("timezone", e.target.value)}
260
+ >
261
+ <SelectItem key="UTC">
262
+ UTC
263
+ </SelectItem>
264
+ <SelectItem key="Europe/Paris">
265
+ Europe/Paris
266
+ </SelectItem>
267
+ <SelectItem key="America/New_York">
268
+ America/New_York
269
+ </SelectItem>
270
+ <SelectItem key="America/Los_Angeles">
271
+ America/Los_Angeles
272
+ </SelectItem>
273
+ <SelectItem key="Asia/Tokyo">
274
+ Asia/Tokyo
275
+ </SelectItem>
276
+ </Select>
277
+ </div>
278
+ </CardBody>
279
+ </Card>
280
+
281
+ {/* Actions */}
282
+ <div className="flex justify-end gap-3">
283
+ <Button
284
+ type="button"
285
+ variant="flat"
286
+ onPress={() => fetchSettings()}
287
+ isDisabled={isSaving}
288
+ >
289
+ Reset
290
+ </Button>
291
+ <Button
292
+ type="button"
293
+ color="primary"
294
+ isLoading={isSaving}
295
+ onPress={handleSave}
296
+ startContent={!isSaving && <Save className="w-4 h-4" />}
297
+ >
298
+ {isSaving ? "Saving..." : "Save Settings"}
299
+ </Button>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ );
304
+ }
@@ -0,0 +1,3 @@
1
+ export function ResetPassword() {
2
+ return <div>Reset Password Page</div>;
3
+ }
@@ -0,0 +1,255 @@
1
+ "use client";
2
+
3
+ import type { FormEvent } from "react";
4
+ import { Suspense, useState } from "react";
5
+ import { supabaseBrowserClient } from "@lastbrain/core";
6
+ import {
7
+ addToast,
8
+ Button,
9
+ Card,
10
+ CardBody,
11
+ CardHeader,
12
+ Chip,
13
+ Input,
14
+ Link,
15
+ } from "@lastbrain/ui";
16
+ import { ArrowRight, Lock, Mail, Sparkles } from "lucide-react";
17
+ import { useRouter, useSearchParams } from "next/navigation";
18
+
19
+ function SignInForm() {
20
+ const searchParams = useSearchParams();
21
+
22
+ const [email, setEmail] = useState("");
23
+ const [password, setPassword] = useState("");
24
+ const [isLoading, setIsLoading] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const redirectUrl = searchParams.get("redirect");
27
+ const router = useRouter();
28
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
29
+ event.preventDefault();
30
+ setIsLoading(true);
31
+
32
+ try {
33
+ const { data, error } =
34
+ await supabaseBrowserClient.auth.signInWithPassword({
35
+ email,
36
+ password,
37
+ });
38
+
39
+ if (error) {
40
+ addToast({
41
+ color: "danger",
42
+ title: "Erreur de connexion",
43
+ description: error.message,
44
+ });
45
+ setIsLoading(false);
46
+ return;
47
+ }
48
+
49
+ setIsLoading(false);
50
+ console.log("Signed in user:", data.user);
51
+ addToast({
52
+ color: "success",
53
+ title: "Connecté avec succès !",
54
+ description: `Bienvenue ${data.user?.email}`,
55
+ });
56
+ router.push(redirectUrl || "/auth/dashboard");
57
+ } catch (err) {
58
+ setIsLoading(false);
59
+ addToast({
60
+ color: "danger",
61
+ title: "Erreur de connexion",
62
+ description: `${err instanceof Error ? err.message : String(err)}`,
63
+ });
64
+ }
65
+ };
66
+
67
+ // return (
68
+ // <div className=" min-h-screen h-full flex flex-col justify-center items-center p-4">
69
+ // <Card className="w-full max-w-md">
70
+ // <CardHeader className="flex flex-col">
71
+ // <h1 className="text-2xl font-semibold text-slate-900 dark:text-white">
72
+ // Connexion
73
+ // </h1>
74
+ // <p className="mt-1 text-sm text-slate-500">
75
+ // Utilisez votre adresse email pour vous connecter.
76
+ // </p>
77
+ // </CardHeader>
78
+ // <CardBody>
79
+ // <form onSubmit={handleSubmit}>
80
+ // <div className="space-y-6">
81
+ // <Input
82
+ // label="Email"
83
+ // type="email"
84
+ // className="mb-6"
85
+ // required
86
+ // value={email}
87
+ // onChange={(event) => setEmail(event.target.value)}
88
+ // />
89
+
90
+ // <Input
91
+ // label="Mot de passe"
92
+ // type="password"
93
+ // required
94
+ // placeholder="Password"
95
+ // className="mb-6"
96
+ // value={password}
97
+ // onChange={(event) => setPassword(event.target.value)}
98
+ // />
99
+
100
+ // <Button
101
+ // size="lg"
102
+ // type="submit"
103
+ // className="w-full"
104
+ // isLoading={isLoading}
105
+ // >
106
+ // Se connecter
107
+ // </Button>
108
+ // </div>
109
+ // </form>
110
+ // </CardBody>
111
+ // </Card>
112
+ // </div>
113
+ // );
114
+
115
+ return (
116
+ <div className="pt-12 relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-primary-50/30 to-secondary-50/30 dark:from-primary-950/50 dark:to-secondary-950/50">
117
+ {/* Background decoration */}
118
+ <div className="absolute inset-0 bg-grid-slate-100 dark:bg-grid-slate-800/20 mask-[linear-gradient(0deg,white,rgba(255,255,255,0.6))] dark:mask-[linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.05))]" />
119
+
120
+ <div className="relative flex min-h-[60vh] items-center justify-center px-4 py-12">
121
+ <div className="w-full max-w-md">
122
+ {/* Header */}
123
+ <div className="mb-8 text-center">
124
+ <Chip
125
+ color="primary"
126
+ variant="flat"
127
+ startContent={<Sparkles className="w-4 h-4" />}
128
+ className="mb-4 p-2"
129
+ >
130
+ Espace membre
131
+ </Chip>
132
+ <h1 className="mb-3 text-4xl font-bold">
133
+ <span className="bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent">
134
+ Bon retour
135
+ </span>
136
+ </h1>
137
+ <p className="text-default-600 dark:text-default-400">
138
+ Connectez-vous pour accéder à votre espace
139
+ </p>
140
+ </div>
141
+
142
+ {/* Form Card */}
143
+ <Card className="border border-default-200/60 bg-background/60 backdrop-blur-md backdrop-saturate-150 shadow-xl">
144
+ <CardBody className="gap-6 p-8">
145
+ <form onSubmit={handleSubmit} className="flex flex-col gap-6">
146
+ <Input
147
+ label="Email"
148
+ type="email"
149
+ placeholder="votre@email.com"
150
+ value={email}
151
+ onChange={(e) => setEmail(e.target.value)}
152
+ required
153
+ startContent={<Mail className="h-4 w-4 text-default-400" />}
154
+ classNames={{
155
+ input: "text-base",
156
+ inputWrapper:
157
+ "border border-default-200/60 hover:border-primary-400 focus-within:border-primary-500",
158
+ }}
159
+ />
160
+
161
+ <div className="flex flex-col gap-2">
162
+ <Input
163
+ label="Mot de passe"
164
+ type="password"
165
+ placeholder="••••••••"
166
+ value={password}
167
+ onChange={(e) => setPassword(e.target.value)}
168
+ required
169
+ minLength={6}
170
+ startContent={<Lock className="h-4 w-4 text-default-400" />}
171
+ classNames={{
172
+ input: "text-base",
173
+ inputWrapper:
174
+ "border border-default-200/60 hover:border-primary-400 focus-within:border-primary-500",
175
+ }}
176
+ />
177
+ <Link
178
+ href="/forgot-password"
179
+ className="text-xs text-default-500 hover:text-primary-500 self-end"
180
+ >
181
+ Mot de passe oublié ?
182
+ </Link>
183
+ </div>
184
+
185
+ {error && (
186
+ <div className="rounded-lg border border-danger-200 bg-danger-50/50 px-4 py-3 dark:border-danger-800 dark:bg-danger-950/50">
187
+ <p className="text-sm text-danger-600 dark:text-danger-400">
188
+ {error}
189
+ </p>
190
+ </div>
191
+ )}
192
+
193
+ <Button
194
+ type="submit"
195
+ color="primary"
196
+ size="lg"
197
+ className="w-full font-semibold"
198
+ endContent={
199
+ isLoading ? null : <ArrowRight className="h-5 w-5" />
200
+ }
201
+ isLoading={isLoading}
202
+ >
203
+ Se connecter
204
+ </Button>
205
+ </form>
206
+
207
+ {/* Divider */}
208
+ <div className="relative flex items-center gap-4 py-2">
209
+ <div className="h-px flex-1 bg-default-200" />
210
+ <span className="text-xs text-default-400">OU</span>
211
+ <div className="h-px flex-1 bg-default-200" />
212
+ </div>
213
+
214
+ {/* Sign up link */}
215
+ <div className="text-center">
216
+ <p className="text-sm text-default-600">
217
+ Pas encore de compte ?{" "}
218
+ <Link
219
+ href={
220
+ redirectUrl
221
+ ? `/signup?redirect=${encodeURIComponent(redirectUrl)}`
222
+ : "/signup"
223
+ }
224
+ className="font-semibold text-primary-600 hover:text-primary-700"
225
+ >
226
+ Créer un compte
227
+ </Link>
228
+ </p>
229
+ </div>
230
+ </CardBody>
231
+ </Card>
232
+
233
+ {/* Footer */}
234
+ <p className="mt-8 text-center text-xs text-default-400">
235
+ En vous connectant, vous acceptez nos{" "}
236
+ <Link
237
+ href="/legal/terms"
238
+ className="underline hover:text-primary-500"
239
+ >
240
+ conditions d'utilisation
241
+ </Link>
242
+ </p>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ );
247
+ }
248
+
249
+ export function SignInPage() {
250
+ return (
251
+ <Suspense fallback={<div>Chargement...</div>}>
252
+ <SignInForm />
253
+ </Suspense>
254
+ );
255
+ }