Skip to content

Commit 7ac2049

Browse files
authored
Merge branch 'main' into fix-#83
2 parents 845e1d0 + 9799104 commit 7ac2049

9 files changed

Lines changed: 152 additions & 59 deletions

File tree

backend/src/application/commands/update_profile.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,29 @@ pub async fn update_profile(
1919

2020
profile.update_info(request.name, request.description, request.avatar_url);
2121
if let Some(ref handle) = request.github_login {
22-
// 1. Trim and validate
2322
let trimmed = handle.trim();
24-
let valid_format = regex::Regex::new(r"^[a-zA-Z0-9-]{1,39}$").unwrap();
25-
if trimmed.is_empty() || !valid_format.is_match(trimmed) {
26-
return Err("Invalid GitHub handle format".to_string());
27-
}
2823

29-
// 2. Check for conflicts
30-
if profile_repository
31-
.find_by_github_login(trimmed)
32-
.await
33-
.map_err(|e| e.to_string())?
34-
.is_some()
35-
{
36-
return Err("GitHub handle already taken".to_string());
24+
// Allow empty handles (set to None)
25+
if trimmed.is_empty() {
26+
profile.github_login = None;
27+
} else {
28+
// Validate format for non-empty handles
29+
let valid_format = regex::Regex::new(r"^[a-zA-Z0-9-]{1,39}$").unwrap();
30+
if !valid_format.is_match(trimmed) {
31+
return Err("Invalid GitHub handle format".to_string());
32+
}
33+
if let Some(conflicting_profile) = profile_repository
34+
.find_by_github_login(trimmed)
35+
.await
36+
.map_err(|e| e.to_string())?
37+
{
38+
// Only conflict if it's not the current user's profile
39+
if conflicting_profile.address != wallet_address {
40+
return Err("GitHub handle already taken".to_string());
41+
}
42+
}
43+
profile.github_login = Some(trimmed.to_string());
3744
}
38-
39-
// 3. Assign it (preserving user’s original casing)
40-
profile.github_login = Some(trimmed.to_string());
4145
}
4246
profile_repository
4347
.update(&profile)

backend/tests/profile_tests.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,66 @@ mod github_handle_tests {
182182
let err_msg = err.unwrap_err();
183183
assert!(err_msg.contains("GitHub handle already taken"));
184184
}
185+
186+
#[tokio::test]
187+
async fn empty_github_handle_allowed() {
188+
let profile = Profile {
189+
address: WalletAddress::new("0x1234567890123456789012345678901234567894".to_string())
190+
.unwrap(),
191+
name: Some("Bob".into()),
192+
description: None,
193+
avatar_url: None,
194+
github_login: Some("BobUser".into()),
195+
login_nonce: 1,
196+
created_at: chrono::Utc::now(),
197+
updated_at: chrono::Utc::now(),
198+
};
199+
let repo = Arc::new(FakeRepo {
200+
profiles: std::sync::Mutex::new(vec![profile.clone()]),
201+
});
202+
203+
// Try updating to empty github handle
204+
let req = UpdateProfileRequest {
205+
name: None,
206+
description: None,
207+
avatar_url: None,
208+
github_login: Some("".into()),
209+
};
210+
211+
let result = update_profile(repo.clone(), profile.address.to_string(), req).await;
212+
assert!(result.is_ok());
213+
let resp = result.unwrap();
214+
assert!(resp.github_login.is_none()); // Should be None, not Some("")
215+
}
216+
217+
#[tokio::test]
218+
async fn user_can_update_own_github_handle() {
219+
let profile = Profile {
220+
address: WalletAddress::new("0x1234567890123456789012345678901234567895".to_string())
221+
.unwrap(),
222+
name: Some("Charlie".into()),
223+
description: None,
224+
avatar_url: None,
225+
github_login: Some("CharlieGit".into()),
226+
login_nonce: 1,
227+
created_at: chrono::Utc::now(),
228+
updated_at: chrono::Utc::now(),
229+
};
230+
let repo = Arc::new(FakeRepo {
231+
profiles: std::sync::Mutex::new(vec![profile.clone()]),
232+
});
233+
234+
// Try updating with the same handle (should succeed)
235+
let req = UpdateProfileRequest {
236+
name: None,
237+
description: None,
238+
avatar_url: None,
239+
github_login: Some("CharlieGit".into()),
240+
};
241+
242+
let result = update_profile(repo.clone(), profile.address.to_string(), req).await;
243+
assert!(result.is_ok());
244+
let resp = result.unwrap();
245+
assert_eq!(resp.github_login.unwrap(), "CharlieGit");
246+
}
185247
}

frontend/.astro/data-store.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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\"}}}"]
1+
[["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\"}}}"]

frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { useAccount, useSignMessage } from "wagmi";
3131
const formSchema = z.object({
3232
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
3333
description: z.string().optional(),
34+
githubLogin: z.string().optional(),
3435
});
3536

3637
type FormValues = z.infer<typeof formSchema>;
@@ -54,14 +55,15 @@ export function CreateProfileButton() {
5455
if (!siweMessage) {
5556
throw new Error("SIWE message not available");
5657
}
57-
58+
5859
// Sign the SIWE message
5960
const signature = await signMessageAsync({ message: siweMessage });
60-
61+
6162
await createProfile.mutateAsync({
6263
input: {
6364
name: values.name,
6465
description: values.description || "",
66+
github_login: values.githubLogin || "",
6567
},
6668
signature,
6769
});
@@ -116,14 +118,31 @@ export function CreateProfileButton() {
116118
</FormItem>
117119
)}
118120
/>
121+
<FormField
122+
control={form.control}
123+
name="githubLogin"
124+
render={({ field }) => (
125+
<FormItem>
126+
<FormLabel>GitHub Handle</FormLabel>
127+
<FormControl>
128+
<div className="flex items-center gap-2">
129+
<span className="text-sm text-gray-500">@</span>
130+
<Input placeholder="username" {...field} />
131+
</div>
132+
</FormControl>
133+
<FormMessage />
134+
</FormItem>
135+
)}
136+
/>
119137
{siweMessage && (
120138
<div className="space-y-2">
121139
<FormLabel>Message to Sign</FormLabel>
122140
<div className="p-3 bg-gray-50 rounded-md text-sm font-mono break-all">
123141
{siweMessage}
124142
</div>
125143
<p className="text-xs text-gray-600">
126-
This message will be signed with your wallet to authenticate your profile creation.
144+
This message will be signed with your wallet to authenticate
145+
your profile creation.
127146
</p>
128147
</div>
129148
)}
@@ -133,16 +152,17 @@ export function CreateProfileButton() {
133152
Cancel
134153
</Button>
135154
</DialogClose>
136-
<Button
137-
type="submit"
138-
disabled={createProfile.isPending || isLoadingNonce || !siweMessage}
139-
>
140-
{isLoadingNonce
141-
? "Loading..."
142-
: createProfile.isPending
143-
? "Creating..."
144-
: "Create"
155+
<Button
156+
type="submit"
157+
disabled={
158+
createProfile.isPending || isLoadingNonce || !siweMessage
145159
}
160+
>
161+
{isLoadingNonce
162+
? "Loading..."
163+
: createProfile.isPending
164+
? "Creating..."
165+
: "Create"}
146166
</Button>
147167
</div>
148168
{createProfile.isError ? (

frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ interface EditProfileDialogProps {
3636
}
3737

3838
const formSchema = z.object({
39-
name: z
40-
.string()
41-
.min(2, { message: "Name must be at least 2 characters." }),
39+
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
4240
description: z.string().optional(),
4341
githubLogin: z.string().optional(),
4442
});
@@ -57,7 +55,8 @@ export function EditProfileDialog({
5755
const queryClient = useQueryClient();
5856
const { address: signerAddress } = useAccount();
5957
const { signMessageAsync } = useSignMessage();
60-
const { data: nonceData, isLoading: isLoadingNonce } = useGetNonce(signerAddress);
58+
const { data: nonceData, isLoading: isLoadingNonce } =
59+
useGetNonce(signerAddress);
6160

6261
const siweMessage = nonceData ? generateSiweMessage(nonceData) : "";
6362

@@ -70,7 +69,6 @@ export function EditProfileDialog({
7069
},
7170
});
7271

73-
7472
useEffect(() => {
7573
if (open) {
7674
form.reset({
@@ -155,10 +153,7 @@ export function EditProfileDialog({
155153
<FormControl>
156154
<div className="flex items-center gap-2">
157155
<span className="text-sm text-gray-500">@</span>
158-
<Input
159-
placeholder="username"
160-
{...field}
161-
/>
156+
<Input placeholder="username" {...field} />
162157
</div>
163158
</FormControl>
164159
<FormMessage />
@@ -172,7 +167,8 @@ export function EditProfileDialog({
172167
{siweMessage}
173168
</div>
174169
<p className="text-xs text-gray-600">
175-
This message will be signed with your wallet to authenticate your profile update.
170+
This message will be signed with your wallet to authenticate
171+
your profile update.
176172
</p>
177173
</div>
178174
)}
@@ -182,16 +178,17 @@ export function EditProfileDialog({
182178
Cancel
183179
</Button>
184180
</DialogClose>
185-
<Button
186-
type="submit"
187-
disabled={updateProfile.isPending || isLoadingNonce || !siweMessage}
188-
>
189-
{isLoadingNonce
190-
? "Loading..."
191-
: updateProfile.isPending
192-
? "Updating..."
193-
: "Update"
181+
<Button
182+
type="submit"
183+
disabled={
184+
updateProfile.isPending || isLoadingNonce || !siweMessage
194185
}
186+
>
187+
{isLoadingNonce
188+
? "Loading..."
189+
: updateProfile.isPending
190+
? "Updating..."
191+
: "Update"}
195192
</Button>
196193
</div>
197194
{updateProfile.isError ? (
@@ -206,4 +203,4 @@ export function EditProfileDialog({
206203
);
207204
}
208205

209-
export default EditProfileDialog;
206+
export default EditProfileDialog;

frontend/src/components/profiles/list/ProfilesList.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,15 @@ export function ProfilesList() {
7575

7676
if (data && data.length === 0) {
7777
return (
78-
<p className="text-2xl text-yellow-600 flex items-center gap-2">
79-
<span>⚠️</span>
80-
{"No Profile Found"}
81-
</p>
78+
<>
79+
<div className="flex gap-4 items-center pb-8">
80+
<CreateProfileButton />
81+
</div>
82+
<p className="text-2xl text-yellow-600 flex items-center gap-2">
83+
<span>⚠️</span>
84+
{"No Profile Found"}
85+
</p>
86+
</>
8287
);
8388
}
8489

frontend/src/hooks/profiles/use-create-profile.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { useMutation, type UseMutationResult, useQueryClient } from "@tanstack/react-query";
1+
import {
2+
useMutation,
3+
type UseMutationResult,
4+
useQueryClient,
5+
} from "@tanstack/react-query";
26
import { useAccount, useSignMessage } from "wagmi";
3-
import type { CreateProfileInput, CreateProfileResponse } from "@/lib/types/api";
7+
import type {
8+
CreateProfileInput,
9+
CreateProfileResponse,
10+
} from "@/lib/types/api";
411
import { API_BASE_URL } from "@/lib/constants/apiConstants";
512

613
async function postCreateProfile(
@@ -46,7 +53,6 @@ export function useCreateProfile(): UseMutationResult<
4653
MutationVariables
4754
> {
4855
const { address } = useAccount();
49-
const { signMessageAsync } = useSignMessage();
5056
const queryClient = useQueryClient();
5157

5258
return useMutation<CreateProfileResponse, Error, MutationVariables>({

frontend/src/lib/types/api.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
export type CreateProfileInput = {
32
name: string;
43
description?: string;
@@ -20,4 +19,4 @@ export type CreateProfileResponse = unknown;
2019

2120
export type DeleteProfileInput = {};
2221

23-
export type DeleteProfileResponse = unknown;
22+
export type DeleteProfileResponse = unknown;

frontend/src/lib/wagmi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { polygonAmoy } from "wagmi/chains";
55
const projectId = import.meta.env.PUBLIC_WALLET_CONNECT_PROJECT_ID as
66
| string
77
| undefined;
8-
console.log(projectId);
8+
99
export const config = getDefaultConfig({
1010
appName: "The Guild Genesis",
1111
projectId: projectId ?? "",

0 commit comments

Comments
 (0)