@lastbrain/module-auth 0.1.3 → 0.1.5
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/README.md +38 -9
- package/dist/api/admin/users.d.ts +1 -28
- package/dist/api/admin/users.d.ts.map +1 -1
- package/dist/api/admin/users.js +14 -63
- package/dist/api/auth/me.d.ts +3 -3
- package/dist/api/auth/me.d.ts.map +1 -1
- package/dist/api/auth/me.js +3 -5
- package/dist/api/auth/profile.d.ts.map +1 -1
- package/dist/api/auth/profile.js +4 -8
- package/dist/api/public/signin.js +3 -3
- package/dist/api/storage.d.ts.map +1 -1
- package/dist/api/storage.js +1 -1
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +21 -2
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +16 -9
- package/dist/web/auth/dashboard.d.ts.map +1 -1
- package/dist/web/auth/dashboard.js +2 -2
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +44 -4
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.d.ts.map +1 -1
- package/dist/web/public/SignInPage.js +1 -1
- package/dist/web/public/SignUpPage.js +1 -1
- package/package.json +3 -2
- package/src/api/admin/users.ts +21 -89
- package/src/api/auth/me.ts +7 -14
- package/src/api/auth/profile.ts +10 -24
- package/src/api/public/signin.ts +3 -3
- package/src/api/storage.ts +8 -5
- package/src/auth.build.config.ts +21 -2
- package/src/web/admin/users.tsx +37 -11
- package/src/web/auth/dashboard.tsx +15 -8
- package/src/web/auth/profile.tsx +49 -7
- package/src/web/auth/reglage.tsx +17 -35
- package/src/web/public/SignInPage.tsx +1 -2
- package/src/web/public/SignUpPage.tsx +2 -2
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/migrations/20251112000000_user_init.sql +1 -1
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +206 -0
- package/supabase/migrations/20251112000002_sync_avatars.sql +54 -0
- package/supabase/migrations-down/20251112000000_user_init.down.sql +2 -0
- package/supabase/migrations-down/20251112000001_auto_profile_and_admin_view.down.sql +23 -0
- package/supabase/migrations-down/20251112000002_sync_avatars.down.sql +9 -0
package/src/web/auth/profile.tsx
CHANGED
|
@@ -43,7 +43,8 @@ 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 [
|
|
46
|
+
const [_error, setError] = useState<string | null>(null);
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
48
|
const [currentUser, setCurrentUser] = useState<any>(null);
|
|
48
49
|
|
|
49
50
|
useEffect(() => {
|
|
@@ -145,19 +146,19 @@ export function ProfilePage() {
|
|
|
145
146
|
"avatar",
|
|
146
147
|
`${currentUser.id}_32_${version}.webp`,
|
|
147
148
|
files.small,
|
|
148
|
-
"image/webp"
|
|
149
|
+
"image/webp",
|
|
149
150
|
);
|
|
150
151
|
urls.medium = await uploadFile(
|
|
151
152
|
"avatar",
|
|
152
153
|
`${currentUser.id}_64_${version}.webp`,
|
|
153
154
|
files.medium,
|
|
154
|
-
"image/webp"
|
|
155
|
+
"image/webp",
|
|
155
156
|
);
|
|
156
157
|
urls.large = await uploadFile(
|
|
157
158
|
"avatar",
|
|
158
159
|
`${currentUser.id}_128_${version}.webp`,
|
|
159
160
|
files.large,
|
|
160
|
-
"image/webp"
|
|
161
|
+
"image/webp",
|
|
161
162
|
);
|
|
162
163
|
|
|
163
164
|
// Update user metadata
|
|
@@ -172,8 +173,30 @@ export function ProfilePage() {
|
|
|
172
173
|
},
|
|
173
174
|
});
|
|
174
175
|
|
|
175
|
-
// Update profile avatar_url
|
|
176
|
-
|
|
176
|
+
// Update profile avatar_url in database
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetch("/api/auth/profile", {
|
|
179
|
+
method: "PATCH",
|
|
180
|
+
headers: {
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
console.error("Failed to update avatar_url in profile");
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("Error updating profile avatar_url:", error);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Update profile avatar_url locally
|
|
196
|
+
setProfile((prev) => ({
|
|
197
|
+
...prev,
|
|
198
|
+
avatar_url: `/avatar/${currentUser.id}_128_${version}.webp`,
|
|
199
|
+
}));
|
|
177
200
|
|
|
178
201
|
return urls;
|
|
179
202
|
};
|
|
@@ -196,7 +219,26 @@ export function ProfilePage() {
|
|
|
196
219
|
},
|
|
197
220
|
});
|
|
198
221
|
|
|
199
|
-
// Update profile
|
|
222
|
+
// Update profile avatar_url in database
|
|
223
|
+
try {
|
|
224
|
+
const response = await fetch("/api/auth/profile", {
|
|
225
|
+
method: "PATCH",
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
avatar_url: null,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
console.error("Failed to update avatar_url in profile");
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error("Error updating profile avatar_url:", error);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Update profile locally
|
|
200
242
|
setProfile((prev) => ({ ...prev, avatar_url: "" }));
|
|
201
243
|
};
|
|
202
244
|
|
package/src/web/auth/reglage.tsx
CHANGED
|
@@ -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
|
-
|
|
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={
|
|
233
|
+
selectedKeys={
|
|
234
|
+
preferences.language ? [preferences.language] : []
|
|
235
|
+
}
|
|
240
236
|
onChange={(e) => handleSelect("language", e.target.value)}
|
|
241
237
|
>
|
|
242
|
-
<SelectItem key="en">
|
|
243
|
-
|
|
244
|
-
</SelectItem>
|
|
245
|
-
<SelectItem key="
|
|
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={
|
|
246
|
+
selectedKeys={
|
|
247
|
+
preferences.timezone ? [preferences.timezone] : []
|
|
248
|
+
}
|
|
259
249
|
onChange={(e) => handleSelect("timezone", e.target.value)}
|
|
260
250
|
>
|
|
261
|
-
<SelectItem key="UTC">
|
|
262
|
-
|
|
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,
|
|
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
|
|
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();
|