Skip to content

Commit d5c853a

Browse files
committed
fix(auth): check username before creation
1 parent d6055b0 commit d5c853a

File tree

5 files changed

+117
-43
lines changed

5 files changed

+117
-43
lines changed

src/components/AuthDialog.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,20 @@ interface AuthDialogProps {
2626
groupName?: string;
2727
}
2828

29-
export const AuthDialog = ({
29+
export function AuthDialog({
3030
open,
3131
onOpenChange,
3232
onSuccess,
3333
inviteToken,
3434
groupName,
35-
}: AuthDialogProps) => {
35+
}: AuthDialogProps) {
3636
const [email, setEmail] = useState("");
3737
const [otp, setOtp] = useState("");
3838
const [loading, setLoading] = useState(false);
3939
const [step, setStep] = useState<"email" | "otp">("email");
4040
const { toast } = useToast();
4141

42-
const handleSendMagicLink = async (e: React.FormEvent) => {
42+
async function handleSendMagicLink(e: React.FormEvent) {
4343
e.preventDefault();
4444
setLoading(true);
4545

@@ -71,9 +71,9 @@ export const AuthDialog = ({
7171
setStep("otp");
7272
}
7373
setLoading(false);
74-
};
74+
}
7575

76-
const handleVerifyOtp = async (e: React.FormEvent) => {
76+
async function handleVerifyOtp(e: React.FormEvent) {
7777
e.preventDefault();
7878
if (otp.length !== 6) return;
7979

@@ -99,16 +99,16 @@ export const AuthDialog = ({
9999
onSuccess();
100100
}
101101
setLoading(false);
102-
};
102+
}
103103

104-
const handleResend = async () => {
104+
async function handleResend() {
105105
await handleSendMagicLink({ preventDefault: () => {} } as React.FormEvent);
106-
};
106+
}
107107

108-
const handleBack = () => {
108+
function handleBack() {
109109
setStep("email");
110110
setOtp("");
111-
};
111+
}
112112

113113
return (
114114
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -208,4 +208,4 @@ export const AuthDialog = ({
208208
</DialogContent>
209209
</Dialog>
210210
);
211-
};
211+
}

src/hooks/queries/useProfileQuery.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { profileOfflineService } from "@/services/profileOfflineService";
88
import { useOfflineProfileToast } from "@/hooks/useOfflineProfileToast";
99

10-
export const useProfileQuery = (userId?: string) => {
10+
export function useProfileQuery(userId?: string) {
1111
const { showOfflineProfileToast, isOnline } = useOfflineProfileToast();
1212

1313
return useQuery({
@@ -55,9 +55,9 @@ export const useProfileQuery = (userId?: string) => {
5555
return failureCount < 2;
5656
},
5757
});
58-
};
58+
}
5959

60-
export const useUpdateProfileMutation = () => {
60+
export function useUpdateProfileMutation() {
6161
const queryClient = useQueryClient();
6262

6363
return useMutation({
@@ -75,4 +75,4 @@ export const useUpdateProfileMutation = () => {
7575
});
7676
},
7777
});
78-
};
78+
}

src/integrations/supabase/types.ts

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,10 @@ export type Json =
77
| Json[];
88

99
export type Database = {
10-
graphql_public: {
11-
Tables: {
12-
[_ in never]: never;
13-
};
14-
Views: {
15-
[_ in never]: never;
16-
};
17-
Functions: {
18-
graphql: {
19-
Args: {
20-
extensions?: Json;
21-
operationName?: string;
22-
query?: string;
23-
variables?: Json;
24-
};
25-
Returns: Json;
26-
};
27-
};
28-
Enums: {
29-
[_ in never]: never;
30-
};
31-
CompositeTypes: {
32-
[_ in never]: never;
33-
};
10+
// Allows to automatically instanciate createClient with right options
11+
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
12+
__InternalSupabase: {
13+
PostgrestVersion: "12.2.3 (519615d)";
3414
};
3515
public: {
3616
Tables: {
@@ -626,6 +606,10 @@ export type Database = {
626606
Args: { check_user_id: string };
627607
Returns: boolean;
628608
};
609+
check_username_exists: {
610+
Args: { check_username: string; exclude_user_id?: string };
611+
Returns: boolean;
612+
};
629613
get_user_id_by_email: {
630614
Args: { user_email: string };
631615
Returns: string;
@@ -671,6 +655,10 @@ export type Database = {
671655
reason: string;
672656
}[];
673657
};
658+
validate_profile_update: {
659+
Args: { user_id: string; new_username?: string };
660+
Returns: string;
661+
};
674662
};
675663
Enums: {
676664
admin_role: "super_admin" | "admin" | "moderator";
@@ -802,9 +790,6 @@ export type CompositeTypes<
802790
: never;
803791

804792
export const Constants = {
805-
graphql_public: {
806-
Enums: {},
807-
},
808793
public: {
809794
Enums: {
810795
admin_role: ["super_admin", "admin", "moderator"],

src/services/queries.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1490,10 +1490,32 @@ export const mutationFunctions = {
14901490

14911491
async updateProfile(variables: {
14921492
userId: string;
1493-
updates: { email?: string | null; username?: string | null };
1493+
updates: { username?: string | null };
14941494
}) {
14951495
const { userId, updates } = variables;
14961496

1497+
// Validate username uniqueness before attempting update
1498+
const { data: validationResult, error: validationError } =
1499+
await supabase.rpc("validate_profile_update", {
1500+
user_id: userId,
1501+
new_username: updates.username?.trim(),
1502+
});
1503+
1504+
console.log(
1505+
"Validation result:",
1506+
validationResult,
1507+
"Error:",
1508+
validationError,
1509+
);
1510+
1511+
if (validationError) {
1512+
throw new Error("Failed to validate profile data");
1513+
}
1514+
1515+
if (validationResult) {
1516+
throw new Error(validationResult);
1517+
}
1518+
14971519
const { data, error } = await supabase
14981520
.from("profiles")
14991521
.update(updates)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
-- Add case-insensitive uniqueness validation for profiles
2+
-- This ensures username and email are unique regardless of case
3+
4+
-- Function to check if username exists (case insensitive)
5+
CREATE OR REPLACE FUNCTION public.check_username_exists(check_username TEXT, exclude_user_id UUID DEFAULT NULL)
6+
RETURNS BOOLEAN
7+
LANGUAGE plpgsql
8+
SECURITY DEFINER SET search_path = ''
9+
AS $function$
10+
DECLARE
11+
exists_count INTEGER;
12+
BEGIN
13+
-- Count users with same username (case insensitive), excluding current user if provided
14+
SELECT COUNT(*) INTO exists_count
15+
FROM public.profiles
16+
WHERE LOWER(username) = LOWER(check_username)
17+
AND (exclude_user_id IS NULL OR id != exclude_user_id);
18+
19+
RETURN exists_count > 0;
20+
END;
21+
$function$;
22+
23+
-- Note: Email validation removed as Supabase Auth handles email case-insensitivity
24+
25+
-- Function to validate profile updates (username only)
26+
CREATE OR REPLACE FUNCTION public.validate_profile_update(
27+
user_id UUID,
28+
new_username TEXT DEFAULT NULL
29+
)
30+
RETURNS TEXT
31+
LANGUAGE plpgsql
32+
SECURITY DEFINER SET search_path = ''
33+
AS $function$
34+
BEGIN
35+
-- Check username uniqueness only if provided and not empty
36+
IF new_username IS NOT NULL AND TRIM(new_username) != '' AND public.check_username_exists(new_username, user_id) THEN
37+
RETURN 'Username already exists';
38+
END IF;
39+
40+
RETURN NULL; -- No conflicts found
41+
END;
42+
$function$;
43+
44+
-- Grant execute permissions
45+
GRANT EXECUTE ON FUNCTION public.check_username_exists(TEXT, UUID) TO authenticated, anon;
46+
GRANT EXECUTE ON FUNCTION public.validate_profile_update(UUID, TEXT) TO authenticated, anon;
47+
48+
-- Update the handle_new_user function to validate username uniqueness only
49+
CREATE OR REPLACE FUNCTION public.handle_new_user()
50+
RETURNS trigger AS $$
51+
DECLARE
52+
proposed_username TEXT;
53+
BEGIN
54+
proposed_username := NEW.raw_user_meta_data ->> 'username';
55+
56+
-- Check username uniqueness only if username is provided (not null and not empty)
57+
IF proposed_username IS NOT NULL AND TRIM(proposed_username) != '' AND public.check_username_exists(proposed_username, NULL::UUID) THEN
58+
RAISE EXCEPTION 'Username already exists';
59+
END IF;
60+
61+
-- Insert the profile (username can be null for initial login)
62+
INSERT INTO public.profiles (id, username, email)
63+
VALUES (NEW.id, CASE WHEN TRIM(COALESCE(proposed_username, '')) = '' THEN NULL ELSE proposed_username END, NEW.email);
64+
65+
RETURN NEW;
66+
END;
67+
$$ LANGUAGE plpgsql SECURITY DEFINER;

0 commit comments

Comments
 (0)