--- sessionId: session-261005-174554-ealk --- # Requirements ### Overview & Goals The objective is to dynamically resolve and read the current user's ID from their JWT authentication token when voting on amendments in `src/components/AmendmentCarousel.vue`. This replaces hardcoded placeholder user IDs (`userId: 1`) with genuine authenticated user context to ensure reliable vote attribution and system integrity. ### Scope - **In Scope**: - Implement a dedicated JWT decoding and auth helper utility (`src/utils/auth.ts`) to extract payload claims (specifically `userId`, `user_id`, or `sub`) from stored tokens in `localStorage` (`authToken` / `token`). - Update `confirm` and `reject` voting methods in `src/components/AmendmentCarousel.vue` to dynamically retrieve the authenticated user ID. - Handle edge cases gracefully when the JWT is absent, expired, or missing user identification. - **Out of Scope**: - Backend token signing or signature verification algorithms (handled on the server). - Redesigning unrelated UI flows or authentication modals. ### User Stories - As a logged-in user, I want my votes on amendments to be recorded under my actual account ID decoded from my session JWT so that my voting contributions are correctly counted. - As an unauthenticated or expired user, I want voting attempts to fail gracefully with informative feedback rather than submitting fraudulent or invalid IDs. ### Functional Requirements - **JWT Claim Extraction**: Safely decode Base64Url-encoded JWT tokens stored in `localStorage` (`authToken` and `token` fallbacks) without third-party runtime bloat. - **User ID Resolution**: Extract the numerical user identifier from `userId`, `user_id`, or `sub` claims in the token payload. - **Dynamic Vote Submission**: In `AmendmentCarousel.vue`, invoke `AmendmentService.voteAmendment` with the decoded `userId`. - **Missing Token Handling**: If no valid user ID can be resolved, log a clear warning or error and prevent submitting invalid voting requests. # Technical Design ### Current Implementation In `src/components/BrandBar.vue`, upon Freighter authentication, the token is stored: ```typescript const jwt = await AuthService.loginFreighter({ stellarAddress: addr.address }); localStorage.setItem("authToken", jwt.token as string); ``` In `src/components/AmendmentCarousel.vue`, `confirm` and `reject` currently use a hardcoded `userId: 1`: ```typescript const confirm = async (amendmentId: number) => { try { await AmendmentService.voteAmendment({ amendmentId, inFavor: true, userId: 1, // Hardcoded placeholder }); await getAmendments(); } catch (e) { console.error("Failed to confirm amendment:", e); } }; ``` ### Key Decisions - **Lightweight Client-Side JWT Parser**: Implement a lightweight decoding utility in `src/utils/auth.ts` using native Base64Url decoding (`atob` / `decodeURIComponent`) to minimize dependencies while ensuring high performance. - **Flexible Token Key Resolution**: Check `localStorage.getItem("authToken")` with fallback to `localStorage.getItem("token")` to ensure seamless compatibility across all authentication paths. - **Graceful Error Handling**: If no valid user ID is present in the token, log an error / set component error state and avoid making a malformed API call. ### Proposed Changes 1. **`src/utils/auth.ts`**: - Create utility functions: ```typescript export interface JwtPayload { userId?: number; user_id?: number; sub?: string | number; stellarAddress?: string; exp?: number; [key: string]: unknown; } export function parseJwt(token: string): JwtPayload | null { try { const base64Url = token.split('.')[1]; if (!base64Url) return null; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent( atob(base64) .split('') .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('') ); return JSON.parse(jsonPayload); } catch { return null; } } export function getAuthToken(): string | null { try { return localStorage.getItem('authToken') || localStorage.getItem('token'); } catch { return null; } } export function getUserIdFromJwt(): number | undefined { const token = getAuthToken(); if (!token) return undefined; const payload = parseJwt(token); if (!payload) return undefined; const rawId = payload.userId ?? payload.user_id ?? payload.sub; const parsedId = Number(rawId); return !isNaN(parsedId) && parsedId > 0 ? parsedId : undefined; } ``` 2. **`src/components/AmendmentCarousel.vue`**: - Import `getUserIdFromJwt` from `../utils/auth.ts`. - Update `confirm` and `reject`: ```typescript const confirm = async (amendmentId: number) => { const userId = getUserIdFromJwt(); if (!userId) { console.error("User is not authenticated or user ID is missing from JWT."); error.value = "Please connect your wallet to vote on amendments."; return; } try { await AmendmentService.voteAmendment({ amendmentId, inFavor: true, userId, }); await getAmendments(); } catch (e) { console.error("Failed to confirm amendment:", e); } }; ``` - Apply matching logic for `reject(amendmentId: number)`. ### File Structure - **New File**: `src/utils/auth.ts` (JWT extraction and user identification helpers) - **Modified File**: `src/components/AmendmentCarousel.vue` (Dynamic user ID in voting handlers) # Testing ### Validation Approach - Execute TypeScript diagnostics to ensure strict type compliance. - Verify JWT decoding behavior against standard token formats, invalid tokens, and missing tokens. ### Key Scenarios 1. **Valid JWT Present**: When `authToken` contains a valid JWT with `userId: 42`, `confirm` and `reject` pass `userId: 42` to `AmendmentService.voteAmendment`. 2. **Alternative Claims**: When JWT contains `user_id` or `sub`, the parser accurately extracts the numeric user ID. 3. **No Token / Invalid Token**: When no token exists or the token is malformed, voting is blocked gracefully with appropriate user/console error messaging. # Delivery Steps ### ✓ Step 1: Implement JWT decoding and auth helper utility Create robust utility functions to read and decode JWT claims from localStorage. - Create `src/utils/auth.ts` with `parseJwt`, `getAuthToken`, and `getUserIdFromJwt`. - Support standard Base64Url decoding with UTF-8 character safety. - Extract `userId`, `user_id`, and `sub` claims, validating numeric output. ### ✓ Step 2: Connect dynamic user ID resolution in AmendmentCarousel Update vote submission handlers to use the decoded user ID from the active JWT session. - Import `getUserIdFromJwt` in `src/components/AmendmentCarousel.vue`. - Update `confirm` to obtain the active user ID and pass it in `VoteAmendmentRequest`. - Update `reject` to obtain the active user ID and pass it in `VoteAmendmentRequest`. - Add validation and user feedback when the user is not authenticated.