diff --git a/api/src/auth.test.ts b/api/src/auth.test.ts index 57bdbb05..b95fb9d5 100644 --- a/api/src/auth.test.ts +++ b/api/src/auth.test.ts @@ -48,6 +48,14 @@ test('HTTP flow: sign up, sign in, wrong password, change password, delete', asy assert.equal((await call('GET', '/api/auth/me', undefined, t1)).statusCode, 401, 'other sessions signed out'); assert.equal((await call('GET', '/api/auth/me', undefined, t2)).statusCode, 200, 'this session kept'); + // profile: Urdu name and bio, trimmed, control characters dropped, length-limited + const prof = (await call('POST', '/api/auth/profile', { full_name: ' مرزا اسد اللہ\u0007 خان ', bio: 'شاعر۔ '.repeat(300) }, t2)).json().user; + assert.equal(prof.full_name, 'مرزا اسد اللہ خان'); + assert.equal(prof.bio.length, 1000); + assert.equal((await call('GET', '/api/auth/me', undefined, t2)).json().user.full_name, 'مرزا اسد اللہ خان'); + assert.equal((await call('POST', '/api/auth/profile', { full_name: '', bio: '' }, t2)).json().user.full_name, '', 'cleared'); + assert.equal((await call('POST', '/api/auth/profile', { full_name: 'x' })).statusCode, 401); + await call('POST', '/api/auth/signout', undefined, t2); assert.equal((await call('GET', '/api/auth/me', undefined, t2)).statusCode, 401); diff --git a/api/src/auth.ts b/api/src/auth.ts index 3f1ff143..db881dd3 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -8,6 +8,7 @@ // POST /api/auth/signout Bearer token // POST /api/auth/password Bearer token {current, next} -> other sessions signed out // POST /api/auth/delete Bearer token {password} -> account and its data deleted +// POST /api/auth/profile Bearer token {full_name, bio} (Urdu or any text; trimmed, length-limited) import { createHash, randomBytes, scrypt, timingSafeEqual } from 'node:crypto'; import type { FastifyInstance, FastifyRequest } from 'fastify'; import { pool } from './db.ts'; @@ -59,7 +60,9 @@ const signinByIp = limiter(20, 15 * 60_000), signinByEmail = limiter(8, 15 * 60_ function sha(t: string) { return createHash('sha256').update(t).digest('hex'); } -export const publicUser = (u: any) => ({ id: Number(u.id), email: u.email, role: u.role as string, created_at: u.created_at }); +export const publicUser = (u: any) => ({ + id: Number(u.id), email: u.email, role: u.role as string, created_at: u.created_at, full_name: u.full_name ?? '', bio: u.bio ?? '', +}); async function newSession(userId: number) { const token = randomBytes(32).toString('base64url'); @@ -125,6 +128,15 @@ export function authRoutes(app: FastifyInstance) { return { ok: true }; }); + app.post<{ Body: { full_name?: string; bio?: string } }>('/api/auth/profile', async (req, reply) => { + const u = await sessionUser(req); + if (!u) return reply.code(401).send({ error: 'دوبارہ لاگ ان کریں' }); + const text = (v: unknown, max: number) => String(v ?? '').normalize('NFC').replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, '').trim().slice(0, max); + const full_name = text(req.body?.full_name, 100).replace(/\s+/g, ' '), bio = text(req.body?.bio, 1000); + const { rows } = await pool.query('UPDATE users SET full_name = $1, bio = $2 WHERE id = $3 RETURNING *', [full_name || null, bio || null, u.id]); + return { user: publicUser(rows[0]) }; + }); + app.post<{ Body: { password?: string } }>('/api/auth/delete', async (req, reply) => { const u = await sessionUser(req); if (!u) return reply.code(401).send({ error: 'دوبارہ لاگ ان کریں' }); diff --git a/db/schema.sql b/db/schema.sql index 34f5a888..cf7efce8 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -123,3 +123,6 @@ CREATE TABLE IF NOT EXISTS grants ( CHECK ((scope = 'all') = (scope_id IS NULL)) ); CREATE INDEX IF NOT EXISTS grants_user ON grants(user_id); +-- profile (owner request): full name and bio, usually in Urdu +ALTER TABLE users ADD COLUMN IF NOT EXISTS full_name text; -- up to 100 characters +ALTER TABLE users ADD COLUMN IF NOT EXISTS bio text; -- up to 1,000 characters diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro index 10674a0f..32148a46 100644 --- a/web/src/layouts/Base.astro +++ b/web/src/layouts/Base.astro @@ -43,7 +43,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
{user.email} · رکنیت: {ud(since.getFullYear())}
+ {user.bio &&{user.bio}
} {error &&{error}
} {done &&{done}
} + +