From 9e3d17721781aefb94869c5146d651e7cc3eb09d Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Wed, 15 Oct 2025 17:59:41 +0000 Subject: [PATCH 1/2] fix: GitHub handle validation - allow empty handles and prevent false conflicts --- .../application/commands/update_profile.rs | 36 ++++++----- backend/tests/profile_tests.rs | 62 +++++++++++++++++++ 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/backend/src/application/commands/update_profile.rs b/backend/src/application/commands/update_profile.rs index c3775f0..e94b524 100644 --- a/backend/src/application/commands/update_profile.rs +++ b/backend/src/application/commands/update_profile.rs @@ -19,25 +19,29 @@ pub async fn update_profile( profile.update_info(request.name, request.description, request.avatar_url); if let Some(ref handle) = request.github_login { - // 1. Trim and validate let trimmed = handle.trim(); - let valid_format = regex::Regex::new(r"^[a-zA-Z0-9-]{1,39}$").unwrap(); - if trimmed.is_empty() || !valid_format.is_match(trimmed) { - return Err("Invalid GitHub handle format".to_string()); - } - // 2. Check for conflicts - if profile_repository - .find_by_github_login(trimmed) - .await - .map_err(|e| e.to_string())? - .is_some() - { - return Err("GitHub handle already taken".to_string()); + // Allow empty handles (set to None) + if trimmed.is_empty() { + profile.github_login = None; + } else { + // Validate format for non-empty handles + let valid_format = regex::Regex::new(r"^[a-zA-Z0-9-]{1,39}$").unwrap(); + if !valid_format.is_match(trimmed) { + return Err("Invalid GitHub handle format".to_string()); + } + if let Some(conflicting_profile) = profile_repository + .find_by_github_login(trimmed) + .await + .map_err(|e| e.to_string())? + { + // Only conflict if it's not the current user's profile + if conflicting_profile.address != wallet_address { + return Err("GitHub handle already taken".to_string()); + } + } + profile.github_login = Some(trimmed.to_string()); } - - // 3. Assign it (preserving user’s original casing) - profile.github_login = Some(trimmed.to_string()); } profile_repository .update(&profile) diff --git a/backend/tests/profile_tests.rs b/backend/tests/profile_tests.rs index ca0a27b..f92a54c 100644 --- a/backend/tests/profile_tests.rs +++ b/backend/tests/profile_tests.rs @@ -182,4 +182,66 @@ mod github_handle_tests { let err_msg = err.unwrap_err(); assert!(err_msg.contains("GitHub handle already taken")); } + + #[tokio::test] + async fn empty_github_handle_allowed() { + let profile = Profile { + address: WalletAddress::new("0x1234567890123456789012345678901234567894".to_string()) + .unwrap(), + name: Some("Bob".into()), + description: None, + avatar_url: None, + github_login: Some("BobUser".into()), + login_nonce: 1, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + let repo = Arc::new(FakeRepo { + profiles: std::sync::Mutex::new(vec![profile.clone()]), + }); + + // Try updating to empty github handle + let req = UpdateProfileRequest { + name: None, + description: None, + avatar_url: None, + github_login: Some("".into()), + }; + + let result = update_profile(repo.clone(), profile.address.to_string(), req).await; + assert!(result.is_ok()); + let resp = result.unwrap(); + assert!(resp.github_login.is_none()); // Should be None, not Some("") + } + + #[tokio::test] + async fn user_can_update_own_github_handle() { + let profile = Profile { + address: WalletAddress::new("0x1234567890123456789012345678901234567895".to_string()) + .unwrap(), + name: Some("Charlie".into()), + description: None, + avatar_url: None, + github_login: Some("CharlieGit".into()), + login_nonce: 1, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + let repo = Arc::new(FakeRepo { + profiles: std::sync::Mutex::new(vec![profile.clone()]), + }); + + // Try updating with the same handle (should succeed) + let req = UpdateProfileRequest { + name: None, + description: None, + avatar_url: None, + github_login: Some("CharlieGit".into()), + }; + + let result = update_profile(repo.clone(), profile.address.to_string(), req).await; + assert!(result.is_ok()); + let resp = result.unwrap(); + assert_eq!(resp.github_login.unwrap(), "CharlieGit"); + } } From 0c1aae3932a76e18cccc8970331da5bf9b1ef338 Mon Sep 17 00:00:00 2001 From: Antoine Estienne Date: Thu, 16 Oct 2025 10:24:43 +0200 Subject: [PATCH 2/2] fix front end --- frontend/.astro/data-store.json | 2 +- .../action-buttons/CreateProfileDialog.tsx | 44 ++++++++++++++----- .../action-buttons/EditProfileDialog.tsx | 37 +++++++--------- .../components/profiles/list/ProfilesList.tsx | 13 ++++-- .../src/hooks/profiles/use-create-profile.ts | 12 +++-- frontend/src/lib/types/api.d.ts | 3 +- frontend/src/lib/wagmi.ts | 2 +- 7 files changed, 70 insertions(+), 43 deletions(-) diff --git a/frontend/.astro/data-store.json b/frontend/.astro/data-store.json index 168d6e6..9e8a7dc 100644 --- a/frontend/.astro/data-store.json +++ b/frontend/.astro/data-store.json @@ -1 +1 @@ -[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.4","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/dev\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/home/tushar/open/TheGuildGenesis/frontend/node_modules/.astro/sessions\"}}}"] \ No newline at end of file +[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.1","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/node\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/Users/antoineestienne/GithubRepositories/TheGuildGenesis/frontend/node_modules/.astro/sessions\"}}}"] \ No newline at end of file diff --git a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx index 253731d..b07ddf7 100644 --- a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx @@ -31,6 +31,7 @@ import { useAccount, useSignMessage } from "wagmi"; const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), description: z.string().optional(), + githubLogin: z.string().optional(), }); type FormValues = z.infer; @@ -54,14 +55,15 @@ export function CreateProfileButton() { if (!siweMessage) { throw new Error("SIWE message not available"); } - + // Sign the SIWE message const signature = await signMessageAsync({ message: siweMessage }); - + await createProfile.mutateAsync({ input: { name: values.name, description: values.description || "", + github_login: values.githubLogin || "", }, signature, }); @@ -116,6 +118,22 @@ export function CreateProfileButton() { )} /> + ( + + GitHub Handle + +
+ @ + +
+
+ +
+ )} + /> {siweMessage && (
Message to Sign @@ -123,7 +141,8 @@ export function CreateProfileButton() { {siweMessage}

- This message will be signed with your wallet to authenticate your profile creation. + This message will be signed with your wallet to authenticate + your profile creation.

)} @@ -133,16 +152,17 @@ export function CreateProfileButton() { Cancel - {createProfile.isError ? ( diff --git a/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx index 772eb0c..835cec8 100644 --- a/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx @@ -36,9 +36,7 @@ interface EditProfileDialogProps { } const formSchema = z.object({ - name: z - .string() - .min(2, { message: "Name must be at least 2 characters." }), + name: z.string().min(2, { message: "Name must be at least 2 characters." }), description: z.string().optional(), githubLogin: z.string().optional(), }); @@ -57,7 +55,8 @@ export function EditProfileDialog({ const queryClient = useQueryClient(); const { address: signerAddress } = useAccount(); const { signMessageAsync } = useSignMessage(); - const { data: nonceData, isLoading: isLoadingNonce } = useGetNonce(signerAddress); + const { data: nonceData, isLoading: isLoadingNonce } = + useGetNonce(signerAddress); const siweMessage = nonceData ? generateSiweMessage(nonceData) : ""; @@ -70,7 +69,6 @@ export function EditProfileDialog({ }, }); - useEffect(() => { if (open) { form.reset({ @@ -155,10 +153,7 @@ export function EditProfileDialog({
@ - +
@@ -172,7 +167,8 @@ export function EditProfileDialog({ {siweMessage}

- This message will be signed with your wallet to authenticate your profile update. + This message will be signed with your wallet to authenticate + your profile update.

)} @@ -182,16 +178,17 @@ export function EditProfileDialog({ Cancel - {updateProfile.isError ? ( @@ -206,4 +203,4 @@ export function EditProfileDialog({ ); } -export default EditProfileDialog; \ No newline at end of file +export default EditProfileDialog; diff --git a/frontend/src/components/profiles/list/ProfilesList.tsx b/frontend/src/components/profiles/list/ProfilesList.tsx index 30a96c0..10efd23 100644 --- a/frontend/src/components/profiles/list/ProfilesList.tsx +++ b/frontend/src/components/profiles/list/ProfilesList.tsx @@ -75,10 +75,15 @@ export function ProfilesList() { if (data && data.length === 0) { return ( -

- ⚠️ - {"No Profile Found"} -

+ <> +
+ +
+

+ ⚠️ + {"No Profile Found"} +

+ ); } diff --git a/frontend/src/hooks/profiles/use-create-profile.ts b/frontend/src/hooks/profiles/use-create-profile.ts index 89d4f01..f3bd1ae 100644 --- a/frontend/src/hooks/profiles/use-create-profile.ts +++ b/frontend/src/hooks/profiles/use-create-profile.ts @@ -1,6 +1,13 @@ -import { useMutation, type UseMutationResult, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + type UseMutationResult, + useQueryClient, +} from "@tanstack/react-query"; import { useAccount, useSignMessage } from "wagmi"; -import type { CreateProfileInput, CreateProfileResponse } from "@/lib/types/api"; +import type { + CreateProfileInput, + CreateProfileResponse, +} from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; async function postCreateProfile( @@ -46,7 +53,6 @@ export function useCreateProfile(): UseMutationResult< MutationVariables > { const { address } = useAccount(); - const { signMessageAsync } = useSignMessage(); const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/lib/types/api.d.ts b/frontend/src/lib/types/api.d.ts index b7236f5..b8c914c 100644 --- a/frontend/src/lib/types/api.d.ts +++ b/frontend/src/lib/types/api.d.ts @@ -1,4 +1,3 @@ - export type CreateProfileInput = { name: string; description?: string; @@ -20,4 +19,4 @@ export type CreateProfileResponse = unknown; export type DeleteProfileInput = {}; -export type DeleteProfileResponse = unknown; \ No newline at end of file +export type DeleteProfileResponse = unknown; diff --git a/frontend/src/lib/wagmi.ts b/frontend/src/lib/wagmi.ts index 6fce4e3..1211dd3 100644 --- a/frontend/src/lib/wagmi.ts +++ b/frontend/src/lib/wagmi.ts @@ -5,7 +5,7 @@ import { polygonAmoy } from "wagmi/chains"; const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as | string | undefined; -console.log(projectId); + export const config = getDefaultConfig({ appName: "The Guild Genesis", projectId: projectId ?? "",