Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions backend/src/application/commands/update_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
62 changes: 62 additions & 0 deletions backend/tests/profile_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
2 changes: 1 addition & 1 deletion frontend/.astro/data-store.json
Original file line number Diff line number Diff line change
@@ -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\"}}}"]
[["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\"}}}"]
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof formSchema>;
Expand All @@ -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,
});
Expand Down Expand Up @@ -116,14 +118,31 @@ export function CreateProfileButton() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="githubLogin"
render={({ field }) => (
<FormItem>
<FormLabel>GitHub Handle</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">@</span>
<Input placeholder="username" {...field} />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{siweMessage && (
<div className="space-y-2">
<FormLabel>Message to Sign</FormLabel>
<div className="p-3 bg-gray-50 rounded-md text-sm font-mono break-all">
{siweMessage}
</div>
<p className="text-xs text-gray-600">
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.
</p>
</div>
)}
Expand All @@ -133,16 +152,17 @@ export function CreateProfileButton() {
Cancel
</Button>
</DialogClose>
<Button
type="submit"
disabled={createProfile.isPending || isLoadingNonce || !siweMessage}
>
{isLoadingNonce
? "Loading..."
: createProfile.isPending
? "Creating..."
: "Create"
<Button
type="submit"
disabled={
createProfile.isPending || isLoadingNonce || !siweMessage
}
>
{isLoadingNonce
? "Loading..."
: createProfile.isPending
? "Creating..."
: "Create"}
</Button>
</div>
{createProfile.isError ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand All @@ -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) : "";

Expand All @@ -70,7 +69,6 @@ export function EditProfileDialog({
},
});


useEffect(() => {
if (open) {
form.reset({
Expand Down Expand Up @@ -155,10 +153,7 @@ export function EditProfileDialog({
<FormControl>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">@</span>
<Input
placeholder="username"
{...field}
/>
<Input placeholder="username" {...field} />
</div>
</FormControl>
<FormMessage />
Expand All @@ -172,7 +167,8 @@ export function EditProfileDialog({
{siweMessage}
</div>
<p className="text-xs text-gray-600">
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.
</p>
</div>
)}
Expand All @@ -182,16 +178,17 @@ export function EditProfileDialog({
Cancel
</Button>
</DialogClose>
<Button
type="submit"
disabled={updateProfile.isPending || isLoadingNonce || !siweMessage}
>
{isLoadingNonce
? "Loading..."
: updateProfile.isPending
? "Updating..."
: "Update"
<Button
type="submit"
disabled={
updateProfile.isPending || isLoadingNonce || !siweMessage
}
>
{isLoadingNonce
? "Loading..."
: updateProfile.isPending
? "Updating..."
: "Update"}
</Button>
</div>
{updateProfile.isError ? (
Expand All @@ -206,4 +203,4 @@ export function EditProfileDialog({
);
}

export default EditProfileDialog;
export default EditProfileDialog;
13 changes: 9 additions & 4 deletions frontend/src/components/profiles/list/ProfilesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ export function ProfilesList() {

if (data && data.length === 0) {
return (
<p className="text-2xl text-yellow-600 flex items-center gap-2">
<span>⚠️</span>
{"No Profile Found"}
</p>
<>
<div className="flex gap-4 items-center pb-8">
<CreateProfileButton />
</div>
<p className="text-2xl text-yellow-600 flex items-center gap-2">
<span>⚠️</span>
{"No Profile Found"}
</p>
</>
);
}

Expand Down
12 changes: 9 additions & 3 deletions frontend/src/hooks/profiles/use-create-profile.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -46,7 +53,6 @@ export function useCreateProfile(): UseMutationResult<
MutationVariables
> {
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
const queryClient = useQueryClient();

return useMutation<CreateProfileResponse, Error, MutationVariables>({
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/lib/types/api.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export type CreateProfileInput = {
name: string;
description?: string;
Expand All @@ -20,4 +19,4 @@ export type CreateProfileResponse = unknown;

export type DeleteProfileInput = {};

export type DeleteProfileResponse = unknown;
export type DeleteProfileResponse = unknown;
2 changes: 1 addition & 1 deletion frontend/src/lib/wagmi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "",
Expand Down
Loading