import dayjs from 'dayjs'; import {API_URI, NODE_ENV} from '../app/constants.js'; import {prisma} from '../database/prisma.js'; import {wrapRedis} from '../database/redis.js'; import {Keys} from './keys.js'; /** * Extract base domain from URL for cookie sharing across subdomains * e.g., "http://api.example.com" -> ".example.com" * e.g., "http://api.localhost" -> ".localhost" * e.g., "http://app.plunk.local" -> ".plunk.local" */ function getCookieDomain(): string | undefined { if (NODE_ENV === 'development') { return undefined; } try { const url = new URL(API_URI); const hostname = url.hostname; // For localhost or IP addresses, don't set a domain if (hostname === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { return undefined; } // Extract base domain (last two parts for most domains, or .localhost) const parts = hostname.split('.'); if (parts.length >= 2) { // For *.localhost, use .localhost (reserved TLD) if (hostname.endsWith('.localhost')) { return '.localhost'; } // For *.local (mDNS TLD), use the actual base domain if (hostname.endsWith('.local')) { return `.${parts.slice(-2).join('.')}`; } // For other domains, use the last two parts (e.g., .example.com) return `.${parts.slice(-2).join('.')}`; } return undefined; } catch { return undefined; } } export class UserService { public static readonly COOKIE_NAME = 'next_token'; public static async id(id: string) { return wrapRedis(Keys.User.id(id), async () => { return prisma.user.findUnique({where: {id}}); }); } public static async email(email: string) { if (!email) { return null; } return wrapRedis(Keys.User.email(email), async () => { return prisma.user.findFirst({ where: { email: { equals: email, mode: 'insensitive', }, }, }); }); } public static async projects(userId: string) { const memberships = await prisma.user.findUnique({where: {id: userId}}).memberships({ include: { project: true, }, }); return memberships ? memberships.map(({project}) => project) : []; } /** * Generates cookie options * @param expires An optional expiry for this cookie (useful for a logout) */ public static cookieOptions(expires?: Date) { // Check if using HTTPS from API_URI const isHttps = NODE_ENV === 'development' ? false : API_URI.startsWith('https://'); return { httpOnly: true, expires: expires ?? dayjs().add(7, 'days').toDate(), secure: isHttps, sameSite: isHttps ? 'none' : 'lax', path: '/', domain: getCookieDomain(), } as const; } }