From daffce4e06eaace5b303aebc516c30c10c188c3b Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Thu, 18 Dec 2025 18:23:56 +0000 Subject: [PATCH 1/3] feat(#150): Implement JWT authentication flow on frontend --- frontend/src/components/AppWrapper.tsx | 2 + frontend/src/components/LoginButton.tsx | 57 +++++++++++++ .../src/hooks/profiles/use-create-profile.ts | 29 ++++--- .../src/hooks/profiles/use-delete-profile.ts | 28 ++++--- .../src/hooks/profiles/use-update-profile.ts | 28 ++++--- frontend/src/hooks/use-auth-header.ts | 79 +++++++++++++++++++ frontend/src/hooks/use-login.ts | 69 ++++++++++++++++ frontend/src/lib/utils/fetch-with-jwt.ts | 48 +++++++++++ frontend/src/lib/utils/jwt.ts | 29 +++++++ 9 files changed, 342 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/LoginButton.tsx create mode 100644 frontend/src/hooks/use-auth-header.ts create mode 100644 frontend/src/hooks/use-login.ts create mode 100644 frontend/src/lib/utils/fetch-with-jwt.ts create mode 100644 frontend/src/lib/utils/jwt.ts diff --git a/frontend/src/components/AppWrapper.tsx b/frontend/src/components/AppWrapper.tsx index 9aa3b3a..ad2a6f4 100644 --- a/frontend/src/components/AppWrapper.tsx +++ b/frontend/src/components/AppWrapper.tsx @@ -11,6 +11,7 @@ import { import { AppSidebar } from "@/components/AppSidebar"; import { ActivityTokenBalance } from "@/components/ActivityTokenBalance"; import { Background } from "@/components/Background"; +import { LoginButton } from "@/components/LoginButton"; const queryClient = new QueryClient(); @@ -39,6 +40,7 @@ export function AppWrapper({ children }: AppWrapperProps) {
+
diff --git a/frontend/src/components/LoginButton.tsx b/frontend/src/components/LoginButton.tsx new file mode 100644 index 0000000..7fe06b7 --- /dev/null +++ b/frontend/src/components/LoginButton.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { useAccount } from "wagmi"; +import { useLogin } from "@/hooks/use-login"; +import { isTokenValid, clearToken } from "@/lib/utils/jwt"; +import { Button } from "@/components/ui/button"; +import { LogIn, LogOut } from "lucide-react"; + +export function LoginButton() { + const { address, isConnected } = useAccount(); + const { login, isLoading, error } = useLogin(); + const [isLoggedIn, setIsLoggedIn] = useState(isTokenValid()); + + const handleLogin = async () => { + try { + await login(); + setIsLoggedIn(true); + } catch (err) { + console.error("Login failed:", err); + } + }; + + const handleLogout = () => { + clearToken(); + setIsLoggedIn(false); + }; + + // Show nothing if wallet not connected + if (!isConnected || !address) { + return null; + } + + if (isLoggedIn) { + return ( + + ); + } + + return ( + + ); +} diff --git a/frontend/src/hooks/profiles/use-create-profile.ts b/frontend/src/hooks/profiles/use-create-profile.ts index f3bd1ae..b4aee3b 100644 --- a/frontend/src/hooks/profiles/use-create-profile.ts +++ b/frontend/src/hooks/profiles/use-create-profile.ts @@ -9,27 +9,37 @@ import type { CreateProfileResponse, } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; +import { getToken } from "@/lib/utils/jwt"; +import { useAuthHeader } from "@/hooks/use-auth-header"; async function postCreateProfile( input: CreateProfileInput, address: string, - signature: string + signature: string, + token?: string ): Promise { + const headers: HeadersInit = { + "Content-Type": "application/json", + }; + + // Use JWT token if available, otherwise use SIWE signature + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } else { + headers["x-eth-address"] = address; + headers["x-eth-signature"] = signature; + } + const response = await fetch(`${API_BASE_URL}/profiles/`, { method: "POST", - headers: { - "Content-Type": "application/json", - "x-eth-address": address, - "x-eth-signature": signature, - }, + headers, body: JSON.stringify(input), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error( - `Failed to create profile: ${response.status} ${response.statusText}${ - text ? ` - ${text}` : "" + `Failed to create profile: ${response.status} ${response.statusText}${text ? ` - ${text}` : "" }` ); } @@ -61,7 +71,8 @@ export function useCreateProfile(): UseMutationResult< if (!address) { throw new Error("Wallet not connected"); } - return postCreateProfile(input, address, signature); + const token = getToken(); + return postCreateProfile(input, address, signature, token || undefined); }, onSuccess: () => { // Invalidate nonce query since it was incremented diff --git a/frontend/src/hooks/profiles/use-delete-profile.ts b/frontend/src/hooks/profiles/use-delete-profile.ts index 6e5c961..351200f 100644 --- a/frontend/src/hooks/profiles/use-delete-profile.ts +++ b/frontend/src/hooks/profiles/use-delete-profile.ts @@ -5,26 +5,35 @@ import type { DeleteProfileResponse, } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; +import { getToken } from "@/lib/utils/jwt"; async function deleteProfile( address: string, signerAddress: string, - signature: string + signature: string, + token?: string ): Promise { + const headers: HeadersInit = { + "Content-Type": "application/json", + }; + + // Use JWT token if available, otherwise use SIWE signature + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } else { + headers["x-eth-address"] = signerAddress; + headers["x-eth-signature"] = signature; + } + const response = await fetch(`${API_BASE_URL}/profiles/${address}`, { method: "DELETE", - headers: { - "Content-Type": "application/json", - "x-eth-address": signerAddress, - "x-eth-signature": signature, - }, + headers, }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error( - `Failed to delete profile: ${response.status} ${response.statusText}${ - text ? ` - ${text}` : "" + `Failed to delete profile: ${response.status} ${response.statusText}${text ? ` - ${text}` : "" }` ); } @@ -52,7 +61,8 @@ export function useDeleteProfile(): UseMutationResult< mutationKey: ["delete-profile"], mutationFn: async ({ signature }) => { if (!address) throw new Error("Wallet not connected"); - return deleteProfile(address, address, signature); + const token = getToken(); + return deleteProfile(address, address, signature, token || undefined); }, onSuccess: () => { // Invalidate nonce query since it was incremented diff --git a/frontend/src/hooks/profiles/use-update-profile.ts b/frontend/src/hooks/profiles/use-update-profile.ts index 7be91b1..4ec01c8 100644 --- a/frontend/src/hooks/profiles/use-update-profile.ts +++ b/frontend/src/hooks/profiles/use-update-profile.ts @@ -5,28 +5,37 @@ import type { UpdateProfileResponse, } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; +import { getToken } from "@/lib/utils/jwt"; async function putUpdateProfile( address: string, body: UpdateProfileInput, signerAddress: string, - signature: string + signature: string, + token?: string ): Promise { + const headers: HeadersInit = { + "Content-Type": "application/json", + }; + + // Use JWT token if available, otherwise use SIWE signature + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } else { + headers["x-eth-address"] = signerAddress; + headers["x-eth-signature"] = signature; + } + const response = await fetch(`${API_BASE_URL}/profiles/${address}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - "x-eth-address": signerAddress, - "x-eth-signature": signature, - }, + headers, body: JSON.stringify(body), }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error( - `Failed to update profile: ${response.status} ${response.statusText}${ - text ? ` - ${text}` : "" + `Failed to update profile: ${response.status} ${response.statusText}${text ? ` - ${text}` : "" }` ); } @@ -55,7 +64,8 @@ export function useUpdateProfile(): UseMutationResult< mutationKey: ["update-profile"], mutationFn: async ({ input, signature }) => { if (!address) throw new Error("Wallet not connected"); - return putUpdateProfile(address, input, address, signature); + const token = getToken(); + return putUpdateProfile(address, input, address, signature, token || undefined); }, onSuccess: () => { // Invalidate nonce query since it was incremented diff --git a/frontend/src/hooks/use-auth-header.ts b/frontend/src/hooks/use-auth-header.ts new file mode 100644 index 0000000..74c4927 --- /dev/null +++ b/frontend/src/hooks/use-auth-header.ts @@ -0,0 +1,79 @@ +import { useCallback, useState } from "react"; +import { useAccount, useSignMessage } from "wagmi"; +import { getToken, saveToken, clearToken } from "@/lib/utils/jwt"; +import { useGetNonce } from "./profiles/use-get-nonce"; +import { generateSiweMessage } from "@/lib/utils/siwe"; +import { API_BASE_URL } from "@/lib/constants/apiConstants"; + +interface AuthHeaders { + [key: string]: string; +} + +export function useAuthHeader() { + const { address } = useAccount(); + const { signMessageAsync } = useSignMessage(); + const { data: nonceData } = useGetNonce(address); + const [isRefreshing, setIsRefreshing] = useState(false); + + const getAuthHeaders = useCallback((): AuthHeaders => { + const token = getToken(); + + // If JWT token exists, use it + if (token) { + return { + Authorization: `Bearer ${token}`, + }; + } + + // Fall back to empty headers (caller will add SIWE headers) + return {}; + }, []); + + const refreshToken = useCallback(async (): Promise => { + try { + setIsRefreshing(true); + + if (!address || !nonceData) { + return false; + } + + // Sign a new message to get a new token + const siweMessage = generateSiweMessage(nonceData); + const signature = await signMessageAsync({ message: siweMessage }); + + // Get new JWT token + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-eth-address": address, + "x-eth-signature": signature, + }, + }); + + if (!response.ok) { + clearToken(); + return false; + } + + const data = (await response.json()) as { + token: string; + address: string; + }; + saveToken(data); + return true; + } catch { + clearToken(); + return false; + } finally { + setIsRefreshing(false); + } + }, [address, nonceData, signMessageAsync]); + + return { + getAuthHeaders, + refreshToken, + isRefreshing, + hasToken: () => getToken() !== null, + }; +} diff --git a/frontend/src/hooks/use-login.ts b/frontend/src/hooks/use-login.ts new file mode 100644 index 0000000..020fd2a --- /dev/null +++ b/frontend/src/hooks/use-login.ts @@ -0,0 +1,69 @@ +import { useAccount, useSignMessage } from "wagmi"; +import { useGetNonce } from "./profiles/use-get-nonce"; +import { generateSiweMessage } from "@/lib/utils/siwe"; +import { saveToken } from "@/lib/utils/jwt"; +import { API_BASE_URL } from "@/lib/constants/apiConstants"; +import { useCallback, useState } from "react"; + +interface LoginResponse { + token: string; + address: string; +} + +export function useLogin() { + const { address } = useAccount(); + const { signMessageAsync } = useSignMessage(); + const { data: nonceData } = useGetNonce(address); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const login = useCallback(async () => { + try { + setIsLoading(true); + setError(null); + + if (!address) { + throw new Error("Wallet not connected"); + } + + if (!nonceData) { + throw new Error("Nonce not available"); + } + + // Generate SIWE message and sign it + const siweMessage = generateSiweMessage(nonceData); + const signature = await signMessageAsync({ message: siweMessage }); + + // Send signature to backend to get JWT token + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-eth-address": address, + "x-eth-signature": signature, + }, + }); + + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error( + `Login failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : "" + }` + ); + } + + const data = (await response.json()) as LoginResponse; + saveToken(data); + + return data; + } catch (err) { + const error = err instanceof Error ? err : new Error("Unknown error"); + setError(error); + throw error; + } finally { + setIsLoading(false); + } + }, [address, nonceData, signMessageAsync]); + + return { login, isLoading, error }; +} diff --git a/frontend/src/lib/utils/fetch-with-jwt.ts b/frontend/src/lib/utils/fetch-with-jwt.ts new file mode 100644 index 0000000..559d748 --- /dev/null +++ b/frontend/src/lib/utils/fetch-with-jwt.ts @@ -0,0 +1,48 @@ +import { getToken, clearToken, saveToken } from "./jwt"; +import { API_BASE_URL } from "@/lib/constants/apiConstants"; + +interface FetchOptions extends RequestInit { + skipAuth?: boolean; + retryOn401?: boolean; +} + +/** + * Enhanced fetch with JWT support and 401 handling + * On 401, attempts to refresh token and retry the request + */ +export async function fetchWithJWT( + url: string, + options: FetchOptions = {}, + refreshTokenFn?: () => Promise +): Promise { + const { skipAuth = false, retryOn401 = true, ...fetchOptions } = options; + + // Add JWT token if available and not skipping auth + if (!skipAuth) { + const token = getToken(); + if (token) { + const headers = new Headers(fetchOptions.headers); + headers.set("Authorization", `Bearer ${token}`); + fetchOptions.headers = headers; + } + } + + let response = await fetch(url, fetchOptions); + + // Handle 401: try to refresh token and retry + if (response.status === 401 && retryOn401 && refreshTokenFn) { + const refreshed = await refreshTokenFn(); + if (refreshed) { + // Retry the request with new token + const token = getToken(); + if (token) { + const headers = new Headers(fetchOptions.headers); + headers.set("Authorization", `Bearer ${token}`); + fetchOptions.headers = headers; + } + response = await fetch(url, fetchOptions); + } + } + + return response; +} diff --git a/frontend/src/lib/utils/jwt.ts b/frontend/src/lib/utils/jwt.ts new file mode 100644 index 0000000..ee2263f --- /dev/null +++ b/frontend/src/lib/utils/jwt.ts @@ -0,0 +1,29 @@ +const JWT_TOKEN_KEY = "jwt_token"; +const JWT_ADDRESS_KEY = "jwt_address"; + +export interface JwtToken { + token: string; + address: string; +} + +export function saveToken(token: JwtToken): void { + localStorage.setItem(JWT_TOKEN_KEY, token.token); + localStorage.setItem(JWT_ADDRESS_KEY, token.address); +} + +export function getToken(): string | null { + return localStorage.getItem(JWT_TOKEN_KEY); +} + +export function getAddress(): string | null { + return localStorage.getItem(JWT_ADDRESS_KEY); +} + +export function clearToken(): void { + localStorage.removeItem(JWT_TOKEN_KEY); + localStorage.removeItem(JWT_ADDRESS_KEY); +} + +export function isTokenValid(): boolean { + return getToken() !== null; +} From 06155a281778cdf692c6fe9a080a8ee4348ec0e4 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Mon, 22 Dec 2025 16:28:23 +0000 Subject: [PATCH 2/3] fix: Add token expiration check and remove unused utility --- .../src/hooks/profiles/use-create-profile.ts | 1 - frontend/src/hooks/use-auth-header.ts | 19 ++++---- frontend/src/lib/utils/fetch-with-jwt.ts | 48 ------------------- frontend/src/lib/utils/jwt.ts | 29 +++++++++-- 4 files changed, 36 insertions(+), 61 deletions(-) delete mode 100644 frontend/src/lib/utils/fetch-with-jwt.ts diff --git a/frontend/src/hooks/profiles/use-create-profile.ts b/frontend/src/hooks/profiles/use-create-profile.ts index b4aee3b..b34b800 100644 --- a/frontend/src/hooks/profiles/use-create-profile.ts +++ b/frontend/src/hooks/profiles/use-create-profile.ts @@ -10,7 +10,6 @@ import type { } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; import { getToken } from "@/lib/utils/jwt"; -import { useAuthHeader } from "@/hooks/use-auth-header"; async function postCreateProfile( input: CreateProfileInput, diff --git a/frontend/src/hooks/use-auth-header.ts b/frontend/src/hooks/use-auth-header.ts index 74c4927..8a2694e 100644 --- a/frontend/src/hooks/use-auth-header.ts +++ b/frontend/src/hooks/use-auth-header.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import { useAccount, useSignMessage } from "wagmi"; -import { getToken, saveToken, clearToken } from "@/lib/utils/jwt"; +import { getToken, saveToken, clearToken, isTokenValid } from "@/lib/utils/jwt"; import { useGetNonce } from "./profiles/use-get-nonce"; import { generateSiweMessage } from "@/lib/utils/siwe"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; @@ -16,13 +16,14 @@ export function useAuthHeader() { const [isRefreshing, setIsRefreshing] = useState(false); const getAuthHeaders = useCallback((): AuthHeaders => { - const token = getToken(); - - // If JWT token exists, use it - if (token) { - return { - Authorization: `Bearer ${token}`, - }; + // Check if token is valid and not expired + if (isTokenValid()) { + const token = getToken(); + if (token) { + return { + Authorization: `Bearer ${token}`, + }; + } } // Fall back to empty headers (caller will add SIWE headers) @@ -74,6 +75,6 @@ export function useAuthHeader() { getAuthHeaders, refreshToken, isRefreshing, - hasToken: () => getToken() !== null, + hasToken: isTokenValid, }; } diff --git a/frontend/src/lib/utils/fetch-with-jwt.ts b/frontend/src/lib/utils/fetch-with-jwt.ts deleted file mode 100644 index 559d748..0000000 --- a/frontend/src/lib/utils/fetch-with-jwt.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { getToken, clearToken, saveToken } from "./jwt"; -import { API_BASE_URL } from "@/lib/constants/apiConstants"; - -interface FetchOptions extends RequestInit { - skipAuth?: boolean; - retryOn401?: boolean; -} - -/** - * Enhanced fetch with JWT support and 401 handling - * On 401, attempts to refresh token and retry the request - */ -export async function fetchWithJWT( - url: string, - options: FetchOptions = {}, - refreshTokenFn?: () => Promise -): Promise { - const { skipAuth = false, retryOn401 = true, ...fetchOptions } = options; - - // Add JWT token if available and not skipping auth - if (!skipAuth) { - const token = getToken(); - if (token) { - const headers = new Headers(fetchOptions.headers); - headers.set("Authorization", `Bearer ${token}`); - fetchOptions.headers = headers; - } - } - - let response = await fetch(url, fetchOptions); - - // Handle 401: try to refresh token and retry - if (response.status === 401 && retryOn401 && refreshTokenFn) { - const refreshed = await refreshTokenFn(); - if (refreshed) { - // Retry the request with new token - const token = getToken(); - if (token) { - const headers = new Headers(fetchOptions.headers); - headers.set("Authorization", `Bearer ${token}`); - fetchOptions.headers = headers; - } - response = await fetch(url, fetchOptions); - } - } - - return response; -} diff --git a/frontend/src/lib/utils/jwt.ts b/frontend/src/lib/utils/jwt.ts index ee2263f..76f66fd 100644 --- a/frontend/src/lib/utils/jwt.ts +++ b/frontend/src/lib/utils/jwt.ts @@ -1,17 +1,24 @@ const JWT_TOKEN_KEY = "jwt_token"; const JWT_ADDRESS_KEY = "jwt_address"; +const JWT_EXPIRY_KEY = "jwt_expiry"; export interface JwtToken { token: string; address: string; } -export function saveToken(token: JwtToken): void { +export function saveToken(token: JwtToken, expiresInSeconds = 86400): void { + const expiryTime = Date.now() + expiresInSeconds * 1000; localStorage.setItem(JWT_TOKEN_KEY, token.token); localStorage.setItem(JWT_ADDRESS_KEY, token.address); + localStorage.setItem(JWT_EXPIRY_KEY, expiryTime.toString()); } export function getToken(): string | null { + if (!isTokenValid()) { + clearToken(); + return null; + } return localStorage.getItem(JWT_TOKEN_KEY); } @@ -22,8 +29,24 @@ export function getAddress(): string | null { export function clearToken(): void { localStorage.removeItem(JWT_TOKEN_KEY); localStorage.removeItem(JWT_ADDRESS_KEY); + localStorage.removeItem(JWT_EXPIRY_KEY); } export function isTokenValid(): boolean { - return getToken() !== null; -} + const token = localStorage.getItem(JWT_TOKEN_KEY); + const expiryStr = localStorage.getItem(JWT_EXPIRY_KEY); + + if (!token || !expiryStr) { + return false; + } + + const expiryTime = parseInt(expiryStr, 10); + const isExpired = Date.now() >= expiryTime; + + if (isExpired) { + clearToken(); + return false; + } + + return true; +} \ No newline at end of file From be7026449e712ba4f987c3411e8fe11a9807b438 Mon Sep 17 00:00:00 2001 From: tusharshah21 Date: Tue, 23 Dec 2025 16:00:26 +0000 Subject: [PATCH 3/3] refactor: Replace SIWE with JWT-first approach in profile CRUD --- .../action-buttons/CreateProfileDialog.tsx | 41 +------------- .../action-buttons/DeleteProfileDialog.tsx | 35 +----------- .../action-buttons/EditProfileDialog.tsx | 39 +------------ .../src/hooks/profiles/use-create-profile.ts | 46 ++++++++------- .../src/hooks/profiles/use-delete-profile.ts | 56 +++++++++---------- .../src/hooks/profiles/use-update-profile.ts | 49 ++++++++-------- 6 files changed, 83 insertions(+), 183 deletions(-) diff --git a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx index 1a9d448..d69d4f1 100644 --- a/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/CreateProfileDialog.tsx @@ -25,9 +25,6 @@ import { import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useGetNonce } from "@/hooks/profiles/use-get-nonce"; -import { generateSiweMessage } from "@/lib/utils/siwe"; -import { useAccount, useSignMessage } from "wagmi"; const formSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -41,11 +38,6 @@ export function CreateProfileButton() { const [open, setOpen] = useState(false); const createProfile = useCreateProfile(); const queryClient = useQueryClient(); - const { address } = useAccount(); - const { signMessageAsync } = useSignMessage(); - const { data: nonceData, isLoading: isLoadingNonce } = useGetNonce(address); - - const siweMessage = nonceData ? generateSiweMessage(nonceData) : ""; const form = useForm({ resolver: zodResolver(formSchema), @@ -53,20 +45,12 @@ export function CreateProfileButton() { }); const onSubmit = async (values: FormValues) => { - 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, }); await queryClient.invalidateQueries({ queryKey: ["profiles"] }); setOpen(false); @@ -136,35 +120,14 @@ export function CreateProfileButton() { )} /> - {siweMessage && ( -
- Message to Sign -
- {siweMessage} -
-

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

-
- )}
-
{createProfile.isError ? ( diff --git a/frontend/src/components/profiles/action-buttons/DeleteProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/DeleteProfileDialog.tsx index 10854c0..9c8d632 100644 --- a/frontend/src/components/profiles/action-buttons/DeleteProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/DeleteProfileDialog.tsx @@ -11,9 +11,6 @@ import { Button } from "@/components/ui/button"; import { useDeleteProfile } from "@/hooks/profiles/use-delete-profile"; import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; -import { useGetNonce } from "@/hooks/profiles/use-get-nonce"; -import { generateSiweMessage } from "@/lib/utils/siwe"; -import { useAccount, useSignMessage } from "wagmi"; interface DeleteProfileDialogProps { children: React.ReactNode; @@ -23,19 +20,9 @@ export function DeleteProfileDialog({ children }: DeleteProfileDialogProps) { const [open, setOpen] = useState(false); const deleteProfile = useDeleteProfile(); const queryClient = useQueryClient(); - const { address } = useAccount(); - const { signMessageAsync } = useSignMessage(); - const { data: nonceData, isLoading: isLoadingNonce } = useGetNonce(address); - - const siweMessage = nonceData ? generateSiweMessage(nonceData) : ""; const onConfirm = async () => { - if (!siweMessage) { - throw new Error("SIWE message not available"); - } - - const signature = await signMessageAsync({ message: siweMessage }); - await deleteProfile.mutateAsync({ signature }); + await deleteProfile.mutateAsync(); await queryClient.invalidateQueries({ queryKey: ["profiles"] }); setOpen(false); }; @@ -51,17 +38,6 @@ export function DeleteProfileDialog({ children }: DeleteProfileDialogProps) { profile. - {siweMessage && ( -
- -
- {siweMessage} -
-

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

-
- )}
{deleteProfile.isError ? ( diff --git a/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx b/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx index 70e3093..2666969 100644 --- a/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx +++ b/frontend/src/components/profiles/action-buttons/EditProfileDialog.tsx @@ -24,9 +24,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useUpdateProfile } from "@/hooks/profiles/use-update-profile"; import { useQueryClient } from "@tanstack/react-query"; import { useState, useEffect } from "react"; -import { useGetNonce } from "@/hooks/profiles/use-get-nonce"; -import { generateSiweMessage } from "@/lib/utils/siwe"; -import { useAccount, useSignMessage } from "wagmi"; interface EditProfileDialogProps { address: string; @@ -54,12 +51,6 @@ export function EditProfileDialog({ const [open, setOpen] = useState(false); const updateProfile = useUpdateProfile(); const queryClient = useQueryClient(); - const { address: signerAddress } = useAccount(); - const { signMessageAsync } = useSignMessage(); - const { data: nonceData, isLoading: isLoadingNonce } = - useGetNonce(signerAddress); - - const siweMessage = nonceData ? generateSiweMessage(nonceData) : ""; const form = useForm({ resolver: zodResolver(formSchema), @@ -81,21 +72,13 @@ export function EditProfileDialog({ }, [open, name, description, githubLogin, form]); const onSubmit = async (values: FormValues) => { - if (!siweMessage) { - throw new Error("SIWE message not available"); - } - try { - // Sign the SIWE message - const signature = await signMessageAsync({ message: siweMessage }); - await updateProfile.mutateAsync({ input: { name: values.name, description: values.description || "", github_login: values.githubLogin || "", }, - signature, }); await queryClient.invalidateQueries({ queryKey: ["profiles"] }); setOpen(false); @@ -162,18 +145,6 @@ export function EditProfileDialog({ )} /> - {siweMessage && ( -
- Message to Sign -
- {siweMessage} -
-

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

-
- )}
{updateProfile.isError ? ( diff --git a/frontend/src/hooks/profiles/use-create-profile.ts b/frontend/src/hooks/profiles/use-create-profile.ts index b34b800..d4c5672 100644 --- a/frontend/src/hooks/profiles/use-create-profile.ts +++ b/frontend/src/hooks/profiles/use-create-profile.ts @@ -3,35 +3,25 @@ import { type UseMutationResult, useQueryClient, } from "@tanstack/react-query"; -import { useAccount, useSignMessage } from "wagmi"; +import { useAccount } from "wagmi"; import type { CreateProfileInput, CreateProfileResponse, } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; -import { getToken } from "@/lib/utils/jwt"; +import { isTokenValid, getToken } from "@/lib/utils/jwt"; +import { useLogin } from "@/hooks/use-login"; async function postCreateProfile( input: CreateProfileInput, - address: string, - signature: string, - token?: string + token: string ): Promise { - const headers: HeadersInit = { - "Content-Type": "application/json", - }; - - // Use JWT token if available, otherwise use SIWE signature - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } else { - headers["x-eth-address"] = address; - headers["x-eth-signature"] = signature; - } - const response = await fetch(`${API_BASE_URL}/profiles/`, { method: "POST", - headers, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, body: JSON.stringify(input), }); @@ -43,7 +33,6 @@ async function postCreateProfile( ); } - // Attempt to parse JSON, but allow empty body try { return (await response.json()) as CreateProfileResponse; } catch { @@ -53,7 +42,6 @@ async function postCreateProfile( type MutationVariables = { input: CreateProfileInput; - signature: string; }; export function useCreateProfile(): UseMutationResult< @@ -63,19 +51,29 @@ export function useCreateProfile(): UseMutationResult< > { const { address } = useAccount(); const queryClient = useQueryClient(); + const { login } = useLogin(); return useMutation({ mutationKey: ["create-profile"], - mutationFn: async ({ input, signature }) => { + mutationFn: async ({ input }) => { if (!address) { throw new Error("Wallet not connected"); } + + // Check if token is valid, if not trigger login + if (!isTokenValid()) { + await login(); + } + const token = getToken(); - return postCreateProfile(input, address, signature, token || undefined); + if (!token) { + throw new Error("Authentication required. Please sign in."); + } + + return postCreateProfile(input, token); }, onSuccess: () => { - // Invalidate nonce query since it was incremented - queryClient.invalidateQueries({ queryKey: ["nonce", address] }); + queryClient.invalidateQueries({ queryKey: ["profiles"] }); }, }); } diff --git a/frontend/src/hooks/profiles/use-delete-profile.ts b/frontend/src/hooks/profiles/use-delete-profile.ts index 351200f..867948c 100644 --- a/frontend/src/hooks/profiles/use-delete-profile.ts +++ b/frontend/src/hooks/profiles/use-delete-profile.ts @@ -1,33 +1,27 @@ -import { useMutation, type UseMutationResult, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + type UseMutationResult, + useQueryClient, +} from "@tanstack/react-query"; import { useAccount } from "wagmi"; import type { DeleteProfileInput, DeleteProfileResponse, } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; -import { getToken } from "@/lib/utils/jwt"; +import { isTokenValid, getToken } from "@/lib/utils/jwt"; +import { useLogin } from "@/hooks/use-login"; async function deleteProfile( address: string, - signerAddress: string, - signature: string, - token?: string + token: string ): Promise { - const headers: HeadersInit = { - "Content-Type": "application/json", - }; - - // Use JWT token if available, otherwise use SIWE signature - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } else { - headers["x-eth-address"] = signerAddress; - headers["x-eth-signature"] = signature; - } - const response = await fetch(`${API_BASE_URL}/profiles/${address}`, { method: "DELETE", - headers, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, }); if (!response.ok) { @@ -45,28 +39,34 @@ async function deleteProfile( } } -type MutationVariables = { - signature: string; -}; - export function useDeleteProfile(): UseMutationResult< DeleteProfileResponse, Error, - MutationVariables + void > { const { address } = useAccount(); const queryClient = useQueryClient(); + const { login } = useLogin(); - return useMutation({ + return useMutation({ mutationKey: ["delete-profile"], - mutationFn: async ({ signature }) => { + mutationFn: async () => { if (!address) throw new Error("Wallet not connected"); + + // Check if token is valid, if not trigger login + if (!isTokenValid()) { + await login(); + } + const token = getToken(); - return deleteProfile(address, address, signature, token || undefined); + if (!token) { + throw new Error("Authentication required. Please sign in."); + } + + return deleteProfile(address, token); }, onSuccess: () => { - // Invalidate nonce query since it was incremented - queryClient.invalidateQueries({ queryKey: ["nonce", address] }); + queryClient.invalidateQueries({ queryKey: ["profiles"] }); }, }); } diff --git a/frontend/src/hooks/profiles/use-update-profile.ts b/frontend/src/hooks/profiles/use-update-profile.ts index 4ec01c8..d619618 100644 --- a/frontend/src/hooks/profiles/use-update-profile.ts +++ b/frontend/src/hooks/profiles/use-update-profile.ts @@ -1,34 +1,28 @@ -import { useMutation, type UseMutationResult, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + type UseMutationResult, + useQueryClient, +} from "@tanstack/react-query"; import { useAccount } from "wagmi"; import type { UpdateProfileInput, UpdateProfileResponse, } from "@/lib/types/api"; import { API_BASE_URL } from "@/lib/constants/apiConstants"; -import { getToken } from "@/lib/utils/jwt"; +import { isTokenValid, getToken } from "@/lib/utils/jwt"; +import { useLogin } from "@/hooks/use-login"; async function putUpdateProfile( address: string, body: UpdateProfileInput, - signerAddress: string, - signature: string, - token?: string + token: string ): Promise { - const headers: HeadersInit = { - "Content-Type": "application/json", - }; - - // Use JWT token if available, otherwise use SIWE signature - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } else { - headers["x-eth-address"] = signerAddress; - headers["x-eth-signature"] = signature; - } - const response = await fetch(`${API_BASE_URL}/profiles/${address}`, { method: "PUT", - headers, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, body: JSON.stringify(body), }); @@ -49,7 +43,6 @@ async function putUpdateProfile( type MutationVariables = { input: UpdateProfileInput; - signature: string; }; export function useUpdateProfile(): UseMutationResult< @@ -59,17 +52,27 @@ export function useUpdateProfile(): UseMutationResult< > { const { address } = useAccount(); const queryClient = useQueryClient(); + const { login } = useLogin(); return useMutation({ mutationKey: ["update-profile"], - mutationFn: async ({ input, signature }) => { + mutationFn: async ({ input }) => { if (!address) throw new Error("Wallet not connected"); + + // Check if token is valid, if not trigger login + if (!isTokenValid()) { + await login(); + } + const token = getToken(); - return putUpdateProfile(address, input, address, signature, token || undefined); + if (!token) { + throw new Error("Authentication required. Please sign in."); + } + + return putUpdateProfile(address, input, token); }, onSuccess: () => { - // Invalidate nonce query since it was incremented - queryClient.invalidateQueries({ queryKey: ["nonce", address] }); + queryClient.invalidateQueries({ queryKey: ["profiles"] }); }, }); }