Update fun module configuration and enhance CSS styles

- Added 'enabled' property to FunConfigSchema with a default value of true for better configuration management.
- Refactored getFunConfig to return a complete default configuration when no data is found.
- Improved globals.css by expanding CSS variables for light and dark themes, enhancing styling consistency across the application.
This commit is contained in:
smueller
2026-07-22 13:56:33 +02:00
parent 7bc1684566
commit 04e04eb11d
39 changed files with 2394 additions and 14 deletions

View File

@@ -0,0 +1,105 @@
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
import { cookies } from 'next/headers';
import { z } from 'zod';
import { SessionUserSchema } from '@nexumi/shared';
import { env } from './env';
import { redis } from './redis';
export const SESSION_COOKIE_NAME = 'nexumi_session';
const SESSION_KEY_PREFIX = 'webui:session:';
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
const SessionPayloadSchema = z.object({
user: SessionUserSchema,
accessToken: z.string().min(1),
tokenExpiresAt: z.number().optional()
});
export type SessionPayload = z.infer<typeof SessionPayloadSchema>;
export class SessionRequiredError extends Error {
constructor() {
super('Session required');
this.name = 'SessionRequiredError';
}
}
function sign(value: string): string {
return createHmac('sha256', env.SESSION_SECRET).update(value).digest('hex');
}
function buildCookieValue(sessionId: string): string {
return `${sessionId}.${sign(sessionId)}`;
}
function parseCookieValue(cookieValue: string): string | null {
const separatorIndex = cookieValue.lastIndexOf('.');
if (separatorIndex <= 0) {
return null;
}
const sessionId = cookieValue.slice(0, separatorIndex);
const signature = cookieValue.slice(separatorIndex + 1);
const expected = sign(sessionId);
const provided = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
if (provided.length !== expectedBuffer.length || !timingSafeEqual(provided, expectedBuffer)) {
return null;
}
return sessionId;
}
function sessionRedisKey(sessionId: string): string {
return `${SESSION_KEY_PREFIX}${sessionId}`;
}
export async function createSession(payload: SessionPayload): Promise<void> {
const sessionId = randomBytes(32).toString('hex');
await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS);
const cookieStore = await cookies();
cookieStore.set(SESSION_COOKIE_NAME, buildCookieValue(sessionId), {
httpOnly: true,
sameSite: 'lax',
secure: env.NODE_ENV === 'production',
path: '/',
maxAge: SESSION_TTL_SECONDS
});
}
export async function getSession(): Promise<SessionPayload | null> {
const cookieStore = await cookies();
const raw = cookieStore.get(SESSION_COOKIE_NAME)?.value;
if (!raw) {
return null;
}
const sessionId = parseCookieValue(raw);
if (!sessionId) {
return null;
}
const stored = await redis.get(sessionRedisKey(sessionId));
if (!stored) {
return null;
}
const parsed = SessionPayloadSchema.safeParse(JSON.parse(stored));
return parsed.success ? parsed.data : null;
}
export async function destroySession(): Promise<void> {
const cookieStore = await cookies();
const raw = cookieStore.get(SESSION_COOKIE_NAME)?.value;
if (raw) {
const sessionId = parseCookieValue(raw);
if (sessionId) {
await redis.del(sessionRedisKey(sessionId));
}
}
cookieStore.delete(SESSION_COOKIE_NAME);
}
export async function requireSession(): Promise<SessionPayload> {
const session = await getSession();
if (!session) {
throw new SessionRequiredError();
}
return session;
}