@lastbrain/module-auth 2.0.13 → 2.0.19
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/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +33 -47
- package/dist/components/AccountButton.d.ts.map +1 -1
- package/dist/components/AccountButton.js +9 -5
- package/dist/web/admin/signup-stats.d.ts.map +1 -1
- package/dist/web/admin/signup-stats.js +4 -2
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +42 -17
- package/dist/web/admin/users-by-signup-source.d.ts.map +1 -1
- package/dist/web/admin/users-by-signup-source.js +18 -7
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +11 -6
- package/dist/web/auth/dashboard.d.ts.map +1 -1
- package/dist/web/auth/dashboard.js +7 -3
- package/dist/web/auth/folder.d.ts.map +1 -1
- package/dist/web/auth/folder.js +5 -3
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +13 -6
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +11 -6
- package/dist/web/public/SignInPage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.js +14 -56
- package/dist/web/public/SignUpPage.d.ts.map +1 -1
- package/dist/web/public/SignUpPage.js +18 -11
- package/package.json +4 -3
- package/src/auth.build.config.ts +34 -48
- package/src/components/AccountButton.tsx +17 -10
- package/src/i18n/en.json +263 -0
- package/src/i18n/fr.json +261 -0
- package/src/web/admin/signup-stats.tsx +10 -3
- package/src/web/admin/user-detail.tsx +135 -56
- package/src/web/admin/users-by-signup-source.tsx +60 -21
- package/src/web/admin/users.tsx +41 -18
- package/src/web/auth/dashboard.tsx +25 -9
- package/src/web/auth/folder.tsx +11 -3
- package/src/web/auth/profile.tsx +63 -29
- package/src/web/auth/reglage.tsx +43 -19
- package/src/web/public/SignInPage.tsx +32 -70
- package/src/web/public/SignUpPage.tsx +48 -26
- package/supabase/migrations/20251112000000_user_init.sql +35 -19
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +8 -3
- package/supabase/migrations/20251112000002_sync_avatars.sql +7 -1
- package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +2 -1
|
@@ -13,17 +13,22 @@ import {
|
|
|
13
13
|
Link,
|
|
14
14
|
} from "@lastbrain/ui";
|
|
15
15
|
import { ArrowRight, Lock, Mail, Sparkles } from "lucide-react";
|
|
16
|
-
import {
|
|
16
|
+
import { useSearchParams } from "next/navigation";
|
|
17
|
+
import { useModuleTranslation } from "@lastbrain/core";
|
|
18
|
+
import { useLocalizedRouter } from "@lastbrain/core";
|
|
17
19
|
|
|
18
20
|
function SignInForm() {
|
|
19
21
|
const searchParams = useSearchParams();
|
|
22
|
+
const t = useModuleTranslation("auth");
|
|
23
|
+
|
|
24
|
+
const router = useLocalizedRouter();
|
|
20
25
|
|
|
21
26
|
const [email, setEmail] = useState("");
|
|
22
27
|
const [password, setPassword] = useState("");
|
|
23
28
|
const [isLoading, setIsLoading] = useState(false);
|
|
24
29
|
const [error, _setError] = useState<string | null>(null);
|
|
25
30
|
const redirectUrl = searchParams.get("redirect");
|
|
26
|
-
|
|
31
|
+
|
|
27
32
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
28
33
|
event.preventDefault();
|
|
29
34
|
setIsLoading(true);
|
|
@@ -38,7 +43,7 @@ function SignInForm() {
|
|
|
38
43
|
if (error) {
|
|
39
44
|
addToast({
|
|
40
45
|
color: "danger",
|
|
41
|
-
title: "Erreur de connexion",
|
|
46
|
+
title: t("signin.error_title") || "Erreur de connexion",
|
|
42
47
|
description: error.message,
|
|
43
48
|
});
|
|
44
49
|
setIsLoading(false);
|
|
@@ -46,71 +51,23 @@ function SignInForm() {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
setIsLoading(false);
|
|
49
|
-
|
|
54
|
+
|
|
50
55
|
addToast({
|
|
51
56
|
color: "success",
|
|
52
|
-
title: "Connecté avec succès !",
|
|
53
|
-
description:
|
|
57
|
+
title: t("signin.success_title") || "Connecté avec succès !",
|
|
58
|
+
description: `${t("signin.success_welcome") || "Bienvenue"} ${data.user?.email}`,
|
|
54
59
|
});
|
|
55
60
|
router.push(redirectUrl || "/auth/dashboard");
|
|
56
61
|
} catch (err) {
|
|
57
62
|
setIsLoading(false);
|
|
58
63
|
addToast({
|
|
59
64
|
color: "danger",
|
|
60
|
-
title: "Erreur de connexion",
|
|
65
|
+
title: t("signin.error_title") || "Erreur de connexion",
|
|
61
66
|
description: `${err instanceof Error ? err.message : String(err)}`,
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
};
|
|
65
70
|
|
|
66
|
-
// return (
|
|
67
|
-
// <div className=" min-h-screen h-full flex flex-col justify-center items-center p-4">
|
|
68
|
-
// <Card className="w-full max-w-md">
|
|
69
|
-
// <CardHeader className="flex flex-col">
|
|
70
|
-
// <h1 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
71
|
-
// Connexion
|
|
72
|
-
// </h1>
|
|
73
|
-
// <p className="mt-1 text-sm text-slate-500">
|
|
74
|
-
// Utilisez votre adresse email pour vous connecter.
|
|
75
|
-
// </p>
|
|
76
|
-
// </CardHeader>
|
|
77
|
-
// <CardBody>
|
|
78
|
-
// <form onSubmit={handleSubmit}>
|
|
79
|
-
// <div className="space-y-6">
|
|
80
|
-
// <Input
|
|
81
|
-
// label="Email"
|
|
82
|
-
// type="email"
|
|
83
|
-
// className="mb-6"
|
|
84
|
-
// required
|
|
85
|
-
// value={email}
|
|
86
|
-
// onChange={(event) => setEmail(event.target.value)}
|
|
87
|
-
// />
|
|
88
|
-
|
|
89
|
-
// <Input
|
|
90
|
-
// label="Mot de passe"
|
|
91
|
-
// type="password"
|
|
92
|
-
// required
|
|
93
|
-
// placeholder="Password"
|
|
94
|
-
// className="mb-6"
|
|
95
|
-
// value={password}
|
|
96
|
-
// onChange={(event) => setPassword(event.target.value)}
|
|
97
|
-
// />
|
|
98
|
-
|
|
99
|
-
// <Button
|
|
100
|
-
// size="lg"
|
|
101
|
-
// type="submit"
|
|
102
|
-
// className="w-full"
|
|
103
|
-
// isLoading={isLoading}
|
|
104
|
-
// >
|
|
105
|
-
// Se connecter
|
|
106
|
-
// </Button>
|
|
107
|
-
// </div>
|
|
108
|
-
// </form>
|
|
109
|
-
// </CardBody>
|
|
110
|
-
// </Card>
|
|
111
|
-
// </div>
|
|
112
|
-
// );
|
|
113
|
-
|
|
114
71
|
return (
|
|
115
72
|
<div className="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">
|
|
116
73
|
{/* Background decoration */}
|
|
@@ -126,15 +83,16 @@ function SignInForm() {
|
|
|
126
83
|
startContent={<Sparkles className="w-4 h-4" />}
|
|
127
84
|
className="mb-4 p-2"
|
|
128
85
|
>
|
|
129
|
-
Espace membre
|
|
86
|
+
{t("signin.chip_label") || "Espace membre"}
|
|
130
87
|
</Chip>
|
|
131
88
|
<h1 className="mb-3 text-4xl font-bold">
|
|
132
89
|
<span className="bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent">
|
|
133
|
-
Bon retour
|
|
90
|
+
{t("signin.title") || "Bon retour"}
|
|
134
91
|
</span>
|
|
135
92
|
</h1>
|
|
136
|
-
<p className="text-default-600 dark:text-default-
|
|
137
|
-
|
|
93
|
+
<p className="text-default-600 dark:text-default-700">
|
|
94
|
+
{t("signin.subtitle") ||
|
|
95
|
+
"Connectez-vous pour accéder à votre espace"}
|
|
138
96
|
</p>
|
|
139
97
|
</div>
|
|
140
98
|
|
|
@@ -143,9 +101,11 @@ function SignInForm() {
|
|
|
143
101
|
<CardBody className="gap-6 p-8">
|
|
144
102
|
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
145
103
|
<Input
|
|
146
|
-
label="Email"
|
|
104
|
+
label={t("email") || "Email"}
|
|
147
105
|
type="email"
|
|
148
|
-
placeholder=
|
|
106
|
+
placeholder={
|
|
107
|
+
t("signin.email_placeholder") || "votre@email.com"
|
|
108
|
+
}
|
|
149
109
|
value={email}
|
|
150
110
|
onChange={(e) => setEmail(e.target.value)}
|
|
151
111
|
required
|
|
@@ -159,7 +119,7 @@ function SignInForm() {
|
|
|
159
119
|
|
|
160
120
|
<div className="flex flex-col gap-2">
|
|
161
121
|
<Input
|
|
162
|
-
label="Mot de passe"
|
|
122
|
+
label={t("password") || "Mot de passe"}
|
|
163
123
|
type="password"
|
|
164
124
|
placeholder="••••••••"
|
|
165
125
|
value={password}
|
|
@@ -177,7 +137,7 @@ function SignInForm() {
|
|
|
177
137
|
href="/forgot-password"
|
|
178
138
|
className="text-xs text-default-500 hover:text-primary-500 self-end"
|
|
179
139
|
>
|
|
180
|
-
Mot de passe oublié ?
|
|
140
|
+
{t("signin.forgot_password") || "Mot de passe oublié ?"}
|
|
181
141
|
</Link>
|
|
182
142
|
</div>
|
|
183
143
|
|
|
@@ -199,21 +159,23 @@ function SignInForm() {
|
|
|
199
159
|
}
|
|
200
160
|
isLoading={isLoading}
|
|
201
161
|
>
|
|
202
|
-
Se connecter
|
|
162
|
+
{t("signin.submit") || "Se connecter"}
|
|
203
163
|
</Button>
|
|
204
164
|
</form>
|
|
205
165
|
|
|
206
166
|
{/* Divider */}
|
|
207
167
|
<div className="relative flex items-center gap-4 py-2">
|
|
208
168
|
<div className="h-px flex-1 bg-default-200" />
|
|
209
|
-
<span className="text-xs text-default-400">
|
|
169
|
+
<span className="text-xs text-default-400">
|
|
170
|
+
{t("or") || "OU"}
|
|
171
|
+
</span>
|
|
210
172
|
<div className="h-px flex-1 bg-default-200" />
|
|
211
173
|
</div>
|
|
212
174
|
|
|
213
175
|
{/* Sign up link */}
|
|
214
176
|
<div className="text-center">
|
|
215
177
|
<p className="text-sm text-default-600">
|
|
216
|
-
Pas encore de compte ?{" "}
|
|
178
|
+
{t("signin.no_account") || "Pas encore de compte ?"}{" "}
|
|
217
179
|
<Link
|
|
218
180
|
href={
|
|
219
181
|
redirectUrl
|
|
@@ -222,7 +184,7 @@ function SignInForm() {
|
|
|
222
184
|
}
|
|
223
185
|
className="font-semibold text-primary-600 hover:text-primary-700"
|
|
224
186
|
>
|
|
225
|
-
Créer un compte
|
|
187
|
+
{t("signin.sign_up") || "Créer un compte"}
|
|
226
188
|
</Link>
|
|
227
189
|
</p>
|
|
228
190
|
</div>
|
|
@@ -230,13 +192,13 @@ function SignInForm() {
|
|
|
230
192
|
</Card>
|
|
231
193
|
|
|
232
194
|
{/* Footer */}
|
|
233
|
-
<p className="mt-8 text-center text-
|
|
234
|
-
|
|
195
|
+
<p className="mt-8 text-center text-sm text-default-700">
|
|
196
|
+
{t("signin.accept_terms")}{" "}
|
|
235
197
|
<Link
|
|
236
198
|
href="/legal/terms"
|
|
237
199
|
className="underline hover:text-primary-500"
|
|
238
200
|
>
|
|
239
|
-
|
|
201
|
+
{t("signin.term")}
|
|
240
202
|
</Link>
|
|
241
203
|
</p>
|
|
242
204
|
</div>
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
Chip,
|
|
10
10
|
addToast,
|
|
11
11
|
} from "@lastbrain/ui";
|
|
12
|
-
import {
|
|
12
|
+
import { useSearchParams } from "next/navigation";
|
|
13
13
|
import { Suspense, useState } from "react";
|
|
14
14
|
import {
|
|
15
15
|
Mail,
|
|
@@ -19,11 +19,14 @@ import {
|
|
|
19
19
|
Sparkles,
|
|
20
20
|
CheckCircle2,
|
|
21
21
|
} from "lucide-react";
|
|
22
|
-
import {
|
|
22
|
+
import { useModuleTranslation } from "@lastbrain/core";
|
|
23
|
+
import { useLocalizedRouter } from "@lastbrain/core";
|
|
23
24
|
|
|
24
25
|
function SignUpForm() {
|
|
25
|
-
const router =
|
|
26
|
+
const router = useLocalizedRouter();
|
|
26
27
|
const searchParams = useSearchParams();
|
|
28
|
+
|
|
29
|
+
const t = useModuleTranslation("auth");
|
|
27
30
|
const [fullName, setFullName] = useState("");
|
|
28
31
|
const [email, setEmail] = useState("");
|
|
29
32
|
const [password, setPassword] = useState("");
|
|
@@ -41,12 +44,18 @@ function SignUpForm() {
|
|
|
41
44
|
setSuccess(null);
|
|
42
45
|
|
|
43
46
|
if (password !== confirmPassword) {
|
|
44
|
-
setError(
|
|
47
|
+
setError(
|
|
48
|
+
t("signup.password_mismatch") ||
|
|
49
|
+
"Les mots de passe ne correspondent pas."
|
|
50
|
+
);
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
if (password.length < 6) {
|
|
49
|
-
setError(
|
|
55
|
+
setError(
|
|
56
|
+
t("signup.password_too_short") ||
|
|
57
|
+
"Le mot de passe doit contenir au moins 6 caractères."
|
|
58
|
+
);
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -86,7 +95,7 @@ function SignUpForm() {
|
|
|
86
95
|
if (redirectUrl) {
|
|
87
96
|
router.push(`/signin?redirect=${encodeURIComponent(redirectUrl)}`);
|
|
88
97
|
} else {
|
|
89
|
-
router.push(
|
|
98
|
+
router.push(`/signin`);
|
|
90
99
|
}
|
|
91
100
|
}, 2000);
|
|
92
101
|
} catch {
|
|
@@ -115,15 +124,16 @@ function SignUpForm() {
|
|
|
115
124
|
startContent={<Sparkles className="w-4 h-4" />}
|
|
116
125
|
className="mb-4 p-2"
|
|
117
126
|
>
|
|
118
|
-
Rejoignez-nous
|
|
127
|
+
{t("signup.chip_label") || "Rejoignez-nous"}
|
|
119
128
|
</Chip>
|
|
120
129
|
<h1 className="mb-3 text-4xl font-bold">
|
|
121
130
|
<span className="bg-gradient-to-r from-secondary-600 to-primary-600 bg-clip-text text-transparent">
|
|
122
|
-
Commencer gratuitement
|
|
131
|
+
{t("signup.title") || "Commencer gratuitement"}
|
|
123
132
|
</span>
|
|
124
133
|
</h1>
|
|
125
|
-
<p className="text-default-600 dark:text-default-
|
|
126
|
-
|
|
134
|
+
<p className="text-default-600 dark:text-default-700">
|
|
135
|
+
{t("signup.subtitle") ||
|
|
136
|
+
"Créez votre compte en quelques secondes"}
|
|
127
137
|
</p>
|
|
128
138
|
</div>
|
|
129
139
|
|
|
@@ -132,9 +142,11 @@ function SignUpForm() {
|
|
|
132
142
|
<CardBody className="gap-6 p-8">
|
|
133
143
|
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
134
144
|
<Input
|
|
135
|
-
label="Nom complet"
|
|
145
|
+
label={t("signup.full_name") || "Nom complet"}
|
|
136
146
|
type="text"
|
|
137
|
-
placeholder=
|
|
147
|
+
placeholder={
|
|
148
|
+
t("signup.full_name_placeholder") || "Jean Dupont"
|
|
149
|
+
}
|
|
138
150
|
value={fullName}
|
|
139
151
|
onChange={(e) => setFullName(e.target.value)}
|
|
140
152
|
startContent={<User className="h-4 w-4 text-default-400" />}
|
|
@@ -146,9 +158,11 @@ function SignUpForm() {
|
|
|
146
158
|
/>
|
|
147
159
|
|
|
148
160
|
<Input
|
|
149
|
-
label="Email"
|
|
161
|
+
label={t("email") || "Email"}
|
|
150
162
|
type="email"
|
|
151
|
-
placeholder=
|
|
163
|
+
placeholder={
|
|
164
|
+
t("signin.email_placeholder") || "votre@email.com"
|
|
165
|
+
}
|
|
152
166
|
value={email}
|
|
153
167
|
onChange={(e) => setEmail(e.target.value)}
|
|
154
168
|
required
|
|
@@ -161,7 +175,7 @@ function SignUpForm() {
|
|
|
161
175
|
/>
|
|
162
176
|
|
|
163
177
|
<Input
|
|
164
|
-
label="Mot de passe"
|
|
178
|
+
label={t("password") || "Mot de passe"}
|
|
165
179
|
type="password"
|
|
166
180
|
placeholder="••••••••"
|
|
167
181
|
value={password}
|
|
@@ -174,11 +188,15 @@ function SignUpForm() {
|
|
|
174
188
|
inputWrapper:
|
|
175
189
|
"border border-default-200/60 hover:border-secondary-400 focus-within:border-secondary-500",
|
|
176
190
|
}}
|
|
177
|
-
description=
|
|
191
|
+
description={
|
|
192
|
+
t("signup.password_hint") || "Minimum 6 caractères"
|
|
193
|
+
}
|
|
178
194
|
/>
|
|
179
195
|
|
|
180
196
|
<Input
|
|
181
|
-
label=
|
|
197
|
+
label={
|
|
198
|
+
t("signup.confirm_password") || "Confirmer le mot de passe"
|
|
199
|
+
}
|
|
182
200
|
type="password"
|
|
183
201
|
placeholder="••••••••"
|
|
184
202
|
value={confirmPassword}
|
|
@@ -222,21 +240,23 @@ function SignUpForm() {
|
|
|
222
240
|
}
|
|
223
241
|
isLoading={loading}
|
|
224
242
|
>
|
|
225
|
-
Créer mon compte
|
|
243
|
+
{t("signup.submit") || "Créer mon compte"}
|
|
226
244
|
</Button>
|
|
227
245
|
</form>
|
|
228
246
|
|
|
229
247
|
{/* Divider */}
|
|
230
248
|
<div className="relative flex items-center gap-4 py-2">
|
|
231
249
|
<div className="h-px flex-1 bg-default-200" />
|
|
232
|
-
<span className="text-xs text-default-400">
|
|
250
|
+
<span className="text-xs text-default-400">
|
|
251
|
+
{t("signup.or") || "OU"}
|
|
252
|
+
</span>
|
|
233
253
|
<div className="h-px flex-1 bg-default-200" />
|
|
234
254
|
</div>
|
|
235
255
|
|
|
236
256
|
{/* Sign in link */}
|
|
237
257
|
<div className="text-center">
|
|
238
258
|
<p className="text-sm text-default-600">
|
|
239
|
-
Déjà inscrit ?{" "}
|
|
259
|
+
{t("signup.have_account") || "Déjà inscrit ?"}{" "}
|
|
240
260
|
<Link
|
|
241
261
|
href={
|
|
242
262
|
redirectUrl
|
|
@@ -245,7 +265,7 @@ function SignUpForm() {
|
|
|
245
265
|
}
|
|
246
266
|
className="font-semibold text-secondary-600 hover:text-secondary-700"
|
|
247
267
|
>
|
|
248
|
-
Se connecter
|
|
268
|
+
{t("signin.submit") || "Se connecter"}
|
|
249
269
|
</Link>
|
|
250
270
|
</p>
|
|
251
271
|
</div>
|
|
@@ -253,20 +273,22 @@ function SignUpForm() {
|
|
|
253
273
|
</Card>
|
|
254
274
|
|
|
255
275
|
{/* Footer */}
|
|
256
|
-
|
|
257
|
-
|
|
276
|
+
|
|
277
|
+
<p className="mt-8 text-center text-sm text-default-700">
|
|
278
|
+
{t("signup.legal_prefix") ||
|
|
279
|
+
"En créant un compte, vous acceptez nos"}{" "}
|
|
258
280
|
<Link
|
|
259
281
|
href="/legal/terms"
|
|
260
282
|
className="underline hover:text-secondary-500"
|
|
261
283
|
>
|
|
262
|
-
conditions d'utilisation
|
|
284
|
+
{t("signup.terms") || "conditions d'utilisation"}
|
|
263
285
|
</Link>{" "}
|
|
264
|
-
et notre{" "}
|
|
286
|
+
{t("signup.legal_and") || "et notre"}{" "}
|
|
265
287
|
<Link
|
|
266
288
|
href="/legal/privacy"
|
|
267
289
|
className="underline hover:text-secondary-500"
|
|
268
290
|
>
|
|
269
|
-
politique de confidentialité
|
|
291
|
+
{t("signup.privacy") || "politique de confidentialité"}
|
|
270
292
|
</Link>
|
|
271
293
|
</p>
|
|
272
294
|
</div>
|
|
@@ -29,23 +29,23 @@ ALTER TABLE public.user_profil ENABLE ROW LEVEL SECURITY;
|
|
|
29
29
|
|
|
30
30
|
DROP POLICY IF EXISTS user_profil_owner_select ON public.user_profil;
|
|
31
31
|
CREATE POLICY user_profil_owner_select ON public.user_profil
|
|
32
|
-
FOR SELECT USING (owner_id = auth.uid());
|
|
32
|
+
FOR SELECT USING (owner_id = (SELECT auth.uid()));
|
|
33
33
|
|
|
34
34
|
DROP POLICY IF EXISTS user_profil_owner_insert ON public.user_profil;
|
|
35
35
|
CREATE POLICY user_profil_owner_insert ON public.user_profil
|
|
36
|
-
FOR INSERT WITH CHECK (owner_id = auth.uid());
|
|
36
|
+
FOR INSERT WITH CHECK (owner_id = (SELECT auth.uid()));
|
|
37
37
|
|
|
38
38
|
DROP POLICY IF EXISTS user_profil_owner_update ON public.user_profil;
|
|
39
39
|
CREATE POLICY user_profil_owner_update ON public.user_profil
|
|
40
|
-
FOR UPDATE USING (owner_id = auth.uid());
|
|
40
|
+
FOR UPDATE USING (owner_id = (SELECT auth.uid()));
|
|
41
41
|
|
|
42
42
|
DROP POLICY IF EXISTS user_profil_owner_delete ON public.user_profil;
|
|
43
43
|
CREATE POLICY user_profil_owner_delete ON public.user_profil
|
|
44
|
-
FOR DELETE USING (owner_id = auth.uid());
|
|
44
|
+
FOR DELETE USING (owner_id = (SELECT auth.uid()));
|
|
45
45
|
|
|
46
46
|
DROP POLICY IF EXISTS user_profil_superadmin_all ON public.user_profil;
|
|
47
47
|
CREATE POLICY user_profil_superadmin_all ON public.user_profil
|
|
48
|
-
FOR ALL USING (is_superadmin(auth.uid()));
|
|
48
|
+
FOR ALL USING (is_superadmin((SELECT auth.uid())));
|
|
49
49
|
|
|
50
50
|
-- Trigger updated_at
|
|
51
51
|
CREATE OR REPLACE FUNCTION public.set_user_profil_updated_at()
|
|
@@ -54,7 +54,8 @@ BEGIN
|
|
|
54
54
|
NEW.updated_at = now();
|
|
55
55
|
RETURN NEW;
|
|
56
56
|
END;
|
|
57
|
-
$$ LANGUAGE plpgsql
|
|
57
|
+
$$ LANGUAGE plpgsql
|
|
58
|
+
SET search_path = public;
|
|
58
59
|
|
|
59
60
|
DROP TRIGGER IF EXISTS set_user_profil_updated_at ON public.user_profil;
|
|
60
61
|
CREATE TRIGGER set_user_profil_updated_at
|
|
@@ -87,23 +88,23 @@ ALTER TABLE public.user_address ENABLE ROW LEVEL SECURITY;
|
|
|
87
88
|
|
|
88
89
|
DROP POLICY IF EXISTS user_address_owner_select ON public.user_address;
|
|
89
90
|
CREATE POLICY user_address_owner_select ON public.user_address
|
|
90
|
-
FOR SELECT USING (owner_id = auth.uid());
|
|
91
|
+
FOR SELECT USING (owner_id = (SELECT auth.uid()));
|
|
91
92
|
|
|
92
93
|
DROP POLICY IF EXISTS user_address_owner_insert ON public.user_address;
|
|
93
94
|
CREATE POLICY user_address_owner_insert ON public.user_address
|
|
94
|
-
FOR INSERT WITH CHECK (owner_id = auth.uid());
|
|
95
|
+
FOR INSERT WITH CHECK (owner_id = (SELECT auth.uid()));
|
|
95
96
|
|
|
96
97
|
DROP POLICY IF EXISTS user_address_owner_update ON public.user_address;
|
|
97
98
|
CREATE POLICY user_address_owner_update ON public.user_address
|
|
98
|
-
FOR UPDATE USING (owner_id = auth.uid());
|
|
99
|
+
FOR UPDATE USING (owner_id = (SELECT auth.uid()));
|
|
99
100
|
|
|
100
101
|
DROP POLICY IF EXISTS user_address_owner_delete ON public.user_address;
|
|
101
102
|
CREATE POLICY user_address_owner_delete ON public.user_address
|
|
102
|
-
FOR DELETE USING (owner_id = auth.uid());
|
|
103
|
+
FOR DELETE USING (owner_id = (SELECT auth.uid()));
|
|
103
104
|
|
|
104
105
|
DROP POLICY IF EXISTS user_address_superadmin_all ON public.user_address;
|
|
105
106
|
CREATE POLICY user_address_superadmin_all ON public.user_address
|
|
106
|
-
FOR ALL USING (is_superadmin(auth.uid()));
|
|
107
|
+
FOR ALL USING (is_superadmin((SELECT auth.uid())));
|
|
107
108
|
|
|
108
109
|
-- Trigger updated_at
|
|
109
110
|
CREATE OR REPLACE FUNCTION public.set_user_address_updated_at()
|
|
@@ -112,7 +113,8 @@ BEGIN
|
|
|
112
113
|
NEW.updated_at = now();
|
|
113
114
|
RETURN NEW;
|
|
114
115
|
END;
|
|
115
|
-
$$ LANGUAGE plpgsql
|
|
116
|
+
$$ LANGUAGE plpgsql
|
|
117
|
+
SET search_path = public;
|
|
116
118
|
|
|
117
119
|
DROP TRIGGER IF EXISTS set_user_address_updated_at ON public.user_address;
|
|
118
120
|
CREATE TRIGGER set_user_address_updated_at
|
|
@@ -141,23 +143,23 @@ ALTER TABLE public.user_notifications ENABLE ROW LEVEL SECURITY;
|
|
|
141
143
|
|
|
142
144
|
DROP POLICY IF EXISTS user_notifications_owner_or_superadmin_select ON public.user_notifications;
|
|
143
145
|
CREATE POLICY user_notifications_owner_or_superadmin_select ON public.user_notifications
|
|
144
|
-
FOR SELECT USING (owner_id = auth.uid() OR is_superadmin(auth.uid()));
|
|
146
|
+
FOR SELECT USING (owner_id = (SELECT auth.uid()) OR is_superadmin((SELECT auth.uid())));
|
|
145
147
|
|
|
146
148
|
DROP POLICY IF EXISTS user_notifications_owner_or_superadmin_insert ON public.user_notifications;
|
|
147
149
|
CREATE POLICY user_notifications_owner_or_superadmin_insert ON public.user_notifications
|
|
148
|
-
FOR INSERT WITH CHECK (owner_id = auth.uid() OR is_superadmin(auth.uid()));
|
|
150
|
+
FOR INSERT WITH CHECK (owner_id = (SELECT auth.uid()) OR is_superadmin((SELECT auth.uid())));
|
|
149
151
|
|
|
150
152
|
DROP POLICY IF EXISTS user_notifications_owner_or_superadmin_update ON public.user_notifications;
|
|
151
153
|
CREATE POLICY user_notifications_owner_or_superadmin_update ON public.user_notifications
|
|
152
|
-
FOR UPDATE USING (owner_id = auth.uid() OR is_superadmin(auth.uid()));
|
|
154
|
+
FOR UPDATE USING (owner_id = (SELECT auth.uid()) OR is_superadmin((SELECT auth.uid())));
|
|
153
155
|
|
|
154
156
|
DROP POLICY IF EXISTS user_notifications_owner_or_superadmin_delete ON public.user_notifications;
|
|
155
157
|
CREATE POLICY user_notifications_owner_or_superadmin_delete ON public.user_notifications
|
|
156
|
-
FOR DELETE USING (owner_id = auth.uid() OR is_superadmin(auth.uid()));
|
|
158
|
+
FOR DELETE USING (owner_id = (SELECT auth.uid()) OR is_superadmin((SELECT auth.uid())));
|
|
157
159
|
|
|
158
160
|
DROP POLICY IF EXISTS user_notifications_superadmin_all ON public.user_notifications;
|
|
159
161
|
CREATE POLICY user_notifications_superadmin_all ON public.user_notifications
|
|
160
|
-
FOR ALL USING (is_superadmin(auth.uid()));
|
|
162
|
+
FOR ALL USING (is_superadmin((SELECT auth.uid())));
|
|
161
163
|
|
|
162
164
|
-- Trigger updated_at
|
|
163
165
|
CREATE OR REPLACE FUNCTION public.set_user_notifications_updated_at()
|
|
@@ -166,7 +168,8 @@ BEGIN
|
|
|
166
168
|
NEW.updated_at = now();
|
|
167
169
|
RETURN NEW;
|
|
168
170
|
END;
|
|
169
|
-
$$ LANGUAGE plpgsql
|
|
171
|
+
$$ LANGUAGE plpgsql
|
|
172
|
+
SET search_path = public;
|
|
170
173
|
|
|
171
174
|
DROP TRIGGER IF EXISTS set_user_notifications_updated_at ON public.user_notifications;
|
|
172
175
|
CREATE TRIGGER set_user_notifications_updated_at
|
|
@@ -177,7 +180,20 @@ CREATE TRIGGER set_user_notifications_updated_at
|
|
|
177
180
|
-- =====================================================
|
|
178
181
|
-- Enable Realtime for user_notifications
|
|
179
182
|
-- =====================================================
|
|
180
|
-
|
|
183
|
+
-- Use DO block to handle idempotent add/remove
|
|
184
|
+
DO $$
|
|
185
|
+
BEGIN
|
|
186
|
+
-- Try to remove the table from publication if it exists
|
|
187
|
+
BEGIN
|
|
188
|
+
ALTER PUBLICATION supabase_realtime DROP TABLE public.user_notifications;
|
|
189
|
+
EXCEPTION WHEN OTHERS THEN
|
|
190
|
+
-- Table not in publication, continue
|
|
191
|
+
NULL;
|
|
192
|
+
END;
|
|
193
|
+
|
|
194
|
+
-- Now add it
|
|
195
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.user_notifications;
|
|
196
|
+
END $$;
|
|
181
197
|
|
|
182
198
|
-- Add signup_source to user_profil table
|
|
183
199
|
-- Track where users signed up from (e.g., 'lastbrain', 'recipe', etc.)
|
|
@@ -36,7 +36,8 @@ EXCEPTION
|
|
|
36
36
|
-- Profile already exists, ignore
|
|
37
37
|
RETURN NEW;
|
|
38
38
|
END;
|
|
39
|
-
$$ LANGUAGE plpgsql SECURITY DEFINER
|
|
39
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER
|
|
40
|
+
SET search_path = public;
|
|
40
41
|
|
|
41
42
|
-- Trigger to call the function when a new user is created
|
|
42
43
|
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
|
@@ -56,6 +57,7 @@ CREATE OR REPLACE FUNCTION public.get_admin_users(
|
|
|
56
57
|
RETURNS JSON
|
|
57
58
|
LANGUAGE plpgsql
|
|
58
59
|
SECURITY DEFINER
|
|
60
|
+
SET search_path = public
|
|
59
61
|
AS $$
|
|
60
62
|
DECLARE
|
|
61
63
|
offset_val INTEGER;
|
|
@@ -63,7 +65,7 @@ DECLARE
|
|
|
63
65
|
total_count INTEGER;
|
|
64
66
|
BEGIN
|
|
65
67
|
-- Check if user is superadmin
|
|
66
|
-
IF NOT is_superadmin(auth.uid()) THEN
|
|
68
|
+
IF NOT is_superadmin((SELECT auth.uid())) THEN
|
|
67
69
|
RAISE EXCEPTION 'Access denied. Superadmin required.';
|
|
68
70
|
END IF;
|
|
69
71
|
|
|
@@ -166,6 +168,8 @@ $$;
|
|
|
166
168
|
-- Function: sync_fullname_to_metadata
|
|
167
169
|
-- =====================================================
|
|
168
170
|
-- Synchronize full_name in auth.users metadata when profile is updated
|
|
171
|
+
-- SECURITY: Runs as SECURITY DEFINER but only modifies the profile owner's metadata
|
|
172
|
+
-- Permission is controlled by RLS policies on user_profil table
|
|
169
173
|
CREATE OR REPLACE FUNCTION public.sync_fullname_to_metadata()
|
|
170
174
|
RETURNS TRIGGER AS $$
|
|
171
175
|
DECLARE
|
|
@@ -193,7 +197,8 @@ BEGIN
|
|
|
193
197
|
|
|
194
198
|
RETURN NEW;
|
|
195
199
|
END;
|
|
196
|
-
$$ LANGUAGE plpgsql SECURITY DEFINER
|
|
200
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER
|
|
201
|
+
SET search_path = public;
|
|
197
202
|
|
|
198
203
|
-- Trigger to sync full_name when profile is updated
|
|
199
204
|
DROP TRIGGER IF EXISTS sync_fullname_on_profile_update ON public.user_profil;
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
-- Module: @lastbrain/module-auth
|
|
3
3
|
|
|
4
4
|
-- Function to sync avatars from auth metadata to user_profil
|
|
5
|
+
-- SECURITY: Runs as SECURITY DEFINER but is an automated system operation
|
|
6
|
+
-- called only via triggers on auth.users - no caller validation needed
|
|
5
7
|
CREATE OR REPLACE FUNCTION public.sync_avatar_from_auth_metadata()
|
|
6
8
|
RETURNS VOID
|
|
7
9
|
LANGUAGE plpgsql
|
|
8
10
|
SECURITY DEFINER
|
|
11
|
+
SET search_path = public
|
|
9
12
|
AS $$
|
|
10
13
|
BEGIN
|
|
11
14
|
-- Update user_profil with avatar URLs from auth.users metadata
|
|
@@ -26,6 +29,8 @@ END;
|
|
|
26
29
|
$$;
|
|
27
30
|
|
|
28
31
|
-- Create a trigger to automatically sync avatar when auth.users is updated
|
|
32
|
+
-- SECURITY: Runs as SECURITY DEFINER but is an automated system operation
|
|
33
|
+
-- Triggered only on auth.users updates - no caller validation needed
|
|
29
34
|
CREATE OR REPLACE FUNCTION public.handle_auth_user_avatar_update()
|
|
30
35
|
RETURNS TRIGGER AS $$
|
|
31
36
|
BEGIN
|
|
@@ -42,7 +47,8 @@ BEGIN
|
|
|
42
47
|
|
|
43
48
|
RETURN NEW;
|
|
44
49
|
END;
|
|
45
|
-
$$ LANGUAGE plpgsql SECURITY DEFINER
|
|
50
|
+
$$ LANGUAGE plpgsql SECURITY DEFINER
|
|
51
|
+
SET search_path = public;
|
|
46
52
|
|
|
47
53
|
-- Create trigger for avatar sync on auth.users update
|
|
48
54
|
DROP TRIGGER IF EXISTS on_auth_user_avatar_updated ON auth.users;
|
|
@@ -9,12 +9,13 @@ CREATE OR REPLACE FUNCTION public.get_admin_user_details(user_id UUID)
|
|
|
9
9
|
RETURNS JSON
|
|
10
10
|
LANGUAGE plpgsql
|
|
11
11
|
SECURITY DEFINER
|
|
12
|
+
SET search_path = public
|
|
12
13
|
AS $$
|
|
13
14
|
DECLARE
|
|
14
15
|
result JSON;
|
|
15
16
|
BEGIN
|
|
16
17
|
-- Check if user is superadmin
|
|
17
|
-
IF NOT is_superadmin(auth.uid()) THEN
|
|
18
|
+
IF NOT is_superadmin((SELECT auth.uid())) THEN
|
|
18
19
|
RAISE EXCEPTION 'Access denied. Superadmin required.';
|
|
19
20
|
END IF;
|
|
20
21
|
|