@lastbrain/module-auth 0.1.3 → 0.1.4

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 (44) hide show
  1. package/README.md +38 -9
  2. package/dist/api/admin/users.d.ts +1 -28
  3. package/dist/api/admin/users.d.ts.map +1 -1
  4. package/dist/api/admin/users.js +12 -64
  5. package/dist/api/auth/me.d.ts +3 -3
  6. package/dist/api/auth/me.d.ts.map +1 -1
  7. package/dist/api/auth/me.js +3 -5
  8. package/dist/api/auth/profile.d.ts.map +1 -1
  9. package/dist/api/auth/profile.js +4 -8
  10. package/dist/api/public/signin.js +3 -3
  11. package/dist/api/storage.d.ts.map +1 -1
  12. package/dist/api/storage.js +1 -1
  13. package/dist/auth.build.config.d.ts.map +1 -1
  14. package/dist/auth.build.config.js +21 -2
  15. package/dist/web/admin/users.d.ts.map +1 -1
  16. package/dist/web/admin/users.js +16 -9
  17. package/dist/web/auth/dashboard.d.ts.map +1 -1
  18. package/dist/web/auth/dashboard.js +2 -2
  19. package/dist/web/auth/profile.d.ts.map +1 -1
  20. package/dist/web/auth/profile.js +43 -4
  21. package/dist/web/auth/reglage.d.ts.map +1 -1
  22. package/dist/web/public/SignInPage.d.ts.map +1 -1
  23. package/dist/web/public/SignInPage.js +1 -1
  24. package/dist/web/public/SignUpPage.js +1 -1
  25. package/package.json +3 -2
  26. package/src/api/admin/users.ts +17 -90
  27. package/src/api/auth/me.ts +8 -17
  28. package/src/api/auth/profile.ts +10 -24
  29. package/src/api/public/signin.ts +3 -3
  30. package/src/api/storage.ts +8 -5
  31. package/src/auth.build.config.ts +21 -2
  32. package/src/web/admin/users.tsx +37 -11
  33. package/src/web/auth/dashboard.tsx +14 -7
  34. package/src/web/auth/profile.tsx +45 -4
  35. package/src/web/auth/reglage.tsx +17 -35
  36. package/src/web/public/SignInPage.tsx +1 -2
  37. package/src/web/public/SignUpPage.tsx +2 -2
  38. package/supabase/.temp/cli-latest +1 -0
  39. package/supabase/migrations/20251112000000_user_init.sql +1 -1
  40. package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +206 -0
  41. package/supabase/migrations/20251112000002_sync_avatars.sql +54 -0
  42. package/supabase/migrations-down/20251112000000_user_init.down.sql +2 -0
  43. package/supabase/migrations-down/20251112000001_auto_profile_and_admin_view.down.sql +23 -0
  44. package/supabase/migrations-down/20251112000002_sync_avatars.down.sql +9 -0
@@ -43,7 +43,7 @@ export function ProfilePage() {
43
43
  const [profile, setProfile] = useState<ProfileData>({});
44
44
  const [isLoading, setIsLoading] = useState(true);
45
45
  const [isSaving, setIsSaving] = useState(false);
46
- const [error, setError] = useState<string | null>(null);
46
+ const [_error, setError] = useState<string | null>(null);
47
47
  const [currentUser, setCurrentUser] = useState<any>(null);
48
48
 
49
49
  useEffect(() => {
@@ -172,8 +172,30 @@ export function ProfilePage() {
172
172
  },
173
173
  });
174
174
 
175
- // Update profile avatar_url
176
- setProfile((prev) => ({ ...prev, avatar_url: urls.large }));
175
+ // Update profile avatar_url in database
176
+ try {
177
+ const response = await fetch("/api/auth/profile", {
178
+ method: "PATCH",
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ },
182
+ body: JSON.stringify({
183
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
184
+ }),
185
+ });
186
+
187
+ if (!response.ok) {
188
+ console.error("Failed to update avatar_url in profile");
189
+ }
190
+ } catch (error) {
191
+ console.error("Error updating profile avatar_url:", error);
192
+ }
193
+
194
+ // Update profile avatar_url locally
195
+ setProfile((prev) => ({
196
+ ...prev,
197
+ avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
198
+ }));
177
199
 
178
200
  return urls;
179
201
  };
@@ -196,7 +218,26 @@ export function ProfilePage() {
196
218
  },
197
219
  });
198
220
 
199
- // Update profile
221
+ // Update profile avatar_url in database
222
+ try {
223
+ const response = await fetch("/api/auth/profile", {
224
+ method: "PATCH",
225
+ headers: {
226
+ "Content-Type": "application/json",
227
+ },
228
+ body: JSON.stringify({
229
+ avatar_url: null,
230
+ }),
231
+ });
232
+
233
+ if (!response.ok) {
234
+ console.error("Failed to update avatar_url in profile");
235
+ }
236
+ } catch (error) {
237
+ console.error("Error updating profile avatar_url:", error);
238
+ }
239
+
240
+ // Update profile locally
200
241
  setProfile((prev) => ({ ...prev, avatar_url: "" }));
201
242
  };
202
243
 
@@ -212,15 +212,9 @@ export function ReglagePage() {
212
212
  selectedKeys={preferences.theme ? [preferences.theme] : []}
213
213
  onChange={(e) => handleSelect("theme", e.target.value)}
214
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>
215
+ <SelectItem key="light">Light</SelectItem>
216
+ <SelectItem key="dark">Dark</SelectItem>
217
+ <SelectItem key="system">System</SelectItem>
224
218
  </Select>
225
219
  </CardBody>
226
220
  </Card>
@@ -236,43 +230,31 @@ export function ReglagePage() {
236
230
  <Select
237
231
  label="Language"
238
232
  placeholder="Select a language"
239
- selectedKeys={preferences.language ? [preferences.language] : []}
233
+ selectedKeys={
234
+ preferences.language ? [preferences.language] : []
235
+ }
240
236
  onChange={(e) => handleSelect("language", e.target.value)}
241
237
  >
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>
238
+ <SelectItem key="en">English</SelectItem>
239
+ <SelectItem key="fr">Français</SelectItem>
240
+ <SelectItem key="es">Español</SelectItem>
241
+ <SelectItem key="de">Deutsch</SelectItem>
254
242
  </Select>
255
243
  <Select
256
244
  label="Timezone"
257
245
  placeholder="Select a timezone"
258
- selectedKeys={preferences.timezone ? [preferences.timezone] : []}
246
+ selectedKeys={
247
+ preferences.timezone ? [preferences.timezone] : []
248
+ }
259
249
  onChange={(e) => handleSelect("timezone", e.target.value)}
260
250
  >
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>
251
+ <SelectItem key="UTC">UTC</SelectItem>
252
+ <SelectItem key="Europe/Paris">Europe/Paris</SelectItem>
253
+ <SelectItem key="America/New_York">America/New_York</SelectItem>
270
254
  <SelectItem key="America/Los_Angeles">
271
255
  America/Los_Angeles
272
256
  </SelectItem>
273
- <SelectItem key="Asia/Tokyo">
274
- Asia/Tokyo
275
- </SelectItem>
257
+ <SelectItem key="Asia/Tokyo">Asia/Tokyo</SelectItem>
276
258
  </Select>
277
259
  </div>
278
260
  </CardBody>
@@ -8,7 +8,6 @@ import {
8
8
  Button,
9
9
  Card,
10
10
  CardBody,
11
- CardHeader,
12
11
  Chip,
13
12
  Input,
14
13
  Link,
@@ -22,7 +21,7 @@ function SignInForm() {
22
21
  const [email, setEmail] = useState("");
23
22
  const [password, setPassword] = useState("");
24
23
  const [isLoading, setIsLoading] = useState(false);
25
- const [error, setError] = useState<string | null>(null);
24
+ const [error, _setError] = useState<string | null>(null);
26
25
  const redirectUrl = searchParams.get("redirect");
27
26
  const router = useRouter();
28
27
  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
@@ -75,7 +75,7 @@ function SignUpForm() {
75
75
  // Si la confirmation par email est requise
76
76
  if (data.user && !data.session) {
77
77
  setSuccess(
78
- "Compte créé avec succès ! Veuillez vérifier votre email pour confirmer votre compte."
78
+ "Compte créé avec succès ! Veuillez vérifier votre email pour confirmer votre compte.",
79
79
  );
80
80
  return;
81
81
  }
@@ -98,7 +98,7 @@ function SignUpForm() {
98
98
  router.push("/signin");
99
99
  }
100
100
  }, 2000);
101
- } catch (err) {
101
+ } catch {
102
102
  addToast({
103
103
  title: "Erreur",
104
104
  description: "Une erreur inattendue est survenue.",
@@ -0,0 +1 @@
1
+ v2.58.5
@@ -172,4 +172,4 @@ DROP TRIGGER IF EXISTS set_user_notifications_updated_at ON public.user_notifica
172
172
  CREATE TRIGGER set_user_notifications_updated_at
173
173
  BEFORE UPDATE ON public.user_notifications
174
174
  FOR EACH ROW
175
- EXECUTE FUNCTION public.set_user_notifications_updated_at();
175
+ EXECUTE FUNCTION public.set_user_notifications_updated_at();
@@ -0,0 +1,206 @@
1
+ -- Auto-create user profile when user signs up and admin RPC
2
+ -- Module: @lastbrain/module-auth
3
+
4
+ -- Add unique constraint on owner_id if not exists
5
+ DO $$
6
+ BEGIN
7
+ IF NOT EXISTS (
8
+ SELECT 1 FROM information_schema.table_constraints
9
+ WHERE constraint_name = 'user_profil_owner_id_key'
10
+ AND table_name = 'user_profil'
11
+ ) THEN
12
+ ALTER TABLE public.user_profil ADD CONSTRAINT user_profil_owner_id_key UNIQUE (owner_id);
13
+ END IF;
14
+ END $$;
15
+
16
+ -- Auto-create user profile when user signs up
17
+ -- This ensures every user has a profile in user_profil table
18
+
19
+ -- Function to create user profile automatically
20
+ CREATE OR REPLACE FUNCTION public.handle_new_user()
21
+ RETURNS TRIGGER AS $$
22
+ BEGIN
23
+ INSERT INTO public.user_profil (owner_id, created_at, updated_at)
24
+ VALUES (NEW.id, now(), now());
25
+ RETURN NEW;
26
+ EXCEPTION
27
+ WHEN unique_violation THEN
28
+ -- Profile already exists, ignore
29
+ RETURN NEW;
30
+ END;
31
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
32
+
33
+ -- Trigger to call the function when a new user is created
34
+ DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
35
+ CREATE TRIGGER on_auth_user_created
36
+ AFTER INSERT ON auth.users
37
+ FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
38
+
39
+ -- =====================================================
40
+ -- Function: get_admin_users
41
+ -- =====================================================
42
+ -- RPC function for admins to get user data with emails
43
+ CREATE OR REPLACE FUNCTION public.get_admin_users(
44
+ page_number INTEGER DEFAULT 1,
45
+ page_size INTEGER DEFAULT 20,
46
+ search_term TEXT DEFAULT ''
47
+ )
48
+ RETURNS JSON
49
+ LANGUAGE plpgsql
50
+ SECURITY DEFINER
51
+ AS $$
52
+ DECLARE
53
+ offset_val INTEGER;
54
+ result JSON;
55
+ total_count INTEGER;
56
+ BEGIN
57
+ -- Check if user is superadmin
58
+ IF NOT is_superadmin(auth.uid()) THEN
59
+ RAISE EXCEPTION 'Access denied. Superadmin required.';
60
+ END IF;
61
+
62
+ offset_val := (page_number - 1) * page_size;
63
+
64
+ -- Get total count first
65
+ SELECT COUNT(*) INTO total_count
66
+ FROM public.user_profil p
67
+ LEFT JOIN auth.users au ON p.owner_id = au.id
68
+ WHERE
69
+ CASE
70
+ WHEN search_term = '' THEN true
71
+ ELSE (
72
+ p.first_name ILIKE '%' || search_term || '%' OR
73
+ p.last_name ILIKE '%' || search_term || '%' OR
74
+ au.email ILIKE '%' || search_term || '%' OR
75
+ (au.raw_user_meta_data->>'full_name') ILIKE '%' || search_term || '%' OR
76
+ (au.raw_app_meta_data->'roles'->>0) ILIKE '%' || search_term || '%'
77
+ )
78
+ END;
79
+
80
+ -- Build the result JSON
81
+ SELECT json_build_object(
82
+ 'data', COALESCE(json_agg(
83
+ json_build_object(
84
+ 'id', fp.owner_id,
85
+ 'email', COALESCE(au.email, 'N/A'),
86
+ 'created_at', COALESCE(au.created_at, fp.created_at),
87
+ 'email_confirmed_at', au.email_confirmed_at,
88
+ 'last_sign_in_at', au.last_sign_in_at,
89
+ 'role', COALESCE(au.raw_app_meta_data->'roles'->>0, 'user'),
90
+ 'full_name', au.raw_user_meta_data->>'full_name',
91
+ 'avatar_path', au.raw_user_meta_data->>'avatar',
92
+ 'metadata', au.raw_user_meta_data,
93
+ 'profile', json_build_object(
94
+ 'first_name', fp.first_name,
95
+ 'last_name', fp.last_name,
96
+ 'avatar_url', fp.avatar_url,
97
+ 'bio', fp.bio,
98
+ 'phone', fp.phone,
99
+ 'company', fp.company,
100
+ 'website', fp.website,
101
+ 'location', fp.location,
102
+ 'language', fp.language,
103
+ 'timezone', fp.timezone
104
+ )
105
+ )
106
+ ), '[]'::json),
107
+ 'pagination', json_build_object(
108
+ 'page', page_number,
109
+ 'per_page', page_size,
110
+ 'total', total_count,
111
+ 'total_pages', CEIL(total_count::DECIMAL / page_size)
112
+ )
113
+ ) INTO result
114
+ FROM (
115
+ SELECT
116
+ p.id,
117
+ p.owner_id,
118
+ p.first_name,
119
+ p.last_name,
120
+ p.avatar_url,
121
+ p.bio,
122
+ p.phone,
123
+ p.company,
124
+ p.website,
125
+ p.location,
126
+ p.language,
127
+ p.timezone,
128
+ p.created_at
129
+ FROM public.user_profil p
130
+ LEFT JOIN auth.users au ON p.owner_id = au.id
131
+ WHERE
132
+ CASE
133
+ WHEN search_term = '' THEN true
134
+ ELSE (
135
+ p.first_name ILIKE '%' || search_term || '%' OR
136
+ p.last_name ILIKE '%' || search_term || '%' OR
137
+ au.email ILIKE '%' || search_term || '%' OR
138
+ (au.raw_user_meta_data->>'full_name') ILIKE '%' || search_term || '%' OR
139
+ (au.raw_app_meta_data->'roles'->>0) ILIKE '%' || search_term || '%'
140
+ )
141
+ END
142
+ ORDER BY p.created_at DESC
143
+ LIMIT page_size
144
+ OFFSET offset_val
145
+ ) fp
146
+ LEFT JOIN auth.users au ON fp.owner_id = au.id;
147
+
148
+ RETURN result;
149
+ END;
150
+ $$;
151
+
152
+ -- =====================================================
153
+ -- Function: sync_fullname_to_metadata
154
+ -- =====================================================
155
+ -- Synchronize full_name in auth.users metadata when profile is updated
156
+ CREATE OR REPLACE FUNCTION public.sync_fullname_to_metadata()
157
+ RETURNS TRIGGER AS $$
158
+ DECLARE
159
+ full_name_value TEXT;
160
+ current_metadata JSONB;
161
+ BEGIN
162
+ -- Build full_name from first_name and last_name
163
+ full_name_value := TRIM(CONCAT(COALESCE(NEW.first_name, ''), ' ', COALESCE(NEW.last_name, '')));
164
+
165
+ -- If full_name is empty or just spaces, set to null
166
+ IF full_name_value = '' OR full_name_value IS NULL THEN
167
+ full_name_value := NULL;
168
+ END IF;
169
+
170
+ -- Get current metadata
171
+ SELECT COALESCE(raw_user_meta_data, '{}'::jsonb)
172
+ INTO current_metadata
173
+ FROM auth.users
174
+ WHERE id = NEW.owner_id;
175
+
176
+ -- Update metadata with new full_name
177
+ UPDATE auth.users
178
+ SET raw_user_meta_data = current_metadata || jsonb_build_object('full_name', full_name_value)
179
+ WHERE id = NEW.owner_id;
180
+
181
+ RETURN NEW;
182
+ END;
183
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
184
+
185
+ -- Trigger to sync full_name when profile is updated
186
+ DROP TRIGGER IF EXISTS sync_fullname_on_profile_update ON public.user_profil;
187
+ CREATE TRIGGER sync_fullname_on_profile_update
188
+ AFTER UPDATE OF first_name, last_name ON public.user_profil
189
+ FOR EACH ROW
190
+ WHEN (OLD.first_name IS DISTINCT FROM NEW.first_name OR OLD.last_name IS DISTINCT FROM NEW.last_name)
191
+ EXECUTE FUNCTION public.sync_fullname_to_metadata();
192
+
193
+ -- Trigger to sync full_name when profile is inserted
194
+ DROP TRIGGER IF EXISTS sync_fullname_on_profile_insert ON public.user_profil;
195
+ CREATE TRIGGER sync_fullname_on_profile_insert
196
+ AFTER INSERT ON public.user_profil
197
+ FOR EACH ROW
198
+ WHEN (NEW.first_name IS NOT NULL OR NEW.last_name IS NOT NULL)
199
+ EXECUTE FUNCTION public.sync_fullname_to_metadata();
200
+
201
+ -- Create profile for existing users who don't have one (if any)
202
+ INSERT INTO public.user_profil (owner_id, created_at, updated_at)
203
+ SELECT id, created_at, created_at
204
+ FROM auth.users
205
+ WHERE id NOT IN (SELECT owner_id FROM public.user_profil WHERE owner_id IS NOT NULL)
206
+ ON CONFLICT (owner_id) DO NOTHING;
@@ -0,0 +1,54 @@
1
+ -- Sync avatar_url from auth.users metadata to user_profil
2
+ -- Module: @lastbrain/module-auth
3
+
4
+ -- Function to sync avatars from auth metadata to user_profil
5
+ CREATE OR REPLACE FUNCTION public.sync_avatar_from_auth_metadata()
6
+ RETURNS VOID
7
+ LANGUAGE plpgsql
8
+ SECURITY DEFINER
9
+ AS $$
10
+ BEGIN
11
+ -- Update user_profil with avatar URLs from auth.users metadata
12
+ UPDATE public.user_profil
13
+ SET avatar_url = CASE
14
+ WHEN (au.raw_user_meta_data->>'avatar') IS NOT NULL
15
+ THEN concat('/', au.raw_user_meta_data->>'avatar')
16
+ ELSE avatar_url
17
+ END
18
+ FROM auth.users au
19
+ WHERE public.user_profil.owner_id = au.id
20
+ AND (au.raw_user_meta_data->>'avatar') IS NOT NULL
21
+ AND public.user_profil.avatar_url IS NULL;
22
+
23
+ -- Log the number of updated records
24
+ RAISE NOTICE 'Avatar sync completed';
25
+ END;
26
+ $$;
27
+
28
+ -- Create a trigger to automatically sync avatar when auth.users is updated
29
+ CREATE OR REPLACE FUNCTION public.handle_auth_user_avatar_update()
30
+ RETURNS TRIGGER AS $$
31
+ BEGIN
32
+ -- Only update if the avatar metadata changed
33
+ IF (OLD.raw_user_meta_data->>'avatar') IS DISTINCT FROM (NEW.raw_user_meta_data->>'avatar') THEN
34
+ UPDATE public.user_profil
35
+ SET avatar_url = CASE
36
+ WHEN (NEW.raw_user_meta_data->>'avatar') IS NOT NULL
37
+ THEN concat('/', NEW.raw_user_meta_data->>'avatar')
38
+ ELSE NULL
39
+ END
40
+ WHERE owner_id = NEW.id;
41
+ END IF;
42
+
43
+ RETURN NEW;
44
+ END;
45
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
46
+
47
+ -- Create trigger for avatar sync on auth.users update
48
+ DROP TRIGGER IF EXISTS on_auth_user_avatar_updated ON auth.users;
49
+ CREATE TRIGGER on_auth_user_avatar_updated
50
+ AFTER UPDATE ON auth.users
51
+ FOR EACH ROW EXECUTE FUNCTION public.handle_auth_user_avatar_update();
52
+
53
+ -- Run initial sync for existing users
54
+ SELECT public.sync_avatar_from_auth_metadata();
@@ -40,6 +40,8 @@ DROP TABLE IF EXISTS public.user_address;
40
40
  -- =====================================================
41
41
  DROP TRIGGER IF EXISTS set_user_profil_updated_at ON public.user_profil;
42
42
  DROP FUNCTION IF EXISTS public.set_user_profil_updated_at();
43
+ DROP FUNCTION IF EXISTS public.handle_new_user();
44
+
43
45
 
44
46
  DROP POLICY IF EXISTS user_profil_superadmin_all ON public.user_profil;
45
47
  DROP POLICY IF EXISTS user_profil_owner_delete ON public.user_profil;
@@ -0,0 +1,23 @@
1
+ -- Rollback auto-create user profile and admin RPC
2
+ -- Module: @lastbrain/module-auth
3
+
4
+ -- Drop the RPC function
5
+ DROP FUNCTION IF EXISTS public.get_admin_users(INTEGER, INTEGER, TEXT);
6
+
7
+ -- Drop the trigger first
8
+ DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
9
+
10
+ -- Drop the function
11
+ DROP FUNCTION IF EXISTS public.handle_new_user() CASCADE;
12
+
13
+ -- Remove unique constraint on owner_id if exists
14
+ DO $$
15
+ BEGIN
16
+ IF EXISTS (
17
+ SELECT 1 FROM information_schema.table_constraints
18
+ WHERE constraint_name = 'user_profil_owner_id_key'
19
+ AND table_name = 'user_profil'
20
+ ) THEN
21
+ ALTER TABLE public.user_profil DROP CONSTRAINT user_profil_owner_id_key;
22
+ END IF;
23
+ END $$;
@@ -0,0 +1,9 @@
1
+ -- Rollback avatar sync functionality
2
+ -- Module: @lastbrain/module-auth
3
+
4
+ -- Drop the trigger
5
+ DROP TRIGGER IF EXISTS on_auth_user_avatar_updated ON auth.users;
6
+
7
+ -- Drop the functions
8
+ DROP FUNCTION IF EXISTS public.handle_auth_user_avatar_update();
9
+ DROP FUNCTION IF EXISTS public.sync_avatar_from_auth_metadata();