import { Context } from '@heyputer/backend/src/core'; import { extension } from '@heyputer/backend/src/extensions'; import { getTaskbarItems } from '@heyputer/backend/src/util/taskbarItems.js'; import type { Request, Response } from 'express'; import TimeAgo from 'javascript-time-ago'; import localeEn from 'javascript-time-ago/locale/en'; const stores = extension.import('store'); const services = extension.import('service'); const clients = extension.import('client'); const timeago = (() => { TimeAgo.addDefaultLocale(localeEn); return new TimeAgo('en-US'); })(); // Allowlist of `config.feature_flags` keys safe to surface via /whoami. // Anything not listed here stays server-side, so internal flags // (payment_bypass, staff_only_*, etc.) cannot leak by accident. Add a // flag here when, and only when, the client actually needs to read it. const CLIENT_VISIBLE_FEATURE_FLAGS: ReadonlySet = new Set([ 'create_shortcut', 'download_directory', 'prompt_user_when_navigation_away_from_puter', ]); export const handleWhoami = async ( req: Request, res: Response, ): Promise => { const actor = Context.get('actor'); if (!actor?.user?.id) { res.status(401).json({ error: 'Authentication required' }); return; } const isUser = !actor.app; const user = await stores.user.getById(actor.user.id); if (!user) { res.status(404).json({ error: 'User not found' }); return; } const oidcOnly = user.password === null; const ALLOWED_ICON_SIZES = new Set([16, 32, 64, 128, 256, 512]); const rawIconSize = typeof req.query?.icon_size === 'string' ? Number(req.query.icon_size) : undefined; const iconSize = rawIconSize !== undefined && ALLOWED_ICON_SIZES.has(rawIconSize) ? rawIconSize : undefined; const noIcons = !iconSize; // Feature flags come from `config.feature_flags`. We only forward keys // listed in CLIENT_VISIBLE_FEATURE_FLAGS so internal flags can't leak. // Non-boolean values (e.g. `"true"` as a string) are coerced so the // client never has to guess. const rawFlags = extension.config.feature_flags ?? {}; const feature_flags: Record = {}; for (const [k, v] of Object.entries(rawFlags)) { if (CLIENT_VISIBLE_FEATURE_FLAGS.has(k)) { feature_flags[k] = Boolean(v); } } const metadata = user.metadata ? { ...user.metadata } : user.metadata; if (metadata) delete metadata.tmp_password; const details: Record = { username: user.username, uuid: user.uuid, email: user.email, unconfirmed_email: user.email, email_confirmed: user.email_confirmed || user.username === 'admin', requires_email_confirmation: user.requires_email_confirmation, phone: user.phone, requires_phone_verification: user.requires_phone_verification, requires_card_verification: user.requires_card_verification, desktop_bg_url: user.desktop_bg_url, desktop_bg_color: user.desktop_bg_color, desktop_bg_fit: user.desktop_bg_fit, is_temp: user.password === null && user.email === null, is_user_token: true, oidc_only: oidcOnly, taskbar_items: isUser ? await getTaskbarItems( user, { clients, stores, services, apiBaseUrl: String(extension.config.api_base_url ?? ''), }, { iconSize, noIcons }, ) : undefined, otp: !!user.otp_enabled, feature_flags, human_readable_age: user.timestamp ? timeago.format(new Date(user.timestamp as string)) : null, metadata, hasDevAccountAccess: !!user.metadata?.hasDevAccountAccess, }; // OIDC revalidate URL for password-less accounts if (oidcOnly) { try { const provider = await services.oidc.getLinkedProviderForUser( user.id as number, ); if (provider) { const origin = (extension.config.origin ?? '').replace( /\/$/, '', ); details.oidc_revalidate_url = `${origin}/auth/oidc/${provider}/start?flow=revalidate&user_uuid=${encodeURIComponent(user.uuid)}`; } } catch { // OIDC not configured } } // Directories — only sent to user actors if (isUser) { const directories: Record = {}; const nameToProp: Record = { desktop_uuid: `/${user.username}/Desktop`, appdata_uuid: `/${user.username}/AppData`, documents_uuid: `/${user.username}/Documents`, pictures_uuid: `/${user.username}/Pictures`, videos_uuid: `/${user.username}/Videos`, trash_uuid: `/${user.username}/Trash`, }; for (const k in nameToProp) { directories[nameToProp[k]] = user[k]; } details.directories = directories; } // Last activity if (user.last_activity_ts) { try { details.last_activity_ts = Math.round( new Date(user.last_activity_ts as string).getTime() / 1000, ); } catch { /* ignore parse error */ } } // Strip sensitive fields for app actors if (!isUser) { const canReadEmail = await services.permission .check(actor, `user:${user.uuid}:email:read`) .catch(() => false); if (!canReadEmail) { delete details.email; delete details.unconfirmed_email; } delete details.desktop_bg_url; delete details.desktop_bg_color; delete details.desktop_bg_fit; delete details.human_readable_age; delete details.is_user_token; delete details.metadata; } if (actor.app) { details.app_name = actor.app.uid; } try { await clients.event.emitAndWait( 'whoami.details', { user, details, isUser }, {}, ); } catch { /* best-effort */ } const subscription = details.subscription as | { offering?: Record } | undefined; if (subscription?.offering) { delete subscription.offering.group; delete subscription.offering.benefits; delete subscription.offering.price_id; } res.json(details); }; extension.get( '/whoami', { subdomain: 'api', requireAuth: true, allowUnconfirmed: true }, handleWhoami, );