mirror of
https://github.com/Such-Software/smirk-website.git
synced 2026-10-17 05:51:28 +02:00
fix(settings): read the v0.3 username response shape
The v0.3 backend answers GET /users/me/username with {"username": ...};
the legacy backend sent a bare string. The page put the object into state
and rendered it, so /settings threw for every signed-in user.
parseUsernameResponse accepts both shapes and returns only a string or
null. Legacy-only fields on /auth/me are optional. Adds node:test cases,
run with npm test.
This commit is contained in:
+2
-1
@@ -6,7 +6,8 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
"lint": "eslint",
|
||||
"test": "node --test --experimental-strip-types src/lib/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@such-software/smirk-dapp-api": "^0.3.0",
|
||||
|
||||
+2
-2
@@ -150,8 +150,8 @@ export default function Home() {
|
||||
.then((userData) => {
|
||||
setUser({
|
||||
id: userData.id,
|
||||
telegram_id: userData.telegram_id,
|
||||
telegram_username: userData.telegram_username,
|
||||
telegram_id: userData.telegram_id ?? null,
|
||||
telegram_username: userData.telegram_username ?? null,
|
||||
});
|
||||
setStep('logged-in');
|
||||
})
|
||||
|
||||
+8
-4
@@ -1,3 +1,5 @@
|
||||
import { parseUsernameResponse } from './username';
|
||||
|
||||
// API client for Smirk backend
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
@@ -24,9 +26,11 @@ export interface AuthResponse {
|
||||
|
||||
export interface MeResponse {
|
||||
id: string;
|
||||
username: string | null;
|
||||
telegram_id: number | null;
|
||||
telegram_username: string | null;
|
||||
username?: string | null;
|
||||
/** Legacy backend only; absent on the v0.3 backend. */
|
||||
telegram_id?: number | null;
|
||||
/** Legacy backend only; absent on the v0.3 backend. */
|
||||
telegram_username?: string | null;
|
||||
}
|
||||
|
||||
export async function getChallenge(origin: string): Promise<ChallengeResponse> {
|
||||
@@ -353,7 +357,7 @@ export async function getMyUsername(token: string): Promise<string | null> {
|
||||
throw new Error('Failed to get username');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
return parseUsernameResponse(await res.json().catch(() => null));
|
||||
}
|
||||
|
||||
export async function setUsername(token: string, username: string): Promise<{ username: string }> {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { parseUsernameResponse } from './username.ts';
|
||||
|
||||
test('accepts the v0.3 object shape', () => {
|
||||
assert.equal(parseUsernameResponse({ username: 'alice' }), 'alice');
|
||||
assert.equal(parseUsernameResponse({ username: null }), null);
|
||||
});
|
||||
|
||||
test('accepts the legacy bare string shape', () => {
|
||||
assert.equal(parseUsernameResponse('alice'), 'alice');
|
||||
assert.equal(parseUsernameResponse(null), null);
|
||||
});
|
||||
|
||||
test('never returns a non-string', () => {
|
||||
for (const input of [undefined, '', 42, true, [], {}, { username: 7 }, { username: { a: 1 } }]) {
|
||||
const result = parseUsernameResponse(input);
|
||||
assert.ok(result === null || typeof result === 'string');
|
||||
assert.equal(result, null);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Normalize a `GET /users/me/username` response to a username or null.
|
||||
*
|
||||
* The legacy backend answered with a bare JSON string (or null). The v0.3
|
||||
* backend answers with `{ "username": string | null }`. Anything else, including
|
||||
* an empty string, is treated as "no username" so a shape change can never put a
|
||||
* non-string into React state and crash the page.
|
||||
*/
|
||||
export function parseUsernameResponse(data: unknown): string | null {
|
||||
const value =
|
||||
data !== null && typeof data === 'object' && 'username' in data
|
||||
? (data as { username: unknown }).username
|
||||
: data;
|
||||
return typeof value === 'string' && value.length > 0 ? value : null;
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
Reference in New Issue
Block a user