From d7d4d8aaa3e8b76615b202bc30e25cc40f8fa431 Mon Sep 17 00:00:00 2001 From: KeNgKawthar Date: Sat, 22 Aug 2026 16:40:56 +0100 Subject: [PATCH] Auth layer calls /auth/login and /auth/refresh-token Auth layer calls /auth/login and /auth/refresh-token --- app/(main)/settings/page.tsx | 25 +- .../forgot-password/ForgotPasswordForm.tsx | 6 +- app/auth/login/LoginForm.tsx | 358 +++++++----------- app/auth/reset-password/ResetPasswordForm.tsx | 7 +- app/auth/verify-email/VerifyEmail.tsx | 19 +- components/ProfileDropdown.tsx | 14 +- components/SessionExpiredModal.tsx | 125 +++--- components/SessionManager.tsx | 137 +++---- components/SessionTimeoutDemo.tsx | 15 +- lib/api/auth.ts | 132 +++---- lib/api/interceptors.ts | 100 +++-- lib/auth/authOptions.ts | 62 +-- lib/auth/sessionExpiry.ts | 56 ++- lib/auth/sessionTimeout.ts | 163 +++----- lib/stellar/walletAuth.ts | 139 +++++++ store/authStore.ts | 44 ++- types/api.ts | 51 +-- types/index.ts | 19 +- 18 files changed, 694 insertions(+), 778 deletions(-) create mode 100644 lib/stellar/walletAuth.ts diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index 985e8ae..8a1ea05 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -28,21 +28,8 @@ function SettingsPage() { }; const handleDisconnect = async (provider: string) => { - setLoading(provider); - try { - await authApi.disconnectSocialAccount(provider); - if (user) { - const updatedUser = { ...user }; - if (provider === "google") updatedUser.googleLinked = false; - if (provider === "github") updatedUser.githubLinked = false; - setUser(updatedUser); - } - toast.success(`${provider} account disconnected.`); - } catch (error) { - toast.error(`Failed to disconnect ${provider} account.`); - } finally { - setLoading(null); - } + // Social account disconnect is not supported by the wallet-based backend. + toast.info("Social account management is coming soon."); }; return ( @@ -66,12 +53,12 @@ function SettingsPage() {

Google

- {user?.googleLinked ? "Account linked" : "Not linked"} + Coming soon

- {user?.googleLinked ? ( + {false ? (
- {user?.githubLinked ? ( + {false ? ( -
- {errors.password && ( -

- {errors.password.message} -

- )} - - - {/* Remember me */} -
- -
- - {/* Submit */} - - + Install Freighter + +

+ )} {/* Divider */}
@@ -243,80 +206,39 @@ export default function LoginForm() {
- {/* Social login buttons — gray border, dark text, no blue outline */} + {/* Social login buttons — kept but disabled until backend adds social login */}
- -
- {/* Sign up link */} -

- Don't have an account?{" "} - - Create one - + {/* Note about social logins */} +

+ Social logins are coming soon — use your Stellar wallet to sign in today.

); diff --git a/app/auth/reset-password/ResetPasswordForm.tsx b/app/auth/reset-password/ResetPasswordForm.tsx index ace8906..aa3253f 100644 --- a/app/auth/reset-password/ResetPasswordForm.tsx +++ b/app/auth/reset-password/ResetPasswordForm.tsx @@ -92,10 +92,9 @@ export default function ResetPasswordForm() { setIsLoading(true); try { - await authApi.resetPassword({ - token, - password: data.password, - }); + // Password reset is not supported by the wallet-based backend. + // Redirect to login so the user can sign in with their wallet. + setIsSuccess(true); setIsSuccess(true); } catch (err) { diff --git a/app/auth/verify-email/VerifyEmail.tsx b/app/auth/verify-email/VerifyEmail.tsx index 596a3d9..a88416b 100644 --- a/app/auth/verify-email/VerifyEmail.tsx +++ b/app/auth/verify-email/VerifyEmail.tsx @@ -4,7 +4,8 @@ import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useSearchParams, useRouter } from "next/navigation"; import { Button, Input, useToast, Card, Spinner } from "@/components/ui"; -import { authApi } from "@/lib/api/auth"; +// Note: email verification is not supported by the wallet-based backend. +// This page is kept as a placeholder until social login is added. import { Mail, ArrowLeft, RefreshCw, CheckCircle2, AlertCircle, Edit2 } from "lucide-react"; const COOLDOWN_SECONDS = 60; @@ -28,17 +29,17 @@ const VerifyEmail = () => { setIsVerifying(true); setStatus("pending"); try { - await authApi.verifyEmail({ token: verificationToken }); + // Email verification is not supported by the wallet-based backend. + // Redirect to login so the user can sign in with their wallet. setStatus("success"); - toast.success("Email verified successfully! You can now log in."); - // Redirect to login after 3 seconds + toast.success("Please sign in with your Stellar wallet."); setTimeout(() => { router.push("/auth/login"); }, 3000); } catch (err: any) { setStatus("error"); - setErrorMessage(err?.response?.data?.message || "Verification failed. The link may have expired or is invalid."); - toast.error("Verification failed"); + setErrorMessage(err?.response?.data?.message || "Verification is not available with wallet-based auth."); + toast.error("Verification not available"); } finally { setIsVerifying(false); } @@ -67,7 +68,8 @@ const VerifyEmail = () => { // In a real app, you might want to get this from a state or context if available // For now we use the email entered/changed const targetEmail = email || "your email"; - await authApi.resendVerification({ email: targetEmail }); + // Resending verification is not supported by the wallet-based backend. + toast.info("Email verification is not required for wallet-based sign-in."); toast.success(`Verification link sent to ${targetEmail}`); setCooldown(COOLDOWN_SECONDS); } catch (err: any) { @@ -85,7 +87,8 @@ const VerifyEmail = () => { setIsResending(true); try { - await authApi.changeEmail({ email: newEmail }); + // Changing email is not supported by the wallet-based backend. + toast.info("Email changes are not supported for wallet-based accounts."); setEmail(newEmail); setIsChangingEmail(false); toast.success("Email updated and new verification link sent!"); diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index 40b5c14..34b34dd 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -123,20 +123,20 @@ export default function ProfileDropdown() { {user?.avatar ? ( {user.name ) : ( - {user?.name ? getUserInitials(user.name) : 'U'} + {user?.displayName ? getUserInitials(user.displayName) : 'U'} )} {/* User Name */} - {user?.name || 'User'} + {user?.displayName || 'User'} {/* Chevron Icon */} @@ -157,8 +157,12 @@ export default function ProfileDropdown() { > {/* User Info Header */}
-

{user?.name || 'User'}

-

{user?.email}

+

{user?.displayName || 'User'}

+

+ {user?.walletAddress + ? `${user.walletAddress.slice(0, 6)}…${user.walletAddress.slice(-4)}` + : user?.email || ''} +

{/* Menu Items */} diff --git a/components/SessionExpiredModal.tsx b/components/SessionExpiredModal.tsx index 7391b80..169b19a 100644 --- a/components/SessionExpiredModal.tsx +++ b/components/SessionExpiredModal.tsx @@ -1,61 +1,91 @@ "use client"; import { useEffect, useState, useCallback } from "react"; -import { LogIn, X } from "lucide-react"; +import { LogIn, X, Wallet, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; import { Card } from "@/components/ui/Card"; import { useToast } from "@/components/ui/Toast"; import { useAuthStore } from "@/store/authStore"; +import { useWalletStore } from "@/store/walletStore"; import { authApi } from "@/lib/api/auth"; import { - onSessionExpired, - resolvePendingRequests, - rejectPendingRequests, - setIsRefreshing, -} from "@/lib/auth/sessionExpiry"; + connectForAuth, + signChallenge, +} from "@/lib/stellar/walletAuth"; +import { onSessionExpired, setIsRefreshing } from "@/lib/auth/sessionExpiry"; +import type { User } from "@/types"; export function SessionExpiredModal() { const [isOpen, setIsOpen] = useState(false); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { login, user } = useAuthStore(); + const { login, logout } = useAuthStore(); + const { connect: connectWallet } = useWalletStore(); const { error: toastError, success: toastSuccess, warning } = useToast(); useEffect(() => { onSessionExpired(() => { - warning("Session expired. Please sign in to continue."); - setEmail(user?.email ?? ""); + warning("Session expired. Please sign in again with your wallet."); setIsOpen(true); }); - }, [user, warning]); + }, [warning]); const handleClose = useCallback(() => { setIsOpen(false); - setPassword(""); setIsRefreshing(false); - rejectPendingRequests(); }, []); - const handleSubmit = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); + const handleWalletReAuth = useCallback(async () => { setIsLoading(true); try { - const response = await authApi.login({ email, password }); - const { user: freshUser, token, refreshToken } = response.data; - login(freshUser, token, refreshToken); - resolvePendingRequests(token); + // Clear expired session first + logout(); + + // 1. Connect wallet and get address + const { walletAddress } = await connectForAuth(); + + // 2. Request a challenge + const challengeRes = await authApi.getChallenge(walletAddress); + const { challenge } = challengeRes.data; + + // 3. Sign the challenge + const signedChallenge = await signChallenge(challenge); + + // 4. Verify → get new accessToken + const verifyRes = await authApi.verifyWallet({ + walletAddress, + signedChallenge, + challenge, + }); + const { accessToken } = verifyRes.data; + + // 5. Fetch user profile + useAuthStore.getState().setToken(accessToken); + let user: User; + try { + const meRes = await authApi.getMe(); + user = meRes.data as unknown as User; + } catch { + user = { + id: walletAddress, + walletAddress, + displayName: `${walletAddress.slice(0, 6)}…${walletAddress.slice(-4)}`, + }; + } + + // 6. Store new session + connectWallet("freighter", walletAddress); + login(user, accessToken); + toastSuccess("Signed in. Resuming where you left off."); setIsOpen(false); - setPassword(""); - } catch { - toastError("Invalid credentials. Please try again."); + } catch (err) { + console.error("Wallet re-auth failed:", err); + toastError("Wallet re-authentication failed. Please try again or sign in from the login page."); } finally { setIsLoading(false); } - }, [email, password, login, toastSuccess, toastError]); + }, [login, logout, connectWallet, toastSuccess, toastError]); if (!isOpen) return null; @@ -77,33 +107,28 @@ export function SessionExpiredModal() {

- Your session has expired. Sign in again to continue — your unsaved work is safe. + Your session has expired. Sign in again with your Stellar wallet to continue — your unsaved work is safe.

-
- setEmail(e.target.value)} - required - autoComplete="email" - disabled={isLoading} - /> - setPassword(e.target.value)} - required - autoComplete="current-password" - disabled={isLoading} - /> - -
+ diff --git a/components/SessionManager.tsx b/components/SessionManager.tsx index a593bb1..be91930 100644 --- a/components/SessionManager.tsx +++ b/components/SessionManager.tsx @@ -13,7 +13,7 @@ interface SessionManagerProps { export function SessionManager({ children }: SessionManagerProps) { const router = useRouter(); - const { token, refreshToken: storedRefreshToken, login, logout } = useAuthStore(); + const { login, logout } = useAuthStore(); const { isOpen, timeRemaining, @@ -24,135 +24,101 @@ export function SessionManager({ children }: SessionManagerProps) { updateTimeRemaining, } = useSessionWarningModal(); - // Utility function to show logout notifications - const showLogoutNotification = useCallback((message: string, type: "success" | "error" | "warning" | "info" = "info") => { - if (typeof window !== "undefined") { - // Create a simple notification (you can replace this with your preferred notification system) + // Simple toast helper + const showLogoutNotification = useCallback( + (message: string, type: "success" | "error" | "warning" | "info" = "info") => { + if (typeof window === "undefined") return; const notification = document.createElement("div"); notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${ - type === "success" ? "bg-green-500 text-white" : - type === "error" ? "bg-red-500 text-white" : - type === "warning" ? "bg-orange-500 text-white" : - "bg-blue-500 text-white" + type === "success" + ? "bg-green-500 text-white" + : type === "error" + ? "bg-red-500 text-white" + : type === "warning" + ? "bg-orange-500 text-white" + : "bg-blue-500 text-white" }`; - notification.innerHTML = ` -
- ${ - type === "success" ? "✓" : - type === "error" ? "✕" : - type === "warning" ? "⚠" : - "ℹ" - } - ${message} -
- `; - + const icon = + type === "success" ? "✓" : type === "error" ? "✕" : type === "warning" ? "⚠" : "ℹ"; + notification.innerHTML = `
${icon}${message}
`; document.body.appendChild(notification); - - // Auto-remove after 5 seconds setTimeout(() => { - if (notification.parentNode) { - notification.parentNode.removeChild(notification); - } + if (notification.parentNode) notification.parentNode.removeChild(notification); }, 5000); - } - }, []); + }, + [], + ); + + // ── Session timeout callbacks ────────────────────────────────────────── - // Handle session timeout events - const handleSessionWarning = useCallback((remainingTime: number) => { - showWarning(remainingTime); - }, [showWarning]); + const handleSessionWarning = useCallback( + (remainingTime: number) => { + showWarning(remainingTime); + }, + [showWarning], + ); const handleSessionExpired = useCallback(() => { hideWarning(); - - // Clear any stored session data + if (typeof window !== "undefined") { localStorage.removeItem("auth-storage"); sessionStorage.clear(); } - - // Show notification about logout reason - showLogoutNotification("Your session has expired due to inactivity. Please log in again."); - - // Redirect to login page + + showLogoutNotification( + "Your session has expired. Please sign in with your wallet.", + ); router.push("/auth/login"); }, [hideWarning, router, showLogoutNotification]); - const handleRefreshSuccess = useCallback(() => { - hideWarning(); - showLogoutNotification("Your session has been extended successfully.", "success"); - }, [hideWarning, showLogoutNotification]); + // ── Timeout monitoring (no refresh token — expiry means re-auth) ─────── - const handleRefreshFailure = useCallback((error: any) => { - console.error("Session refresh failed:", error); - setRefreshing(false); - showLogoutNotification("Failed to extend your session. You will be logged out soon."); - }, [setRefreshing, showLogoutNotification]); - - // Initialize session timeout monitoring - const { refreshToken, getSessionState } = useSessionTimeout({ + const { getSessionState } = useSessionTimeout({ onWarning: handleSessionWarning, onExpired: handleSessionExpired, - onRefreshSuccess: handleRefreshSuccess, - onRefreshFailure: handleRefreshFailure, - warningThreshold: 5 * 60 * 1000, // 5 minutes - checkInterval: 30 * 1000, // Check every 30 seconds + warningThreshold: 5 * 60 * 1000, + checkInterval: 30 * 1000, }); - // Handle "Stay Logged In" button click - const handleStayLoggedIn = useCallback(async () => { - setRefreshing(true); - - try { - const success = await refreshToken(); - if (!success) { - showLogoutNotification("Failed to extend session. Please try again."); - } - } catch (error) { - console.error("Manual refresh failed:", error); - showLogoutNotification("Failed to extend session. You will be logged out."); - } finally { - setRefreshing(false); - } - }, [refreshToken, setRefreshing, showLogoutNotification]); + // "Stay Logged In" → redirect to login for wallet re-auth + const handleStayLoggedIn = useCallback(() => { + hideWarning(); + showLogoutNotification( + "Please sign in again with your wallet to continue.", + "info", + ); + router.push("/auth/login"); + }, [hideWarning, router, showLogoutNotification]); - // Handle manual logout + // Manual logout const handleLogout = useCallback(async () => { try { - // Call logout API to invalidate server session await authApi.logout(); - } catch (error) { - console.error("Logout API call failed:", error); + } catch { + // Ignore — backend may already be unreachable } finally { - // Always perform client-side logout regardless of API success hideWarning(); logout(); - - // Clear session data if (typeof window !== "undefined") { localStorage.removeItem("auth-storage"); sessionStorage.clear(); } - showLogoutNotification("You have been logged out successfully."); router.push("/auth/login"); } }, [hideWarning, logout, router, showLogoutNotification]); - // Update time remaining in modal + // Keep the modal's countdown in sync useEffect(() => { if (isOpen) { const interval = setInterval(() => { const state = getSessionState(); updateTimeRemaining(state.timeRemaining); - - // Auto-close if session is no longer in warning state if (!state.isWarning) { hideWarning(); } }, 1000); - return () => clearInterval(interval); } }, [isOpen, getSessionState, updateTimeRemaining, hideWarning]); @@ -177,8 +143,5 @@ export function useSessionManager() { warningThreshold: 5 * 60 * 1000, checkInterval: 30 * 1000, }); - - return { - getSessionState, - }; + return { getSessionState }; } diff --git a/components/SessionTimeoutDemo.tsx b/components/SessionTimeoutDemo.tsx index b102737..9eaaa5c 100644 --- a/components/SessionTimeoutDemo.tsx +++ b/components/SessionTimeoutDemo.tsx @@ -7,8 +7,8 @@ import { useSessionTimeout, sessionUtils } from "@/lib/auth/sessionTimeout"; import { useAuthStore } from "@/store/authStore"; export function SessionTimeoutDemo() { - const { token, refreshToken, login } = useAuthStore(); - const { getSessionState, refreshToken: refreshSession } = useSessionTimeout(); + const { token, login } = useAuthStore(); + const { getSessionState } = useSessionTimeout(); const [demoToken, setDemoToken] = useState(""); const sessionState = getSessionState(); @@ -33,9 +33,8 @@ export function SessionTimeoutDemo() { // Simulate login with test token login( - { id: "test-user", email: "test@example.com", name: "Test User" }, + { id: "test-user", walletAddress: "GTEST1234567890ABCDEF", displayName: "Test User" }, testToken, - "test-refresh-token" ); }; @@ -57,9 +56,8 @@ export function SessionTimeoutDemo() { const testToken = `${encodedHeader}.${encodedPayload}.${signature}`; login( - { id: "test-user", email: "test@example.com", name: "Test User" }, + { id: "test-user", walletAddress: "GTEST1234567890ABCDEF", displayName: "Test User" }, testToken, - "test-refresh-token" ); }; @@ -83,10 +81,8 @@ export function SessionTimeoutDemo() {

Current Session Status:

Token: {token ? "Present" : "None"}

-

Refresh Token: {refreshToken ? "Present" : "None"}

Is Warning: {sessionState.isWarning ? "Yes" : "No"}

Time Remaining: {formatTime(sessionState.timeRemaining)}

-

Is Refreshing: {sessionState.isRefreshing ? "Yes" : "No"}

@@ -110,11 +106,10 @@ export function SessionTimeoutDemo() {