@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.
Files changed (43) hide show
  1. package/dist/auth.build.config.d.ts.map +1 -1
  2. package/dist/auth.build.config.js +33 -47
  3. package/dist/components/AccountButton.d.ts.map +1 -1
  4. package/dist/components/AccountButton.js +9 -5
  5. package/dist/web/admin/signup-stats.d.ts.map +1 -1
  6. package/dist/web/admin/signup-stats.js +4 -2
  7. package/dist/web/admin/user-detail.d.ts.map +1 -1
  8. package/dist/web/admin/user-detail.js +42 -17
  9. package/dist/web/admin/users-by-signup-source.d.ts.map +1 -1
  10. package/dist/web/admin/users-by-signup-source.js +18 -7
  11. package/dist/web/admin/users.d.ts.map +1 -1
  12. package/dist/web/admin/users.js +11 -6
  13. package/dist/web/auth/dashboard.d.ts.map +1 -1
  14. package/dist/web/auth/dashboard.js +7 -3
  15. package/dist/web/auth/folder.d.ts.map +1 -1
  16. package/dist/web/auth/folder.js +5 -3
  17. package/dist/web/auth/profile.d.ts.map +1 -1
  18. package/dist/web/auth/profile.js +13 -6
  19. package/dist/web/auth/reglage.d.ts.map +1 -1
  20. package/dist/web/auth/reglage.js +11 -6
  21. package/dist/web/public/SignInPage.d.ts.map +1 -1
  22. package/dist/web/public/SignInPage.js +14 -56
  23. package/dist/web/public/SignUpPage.d.ts.map +1 -1
  24. package/dist/web/public/SignUpPage.js +18 -11
  25. package/package.json +4 -3
  26. package/src/auth.build.config.ts +34 -48
  27. package/src/components/AccountButton.tsx +17 -10
  28. package/src/i18n/en.json +263 -0
  29. package/src/i18n/fr.json +261 -0
  30. package/src/web/admin/signup-stats.tsx +10 -3
  31. package/src/web/admin/user-detail.tsx +135 -56
  32. package/src/web/admin/users-by-signup-source.tsx +60 -21
  33. package/src/web/admin/users.tsx +41 -18
  34. package/src/web/auth/dashboard.tsx +25 -9
  35. package/src/web/auth/folder.tsx +11 -3
  36. package/src/web/auth/profile.tsx +63 -29
  37. package/src/web/auth/reglage.tsx +43 -19
  38. package/src/web/public/SignInPage.tsx +32 -70
  39. package/src/web/public/SignUpPage.tsx +48 -26
  40. package/supabase/migrations/20251112000000_user_init.sql +35 -19
  41. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +8 -3
  42. package/supabase/migrations/20251112000002_sync_avatars.sql +7 -1
  43. 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 { useRouter, useSearchParams } from "next/navigation";
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
- const router = useRouter();
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
- console.log("Signed in user:", data.user);
54
+
50
55
  addToast({
51
56
  color: "success",
52
- title: "Connecté avec succès !",
53
- description: `Bienvenue ${data.user?.email}`,
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-400">
137
- Connectez-vous pour accéder à votre espace
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="votre@email.com"
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">OU</span>
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-xs text-default-400">
234
- En vous connectant, vous acceptez nos{" "}
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
- conditions d'utilisation
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 { useRouter, useSearchParams } from "next/navigation";
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 { supabaseBrowserClient } from "@lastbrain/core";
22
+ import { useModuleTranslation } from "@lastbrain/core";
23
+ import { useLocalizedRouter } from "@lastbrain/core";
23
24
 
24
25
  function SignUpForm() {
25
- const router = useRouter();
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("Les mots de passe ne correspondent pas.");
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("Le mot de passe doit contenir au moins 6 caractères.");
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("/signin");
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-400">
126
- Créez votre compte en quelques secondes
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="Jean Dupont"
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="votre@email.com"
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="Minimum 6 caractères"
191
+ description={
192
+ t("signup.password_hint") || "Minimum 6 caractères"
193
+ }
178
194
  />
179
195
 
180
196
  <Input
181
- label="Confirmer le mot de passe"
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">OU</span>
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
- <p className="mt-8 text-center text-xs text-default-400">
257
- En créant un compte, vous acceptez nos{" "}
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
- ALTER PUBLICATION supabase_realtime ADD TABLE public.user_notifications;
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