feat: add prelaunch site controls and refresh branding
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 19s
Validate and deploy production / deploy (push) Successful in 1m0s
Validate and deploy staging / deploy (push) Successful in 36s

This commit is contained in:
JB
2026-07-27 02:24:13 -04:00
parent 7b3aff30d8
commit 5a67b252aa
37 changed files with 1738 additions and 100 deletions
+197
View File
@@ -0,0 +1,197 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const databaseRow = vi.hoisted(() => ({ value: undefined as Record<string, unknown> | undefined }));
vi.mock('$lib/server/database', () => ({
getDatabase: vi.fn(async () => ({
prepare: vi.fn(async () => ({
get: vi.fn(async () => databaseRow.value)
}))
}))
}));
import {
getSiteAccessBlock,
getSiteAccessSettings,
invalidateSiteAccessSettings,
isLaunchGateExempt,
siteSectionForPath,
type SiteAccessSettings,
type SiteSectionKey
} from './site-access';
function accessSettings(
overrides: Partial<Pick<SiteAccessSettings, 'comingSoonEnabled' | 'accountCreationEnabled'>> & {
sections?: Partial<SiteAccessSettings['sections']>;
} = {}
): SiteAccessSettings {
return {
comingSoonEnabled: overrides.comingSoonEnabled ?? false,
accountCreationEnabled: overrides.accountCreationEnabled ?? true,
sections: {
news: true,
database: true,
builds: true,
guides: true,
builder: true,
...overrides.sections
},
updatedAt: '2026-07-27T00:00:00.000Z'
};
}
describe('siteSectionForPath', () => {
it.each([
['/news', 'news'],
['/news/latest', 'news'],
['/database', 'database'],
['/database/weapons/first-wave-vector', 'database'],
['/builds', 'builds'],
['/builds/shared-loadout', 'builds'],
['/guides', 'guides'],
['/guides/combat-medic', 'guides'],
['/builder', 'builder'],
['/builder/loadout-id', 'builder']
])('maps %s to the %s launch section', (pathname, section) => {
expect(siteSectionForPath(pathname)).toBe(section);
});
it.each(['/', '/newsroom', '/database-tools', '/build', '/guide', '/builder-tools', '/isac'])(
'does not treat %s as a gated section',
(pathname) => {
expect(siteSectionForPath(pathname)).toBeNull();
}
);
});
describe('isLaunchGateExempt', () => {
it.each([
'/coming-soon',
'/sign-in',
'/terms',
'/dmca',
'/contact',
'/auth/discord',
'/auth/discord/callback',
'/api/health/database',
'/api/internal/notifications',
'/_app/immutable/entry/start.js',
'/favicon.svg'
])('keeps %s reachable while the launch gate is active', (pathname) => {
expect(isLaunchGateExempt(pathname)).toBe(true);
});
it.each([
'/',
'/news',
'/isac',
'/coming-soon/preview',
'/sign-in/help',
'/auth',
'/api/health',
'/api/internal',
'/favicon.png'
])('does not exempt %s', (pathname) => {
expect(isLaunchGateExempt(pathname)).toBe(false);
});
});
describe('getSiteAccessBlock', () => {
it('gives the global launch gate priority over a disabled section for anonymous visitors', () => {
const settings = accessSettings({
comingSoonEnabled: true,
sections: { builder: false }
});
expect(getSiteAccessBlock(settings, '/builder', false)).toEqual({ kind: 'launch' });
});
it('lets authenticated users bypass the global launch gate when the requested section is enabled', () => {
const settings = accessSettings({ comingSoonEnabled: true });
expect(getSiteAccessBlock(settings, '/builder', true)).toBeNull();
});
it.each([
['/news', 'news'],
['/database/weapons', 'database'],
['/builds/agent-loadout', 'builds'],
['/guides/combat-medic', 'guides'],
['/builder', 'builder']
] satisfies Array<[string, SiteSectionKey]>)(
'blocks %s for authenticated and anonymous visitors when the %s section is disabled',
(pathname, section) => {
const settings = accessSettings({
sections: { [section]: false }
});
expect(getSiteAccessBlock(settings, pathname, false)).toEqual({ kind: 'section', section });
expect(getSiteAccessBlock(settings, pathname, true)).toEqual({ kind: 'section', section });
}
);
it.each(['/coming-soon', '/sign-in', '/terms', '/dmca', '/contact', '/auth/discord/callback'])(
'keeps exempt route %s available while the global gate is active',
(pathname) => {
const settings = accessSettings({ comingSoonEnabled: true });
expect(getSiteAccessBlock(settings, pathname, false)).toBeNull();
expect(getSiteAccessBlock(settings, pathname, true)).toBeNull();
}
);
it('allows an enabled section when the global launch gate is off', () => {
const settings = accessSettings();
expect(getSiteAccessBlock(settings, '/news', false)).toBeNull();
expect(getSiteAccessBlock(settings, '/news', true)).toBeNull();
});
});
describe('getSiteAccessSettings', () => {
beforeEach(() => {
databaseRow.value = undefined;
invalidateSiteAccessSettings();
});
it('fails safely to the launch page while leaving registration and sections staged on a new database', async () => {
await expect(getSiteAccessSettings()).resolves.toEqual({
comingSoonEnabled: true,
accountCreationEnabled: true,
sections: {
news: true,
database: true,
builds: true,
guides: true,
builder: true
},
updatedAt: '2026-07-27T00:00:00.000Z'
});
});
it('normalizes persisted integer flags into the public settings shape', async () => {
databaseRow.value = {
coming_soon_enabled: 0,
account_creation_enabled: 0,
news_enabled: 1,
database_enabled: 0,
builds_enabled: 1,
guides_enabled: 0,
builder_enabled: 1,
updated_at: '2026-08-01T12:30:00.000Z'
};
await expect(getSiteAccessSettings()).resolves.toEqual({
comingSoonEnabled: false,
accountCreationEnabled: false,
sections: {
news: true,
database: false,
builds: true,
guides: false,
builder: true
},
updatedAt: '2026-08-01T12:30:00.000Z'
});
});
});
+85
View File
@@ -0,0 +1,85 @@
import { getDatabase } from '$lib/server/database';
export const SITE_SECTION_KEYS = ['news', 'database', 'builds', 'guides', 'builder'] as const;
export type SiteSectionKey = (typeof SITE_SECTION_KEYS)[number];
export type SiteAccessSettings = {
comingSoonEnabled: boolean;
accountCreationEnabled: boolean;
sections: Record<SiteSectionKey, boolean>;
updatedAt: string;
};
export type SiteAccessBlock =
| { kind: 'launch' }
| { kind: 'section'; section: SiteSectionKey };
const DEFAULT_SETTINGS: SiteAccessSettings = {
comingSoonEnabled: true,
accountCreationEnabled: true,
sections: { news: true, database: true, builds: true, guides: true, builder: true },
updatedAt: '2026-07-27T00:00:00.000Z'
};
let cached: { expiresAt: number; value: SiteAccessSettings } | null = null;
function normalized(row: Record<string, unknown> | undefined): SiteAccessSettings {
if (!row) return structuredClone(DEFAULT_SETTINGS);
return {
comingSoonEnabled: Boolean(row.coming_soon_enabled),
accountCreationEnabled: Boolean(row.account_creation_enabled),
sections: {
news: Boolean(row.news_enabled),
database: Boolean(row.database_enabled),
builds: Boolean(row.builds_enabled),
guides: Boolean(row.guides_enabled),
builder: Boolean(row.builder_enabled)
},
updatedAt: String(row.updated_at ?? DEFAULT_SETTINGS.updatedAt)
};
}
export function invalidateSiteAccessSettings() {
cached = null;
}
export async function getSiteAccessSettings() {
if (cached && cached.expiresAt > Date.now()) return cached.value;
const database = await getDatabase();
const row = await (await database.prepare("SELECT * FROM site_launch_settings WHERE id='primary'")).get();
const value = normalized(row);
cached = { value, expiresAt: Date.now() + 5_000 };
return value;
}
export function siteSectionForPath(pathname: string): SiteSectionKey | null {
for (const section of SITE_SECTION_KEYS) {
if (pathname === `/${section}` || pathname.startsWith(`/${section}/`)) return section;
}
return null;
}
export function isLaunchGateExempt(pathname: string) {
return pathname === '/coming-soon' ||
pathname === '/sign-in' ||
pathname === '/terms' ||
pathname === '/dmca' ||
pathname === '/contact' ||
pathname.startsWith('/auth/') ||
pathname.startsWith('/api/health/') ||
pathname.startsWith('/api/internal/') ||
pathname.startsWith('/_app/') ||
pathname === '/favicon.svg';
}
export function getSiteAccessBlock(
settings: SiteAccessSettings,
pathname: string,
authenticated: boolean
): SiteAccessBlock | null {
if (isLaunchGateExempt(pathname)) return null;
if (!authenticated && settings.comingSoonEnabled) return { kind: 'launch' };
const section = siteSectionForPath(pathname);
if (section && !settings.sections[section]) return { kind: 'section', section };
return null;
}