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",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"test": "node --test --experimental-strip-types src/lib/*.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@such-software/smirk-dapp-api": "^0.3.0",
|
"@such-software/smirk-dapp-api": "^0.3.0",
|
||||||
|
|||||||
+2
-2
@@ -150,8 +150,8 @@ export default function Home() {
|
|||||||
.then((userData) => {
|
.then((userData) => {
|
||||||
setUser({
|
setUser({
|
||||||
id: userData.id,
|
id: userData.id,
|
||||||
telegram_id: userData.telegram_id,
|
telegram_id: userData.telegram_id ?? null,
|
||||||
telegram_username: userData.telegram_username,
|
telegram_username: userData.telegram_username ?? null,
|
||||||
});
|
});
|
||||||
setStep('logged-in');
|
setStep('logged-in');
|
||||||
})
|
})
|
||||||
|
|||||||
+8
-4
@@ -1,3 +1,5 @@
|
|||||||
|
import { parseUsernameResponse } from './username';
|
||||||
|
|
||||||
// API client for Smirk backend
|
// API client for Smirk backend
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||||
@@ -24,9 +26,11 @@ export interface AuthResponse {
|
|||||||
|
|
||||||
export interface MeResponse {
|
export interface MeResponse {
|
||||||
id: string;
|
id: string;
|
||||||
username: string | null;
|
username?: string | null;
|
||||||
telegram_id: number | null;
|
/** Legacy backend only; absent on the v0.3 backend. */
|
||||||
telegram_username: string | null;
|
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> {
|
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');
|
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 }> {
|
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,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
|
|||||||
Reference in New Issue
Block a user