Website header: the T5DE logo, four links, and a profile menu
CI / test (push) Successful in 1m11s

The header shows T5DE's logo (the client's, also the tab icon) and Online, Rooms, Products and
Catalogs; the person's pages (Account, Library, Console for moderators) and signing out are in a menu
under their picture: their first linked IMVU account's avatar picture, which /me now gives (kept an
hour), or their initial. Creating a product moves to Products (New product, /products/new; /create
redirects there), as creating a room is on Rooms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 07:18:26 -07:00
1 parent 76c8e0342e
commit 07bbc11565
19 files changed
+160 -79

No files matched your search

+26 -1
View File
@@ -4,6 +4,7 @@
import { Elysia, t } from "elysia";
import {
and,
asc,
desc,
eq,
ilike,
@@ -151,6 +152,30 @@ export function createApp({ db, auth, imvu, realtime: rt, age: ageService, rooms
banReason: p.banReason ?? "",
});
/** A person's picture, for the website's header: their first linked IMVU account's avatar picture (null if none,
* or IMVU can't say). Kept an hour per account, so pages don't each ask IMVU. */
const pictures = new Map<number, { url: string | null; at: number }>();
const picture = async (userId: string): Promise<string | null> => {
const [first] = await db
.select({ imvuId: imvuLink.imvuId })
.from(imvuLink)
.where(eq(imvuLink.userId, userId))
.orderBy(asc(imvuLink.linkedAt))
.limit(1);
if (!first) return null;
const kept = pictures.get(first.imvuId);
if (kept && Date.now() - kept.at < 60 * 60 * 1000) return kept.url;
let url: string | null = kept?.url ?? null;
try {
url = (await imvu.byId(first.imvuId)).picture;
} catch {
// IMVU unavailable: the last known one (or none), asked again next time.
return url;
}
pictures.set(first.imvuId, { url, at: Date.now() });
return url;
};
/** Links as the website shows them: with live presence, and age by the one rule. */
const linkViews = async (userId: string, links: Link[]) => {
const ids = links.map((l) => l.imvuId);
@@ -442,7 +467,7 @@ export function createApp({ db, auth, imvu, realtime: rt, age: ageService, rooms
})
// Who's signed in, and the home page.
.get("/me", ({ me }) => ({ person: personView(me) }), { signedIn: true })
.get("/me", async ({ me }) => ({ person: { ...personView(me), picture: await picture(me.id) } }), { signedIn: true })
.get("/home", async ({ person }) => {
const online = (await visibleOnline(person)).length;
if (!person) return { onlineCount: online };
+2
View File
@@ -8,6 +8,7 @@ export type ImvuUser = {
tagline: string;
ageVerified: boolean;
accessPass: boolean;
picture: string | null; // the avatar's picture (https, on *.imvu.com)
};
export type ImvuRoom = {
@@ -182,5 +183,6 @@ export function parse(answer: unknown): ImvuUser {
tagline: typeof data.tagline === "string" ? data.tagline : "",
ageVerified: data.is_ageverified === true,
accessPass: data.is_ap === true,
picture: imvuImage(data.thumbnail_url),
};
}
+1 -1
View File
@@ -78,7 +78,7 @@ async function personWithAccounts(name: string, ids: { id: number; ageVerified?:
const cookie = { v: "" };
await call(cookie, "POST", "/api/auth/sign-up/email", { email: `${name}@example.test`, password: "a long password", name, username: name });
for (const a of ids) {
accounts.set(a.id, { id: a.id, username: `${name}${a.id}`, tagline: "", ageVerified: !!a.ageVerified, accessPass: !!a.accessPass });
accounts.set(a.id, { id: a.id, username: `${name}${a.id}`, tagline: "", ageVerified: !!a.ageVerified, accessPass: !!a.accessPass, picture: null });
const started = await call(cookie, "POST", "/api/v1/account/links", { username: `${name}${a.id}` });
accounts.get(a.id)!.tagline = started.json.challenge.code;
expect((await call(cookie, "POST", `/api/v1/account/links/${a.id}/check`)).status).toBe(200);
+1 -1
View File
@@ -67,7 +67,7 @@ async function person(name: string, ...ids: number[]): Promise<Person> {
expect(p.token).not.toBe("");
for (const [i, id] of ids.entries()) {
const username = `${name}Av${i}`;
accounts.set(id, { id, username, tagline: "", ageVerified: false, accessPass: false });
accounts.set(id, { id, username, tagline: "", ageVerified: false, accessPass: false, picture: null });
const started = await call(p, "POST", "/api/v1/account/links", { username });
accounts.get(id)!.tagline = started.json.challenge.code;
expect((await call(p, "POST", `/api/v1/account/links/${id}/check`)).status).toBe(200);
+1 -1
View File
@@ -70,7 +70,7 @@ async function person(name: string, ...imvu: { id: number; ageVerified?: boolean
p.id = (await call(p, "GET", "/api/v1/me")).json.person.id;
for (const [i, a] of imvu.entries()) {
const username = `${name}Av${i || ""}`;
accounts.set(a.id, { id: a.id, username, tagline: "", ageVerified: !!a.ageVerified, accessPass: false });
accounts.set(a.id, { id: a.id, username, tagline: "", ageVerified: !!a.ageVerified, accessPass: false, picture: null });
const started = await call(p, "POST", "/api/v1/account/links", { username });
accounts.get(a.id)!.tagline = started.json.challenge.code;
expect((await call(p, "POST", `/api/v1/account/links/${a.id}/check`)).status).toBe(200);
+2 -2
View File
@@ -66,7 +66,7 @@ async function person(name: string, ...imvu: { id: number; ageVerified?: boolean
expect(p.token).not.toBe("");
for (const [i, a] of imvu.entries()) {
const username = `${name}Av${i || ""}`;
accounts.set(a.id, { id: a.id, username, tagline: "", ageVerified: !!a.ageVerified, accessPass: false });
accounts.set(a.id, { id: a.id, username, tagline: "", ageVerified: !!a.ageVerified, accessPass: false, picture: null });
const started = await call(p, "POST", "/api/v1/account/links", { username });
accounts.get(a.id)!.tagline = started.json.challenge.code;
expect((await call(p, "POST", `/api/v1/account/links/${a.id}/check`)).status).toBe(200);
@@ -330,7 +330,7 @@ test("moderators boot and ban; owners lift bans", async () => {
const mod = await person("modr", { id: 602 });
const usr = await person("usrr", { id: 603 });
const md2 = await person("mod2", { id: 604 });
accounts.set(605, { id: 605, username: "NotOnT5", tagline: "", ageVerified: false, accessPass: false }); // IMVU only
accounts.set(605, { id: 605, username: "NotOnT5", tagline: "", ageVerified: false, accessPass: false, picture: null }); // IMVU only
const n = (await call(own, "POST", "/api/v1/hosted-rooms", newRoom(601))).json.room.n;
// Moderators: IDs or names (linked accounts, else IMVU); the owner's own account is left out; up to 20.
+6 -6
View File
@@ -130,14 +130,14 @@ describe("linking IMVU accounts", () => {
test("by tagline; the most recent proof wins; link caps", async () => {
const carol = new Browser();
await carol.signup("carol");
accounts.set(42, { id: 42, username: "CarolAv", tagline: "", ageVerified: true, accessPass: false });
accounts.set(42, { id: 42, username: "CarolAv", tagline: "", ageVerified: true, accessPass: false, picture: null });
const started = await carol.call("POST", "/api/v1/account/links", { username: "carolav" });
expect(started.json.challenge.code).toMatch(/^t5de-[A-Z2-9]{4}-[A-Z2-9]{4}$/);
const missing = await carol.call("POST", "/api/v1/account/links/42/check");
expect(missing.status).toBe(409);
expect(missing.json.error).toBe("code_missing");
const linked = await link(carol, { id: 42, username: "CarolAv", ageVerified: true, accessPass: false });
const linked = await link(carol, { id: 42, username: "CarolAv", ageVerified: true, accessPass: false, picture: null });
expect(linked.status).toBe(200);
expect(linked.json.link).toMatchObject({ imvuId: 42, username: "CarolAv", adult: true });
expect((await carol.call("POST", "/api/v1/account/links", { username: "nobody" })).json.error).toBe(
@@ -148,7 +148,7 @@ describe("linking IMVU accounts", () => {
const before = (await db.query.imvuLink.findFirst({ where: (l, { eq }) => eq(l.imvuId, 42) }))!;
const dave = new Browser();
await dave.signup("dave");
const moved = await link(dave, { id: 42, username: "CarolAv", ageVerified: false, accessPass: false });
const moved = await link(dave, { id: 42, username: "CarolAv", ageVerified: false, accessPass: false, picture: null });
expect(moved.json.moved).toBe(true);
const after = (await db.query.imvuLink.findFirst({ where: (l, { eq }) => eq(l.imvuId, 42) }))!;
expect(after.userId).not.toBe(before.userId);
@@ -160,7 +160,7 @@ describe("linking IMVU accounts", () => {
await admin.call("POST", "/api/auth/sign-in/username", { username: "alice", password: "a long password" });
const daveId = (await dave.call("GET", "/api/v1/me")).json.person.id;
expect((await admin.call("POST", `/api/v1/console/people/${daveId}/link-cap`, { linkCap: 1 })).status).toBe(200);
const over = await link(dave, { id: 43, username: "DaveAlt", ageVerified: false, accessPass: false });
const over = await link(dave, { id: 43, username: "DaveAlt", ageVerified: false, accessPass: false, picture: null });
expect(over.status).toBe(403);
expect(over.json.error).toBe("link_cap");
});
@@ -170,7 +170,7 @@ describe("T5DE's proxy", () => {
test("signs in with a device code, connects, shows presence; a ban ends it", async () => {
const erin = new Browser();
await erin.signup("erin");
await link(erin, { id: 77, username: "ErinAv", ageVerified: false, accessPass: false });
await link(erin, { id: 77, username: "ErinAv", ageVerified: false, accessPass: false, picture: null });
// The proxy asks for a code; until it's approved, the token request waits.
const codeRes = await fetch(`${base}/api/auth/device/code`, {
@@ -216,7 +216,7 @@ describe("T5DE's proxy", () => {
// An account linked while the client runs counts at once.
ws.send(JSON.stringify({ type: "session", imvu_id: 78 }));
await waitFor(() => messages.filter((m) => m.message === "not_linked").length === 2);
await link(erin, { id: 78, username: "ErinAlt", ageVerified: false, accessPass: false });
await link(erin, { id: 78, username: "ErinAlt", ageVerified: false, accessPass: false, picture: null });
await waitFor(() => hub.realtime.online().some((o) => o.imvuId === 78));
await erin.call("DELETE", "/api/v1/account/links/78");
await waitFor(() => !hub.realtime.online().some((o) => o.imvuId === 78));
+10 -2
View File
@@ -4,7 +4,7 @@ import { createLocalJWKSet, jwtVerify } from "jose";
import { openDatabase, type Database } from "@t5de/db";
import { createApp } from "../src/app";
import { createAuth } from "../src/auth";
import { ImvuClient, imvuImage, parseProduct, type ImvuUser } from "../src/imvu";
import { ImvuClient, imvuImage, parse, parseProduct, type ImvuUser } from "../src/imvu";
const webURL = "http://web.test";
const apiURL = "http://api.test";
@@ -77,7 +77,7 @@ async function person(name: string, imvu?: { id: number; ageVerified: boolean })
});
const p = { cookie: res.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ") };
if (imvu) {
accounts.set(imvu.id, { id: imvu.id, username: `${name}Av`, tagline: "", ageVerified: imvu.ageVerified, accessPass: false });
accounts.set(imvu.id, { id: imvu.id, username: `${name}Av`, tagline: "", ageVerified: imvu.ageVerified, accessPass: false, picture: null });
const started = await call(p, "POST", "/api/v1/account/links", { username: `${name}Av` });
accounts.get(imvu.id)!.tagline = started.json.challenge.code;
expect((await call(p, "POST", `/api/v1/account/links/${imvu.id}/check`)).status).toBe(200);
@@ -270,3 +270,11 @@ test("derived products carry their IMVU parent's image and categories", async ()
expect((await call(dresser, "PUT", `/api/v1/library/${coatID}`)).status).toBe(200);
expect((await call(dresser, "GET", "/api/v1/library")).json.library[0].product).toMatchObject({ image: coat.product_image });
});
test("an IMVU account's avatar picture is kept only from IMVU's own hosts", () => {
const user = (thumbnail_url: unknown) =>
parse({ denormalized: { "https://api.imvu.com/user/user-5": { data: { legacy_cid: 5, username: "Av", thumbnail_url } } } });
expect(user("https://webasset-akm.imvu.com/resized_image/p.png").picture).toBe("https://webasset-akm.imvu.com/resized_image/p.png");
expect(user("https://evil.example/p.png").picture).toBeNull();
expect(user(undefined).picture).toBeNull();
});
+1 -1
View File
@@ -88,7 +88,7 @@ async function person(name: string, imvu: { id: number; ageVerified?: boolean })
token: res.headers.get("set-auth-token") ?? "",
};
expect(p.token).not.toBe("");
accounts.set(imvu.id, { id: imvu.id, username: `${name}Av`, tagline: "", ageVerified: !!imvu.ageVerified, accessPass: false });
accounts.set(imvu.id, { id: imvu.id, username: `${name}Av`, tagline: "", ageVerified: !!imvu.ageVerified, accessPass: false, picture: null });
const started = await call(p, "POST", "/api/v1/account/links", { username: `${name}Av` });
accounts.get(imvu.id)!.tagline = started.json.challenge.code;
expect((await call(p, "POST", `/api/v1/account/links/${imvu.id}/check`)).status).toBe(200);