From 223b0a5669e6f2b5885a7ce35a841cd0be204771 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Mon, 2 Nov 2026 23:30:29 +0530 Subject: [PATCH] Brand the hosted login and app picker pages The login page is served by oauth-server-kit from the API host, but MediaLit passed only the app name, so it showed the kit's default brown button, a black M placeholder and no favicon. Pass the web app's icon as the logo and favicon and use the medialit product color (oklch(0.6 0.14 300)) for the login page. The app picker takes the same brand so the flow stays consistent. Co-Authored-By: Claude --- apps/api/src/app.ts | 11 +++++++++++ apps/api/src/auth/oauth-app-pages.ts | 29 ++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 951cbe8e..b975bf88 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -46,6 +46,13 @@ export function createApp(input: { }): express.Express { const { auth, webOrigin } = input; const loginCaptcha = loginCaptchaConfig(); + // The sign-in and app-picker pages are served from the API host, so the + // icon comes from the web app. The color matches the dashboard's medialit + // product token. + const brand = { + iconUrl: new URL("/icon.svg", webOrigin).toString(), + primaryColor: "oklch(0.6 0.14 300)", + }; const app = express(); app.set( @@ -83,6 +90,9 @@ export function createApp(input: { authBasePath: AUTH_BASE_PATH, allowedRedirectOrigins: [new URL(webOrigin).origin], defaultRedirectUrl: `${webOrigin}/`, + logoUrl: brand.iconUrl, + faviconUrl: brand.iconUrl, + primaryColor: brand.primaryColor, loginMethods: [{ type: "email-otp" }], ...(loginCaptcha ? { @@ -99,6 +109,7 @@ export function createApp(input: { auth: auth.auth, adapter: oauthAppSelectionAdapter, authBasePath: AUTH_BASE_PATH, + brand, }), ); diff --git a/apps/api/src/auth/oauth-app-pages.ts b/apps/api/src/auth/oauth-app-pages.ts index fe196e52..1ada6054 100644 --- a/apps/api/src/auth/oauth-app-pages.ts +++ b/apps/api/src/auth/oauth-app-pages.ts @@ -28,15 +28,23 @@ function escapeHtml(value: string): string { .replace(/'/g, "'"); } +export interface OAuthAppPagesBrand { + iconUrl: string; + primaryColor: string; +} + // Mirrors the hosted login and consent pages from oauth-server-kit. -const STYLES = `html{--brand-primary:#8c7a6b;color-scheme:light}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f4f4f5;color:#171717;font-family:system-ui,sans-serif} -.card{width:100%;max-width:400px;padding:32px;border:1px solid #e4e4e7;border-radius:14px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.04)}.logo{width:38px;height:38px;margin:0 auto 20px;display:grid;place-items:center;border-radius:50%;background:#171717;color:#fff;font-weight:700} +function styles(primaryColor: string): string { + return `html{--brand-primary:${primaryColor};color-scheme:light}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f4f4f5;color:#171717;font-family:system-ui,sans-serif} +.card{width:100%;max-width:400px;padding:32px;border:1px solid #e4e4e7;border-radius:14px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.04)}.logo{width:38px;height:38px;margin:0 auto 20px;display:grid;place-items:center;border-radius:50%;background:#171717;color:#fff;font-weight:700}.logo.has-image{background:none}.logo img{width:38px;height:38px} h1{margin:0 0 24px;text-align:center;font-size:22px;font-weight:650}.sub{margin:-12px 0 24px;text-align:center;color:#71717a;font-size:13px;line-height:1.5}.error{display:none;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#fef2f2;color:#b42318;font-size:13px} form{margin:0}.apps{display:grid;gap:8px;margin-bottom:16px}.app{display:flex;align-items:center;gap:10px;padding:12px 14px;border:1px solid #e4e4e7;border-radius:9px;background:#fafafa;cursor:pointer;font-size:14px}.app:has(input:checked){border-color:var(--brand-primary);background:#fff}.app input{margin:0;accent-color:var(--brand-primary)}.app-name{flex:1;font-weight:550;word-break:break-word}.badge{padding:2px 6px;border:1px solid #e4e4e7;border-radius:6px;color:#52525b;font-size:11px} button{width:100%;margin-top:8px;padding:11px;border:1px solid var(--brand-primary);border-radius:8px;background:var(--brand-primary);color:#fff;font-size:14px;font-weight:550;cursor:pointer}button:focus-visible{outline:2px solid var(--brand-primary);outline-offset:2px}button:disabled{opacity:.55;cursor:not-allowed}`; +} -function page(body: string): string { - return `Choose a MediaLit app
${body}
`; +function page(body: string, brand: OAuthAppPagesBrand): string { + const icon = escapeHtml(brand.iconUrl); + return `Choose a MediaLit app
${body}
`; } /** @@ -54,7 +62,11 @@ function continueScript(authBasePath: string): string { return `async function continueAuthorization(){var response=await fetch(${scriptString(`${authBasePath}/oauth2/continue`)},{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({postLogin:true,oauth_query:location.search.slice(1)})});var data={};try{data=await response.json()}catch{}var target=data.url||data.redirect_uri;if(!response.ok||!target)throw new Error();location.assign(target)}`; } -function pickerPage(apps: OAuthApp[], authBasePath: string): string { +function pickerPage( + apps: OAuthApp[], + authBasePath: string, + brand: OAuthAppPagesBrand, +): string { const defaultApp = apps.find((app) => app.isDefault) ?? apps[0]; const choices = apps .map( @@ -64,6 +76,7 @@ function pickerPage(apps: OAuthApp[], authBasePath: string): string { .join(""); return page( `

Choose an app

The connected client will upload, list and delete files in this app.

${choices}
`, + brand, ); } @@ -84,6 +97,7 @@ export function createOAuthAppPages(options: { auth: SessionApi; adapter: OAuthAppSelectionAdapter; authBasePath: string; + brand: OAuthAppPagesBrand; }): Router { const router = Router(); @@ -113,11 +127,14 @@ export function createOAuthAppPages(options: { .send( page( `

Continuing…

`, + options.brand, ), ); return; } - response.type("html").send(pickerPage(apps, options.authBasePath)); + response + .type("html") + .send(pickerPage(apps, options.authBasePath, options.brand)); }), );