Move website directory

This commit is contained in:
Aaron Po
2026-04-27 16:00:11 -04:00
parent 189bce040b
commit 5a21589029
58 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import { createCookieSessionStorage, redirect } from 'react-router';
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
export interface AuthTokens {
accessToken: string;
refreshToken: string;
userAccountId: string;
username: string;
}
interface ApiResponse<T> {
message: string;
payload: T;
}
interface LoginPayload {
userAccountId: string;
username: string;
refreshToken: string;
accessToken: string;
}
interface RegistrationPayload extends LoginPayload {
confirmationEmailSent: boolean;
}
const sessionStorage = createCookieSessionStorage({
cookie: {
name: '__session',
httpOnly: true,
maxAge: 60 * 60 * 24 * 21, // 21 days (matches refresh token)
path: '/',
sameSite: 'lax',
secrets: [process.env.SESSION_SECRET || 'dev-secret-change-me'],
secure: process.env.NODE_ENV === 'production',
},
});
export async function getSession(request: Request) {
return sessionStorage.getSession(request.headers.get('Cookie'));
}
export async function commitSession(session: Awaited<ReturnType<typeof getSession>>) {
return sessionStorage.commitSession(session);
}
export async function destroySession(session: Awaited<ReturnType<typeof getSession>>) {
return sessionStorage.destroySession(session);
}
export async function requireAuth(request: Request): Promise<AuthTokens> {
const session = await getSession(request);
const accessToken = session.get('accessToken');
const refreshToken = session.get('refreshToken');
if (!accessToken || !refreshToken) {
throw redirect('/login');
}
return {
accessToken,
refreshToken,
userAccountId: session.get('userAccountId'),
username: session.get('username'),
};
}
export async function getOptionalAuth(request: Request): Promise<AuthTokens | null> {
const session = await getSession(request);
const accessToken = session.get('accessToken');
if (!accessToken) return null;
return {
accessToken,
refreshToken: session.get('refreshToken'),
userAccountId: session.get('userAccountId'),
username: session.get('username'),
};
}
export async function login(username: string, password: string) {
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Login failed (${res.status})`);
}
const data: ApiResponse<LoginPayload> = await res.json();
return data.payload;
}
export async function register(body: {
username: string;
firstName: string;
lastName: string;
email: string;
dateOfBirth: string;
password: string;
}) {
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Registration failed (${res.status})`);
}
const data: ApiResponse<RegistrationPayload> = await res.json();
return data.payload;
}
export async function refreshTokens(refreshToken: string) {
const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!res.ok) {
throw new Error('Token refresh failed');
}
const data: ApiResponse<LoginPayload> = await res.json();
return data.payload;
}
export async function confirmEmail(token: string, accessToken: string) {
const res = await fetch(`${API_BASE_URL}/api/auth/confirm?token=${encodeURIComponent(token)}`, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Confirmation failed (${res.status})`);
}
const data: ApiResponse<{ userAccountId: string; confirmedDate: string }> = await res.json();
return data.payload;
}
export async function createAuthSession(payload: LoginPayload, redirectTo: string) {
const session = await sessionStorage.getSession();
session.set('accessToken', payload.accessToken);
session.set('refreshToken', payload.refreshToken);
session.set('userAccountId', payload.userAccountId);
session.set('username', payload.username);
return redirect(redirectTo, {
headers: { 'Set-Cookie': await commitSession(session) },
});
}

View File

@@ -0,0 +1,33 @@
import { z } from 'zod';
export const loginSchema = z.object({
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
});
export type LoginSchema = z.infer<typeof loginSchema>;
export const registerSchema = z
.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be at most 20 characters'),
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email address'),
dateOfBirth: z.string().min(1, 'Date of birth is required'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
.regex(/[a-z]/, 'Password must contain a lowercase letter')
.regex(/[0-9]/, 'Password must contain a number'),
confirmPassword: z.string().min(1, 'Please confirm your password'),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
});
export type RegisterSchema = z.infer<typeof registerSchema>;

View File

@@ -0,0 +1,41 @@
export type ThemeName =
| 'biergarten-lager'
| 'biergarten-stout'
| 'biergarten-cassis'
| 'biergarten-weizen';
export interface ThemeOption {
value: ThemeName;
label: string;
vibe: string;
}
export const defaultThemeName: ThemeName = 'biergarten-lager';
export const themeStorageKey = 'biergarten-theme';
export const biergartenThemes: ThemeOption[] = [
{
value: 'biergarten-lager',
label: 'Biergarten Lager',
vibe: 'Muted parchment, mellow amber, daytime beer garden',
},
{
value: 'biergarten-stout',
label: 'Biergarten Stout',
vibe: 'Charred barrel, deep roast, cozy evening cellar',
},
{
value: 'biergarten-cassis',
label: 'Biergarten Cassis',
vibe: 'Blackberry barrel, sour berry dark, vivid night market',
},
{
value: 'biergarten-weizen',
label: 'Biergarten Weizen',
vibe: 'Ultra-light young barley, green undertone, bright spring afternoon',
},
];
export function isBiergartenTheme(value: string | null | undefined): value is ThemeName {
return biergartenThemes.some((theme) => theme.value === value);
}