|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- ---
- 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.
|