99 lines
2.6 KiB
TypeScript
99 lines
2.6 KiB
TypeScript
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"
|
|
*/
|
|
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 or *.local, use the full hostname with leading dot
|
|
if (hostname.endsWith('.localhost') || hostname.endsWith('.local')) {
|
|
return '.localhost';
|
|
}
|
|
// 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;
|
|
}
|
|
}
|