feat(auth): per-user UI language preference (sq default, en)

Add users.language ('sq'|'en', default 'sq'; migration 0003). Returned from
/api/auth/login and /api/auth/me (read from the DB, not the JWT — so changing it
needs no re-login). New PUT /api/auth/language for self-service. Loaded on login
and restored from any booth. Printed tickets stay Albanian (customer-facing).
This commit is contained in:
2026-06-18 11:47:30 +02:00
parent 062feeae2f
commit 445bca0bf6
5 changed files with 855 additions and 3 deletions
+28 -3
View File
@@ -16,6 +16,12 @@ interface LoginBody {
password: string;
}
const LANGS = ["sq", "en"] as const;
type Lang = (typeof LANGS)[number];
interface LanguageBody {
language: Lang;
}
export async function authRoutes(app: FastifyInstance, db: Db): Promise<void> {
app.post<{ Body: LoginBody }>("/api/auth/login", async (req, reply) => {
const { username, password } = req.body ?? {};
@@ -41,7 +47,9 @@ export async function authRoutes(app: FastifyInstance, db: Db): Promise<void> {
csrf,
});
setAuthCookies(reply, token, csrf);
return { id: user.id, username: user.username, role: user.role };
// `language` is NOT in the JWT (identity/role only) — it's a mutable preference
// read from the DB, so changing it needs no token refresh.
return { id: user.id, username: user.username, role: user.role, language: user.language };
});
app.post("/api/auth/logout", async (_req, reply) => {
@@ -49,13 +57,30 @@ export async function authRoutes(app: FastifyInstance, db: Db): Promise<void> {
return { ok: true };
});
// Who am I — used by the SPA to bootstrap session state on load.
// Who am I — used by the SPA to bootstrap session state on load. Reads the live
// `language` preference from the DB (not the token).
app.get(
"/api/auth/me",
{ preHandler: requireRole("admin", "operator", "cashier", "readonly") },
async (req) => {
const { sub, username, role } = req.user;
return { id: sub, username, role };
const row = await db.select().from(users).where(eq(users.id, sub)).get();
return { id: sub, username, role, language: row?.language ?? "sq" };
},
);
// Change MY own UI language preference (any signed-in user). Persisted to the
// users row so it's restored on the next login, from any booth. See i18n.md.
app.put<{ Body: LanguageBody }>(
"/api/auth/language",
{ preHandler: requireRole("admin", "operator", "cashier", "readonly") },
async (req, reply) => {
const language = req.body?.language;
if (!language || !LANGS.includes(language)) {
return reply.code(400).send({ error: `language must be one of: ${LANGS.join(", ")}` });
}
await db.update(users).set({ language }).where(eq(users.id, req.user.sub)).run();
return { language };
},
);
}