diff --git a/apps/dashboard/src/lib/remote/admin-projects.remote.ts b/apps/dashboard/src/lib/remote/admin-projects.remote.ts index 323eb97..3bb3f2b 100644 --- a/apps/dashboard/src/lib/remote/admin-projects.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-projects.remote.ts @@ -29,6 +29,10 @@ import { import { sendRenderedEmail } from '$lib/server/email'; import { softDeleteOrganizationResources } from '$lib/server/project-deletion'; import { provisionVm } from '$lib/server/vm-provisioning'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureAdminProjects +} from '$lib/server/accessibility-fixtures'; export type AdminProjectBillingStatus = 'configured' | 'past_due' | 'suspended' | 'none'; @@ -69,6 +73,7 @@ function makeCountMap(rows: { key: string | null; count: number }[]) { } export const listAdminProjects = query(async (): Promise => { + if (accessibilityFixtureEnabled) return accessibilityFixtureAdminProjects; const db = await requireCurrentAdmin(); const [orgs, owners, memberCounts, vmCounts, volumeCounts, billingCustomers] = await Promise.all([ diff --git a/apps/dashboard/src/lib/remote/admin-users.remote.ts b/apps/dashboard/src/lib/remote/admin-users.remote.ts index 2cbd351..0317f51 100644 --- a/apps/dashboard/src/lib/remote/admin-users.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-users.remote.ts @@ -25,6 +25,10 @@ import { import { updateProjectCustomer } from '$lib/server/billing/autumn'; import { sendRenderedEmail } from '$lib/server/email'; import { softDeleteOrganizationResources } from '$lib/server/project-deletion'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureAdminUsers +} from '$lib/server/accessibility-fixtures'; export type UserSession = { id: string; @@ -156,6 +160,7 @@ function makeCountMap(rows: { userId: string | null; count: number }[]) { } export const listAdminUsers = query(async (): Promise => { + if (accessibilityFixtureEnabled) return accessibilityFixtureAdminUsers; const { db } = await requireCurrentAdmin(); const users = await db diff --git a/apps/dashboard/src/lib/remote/admin-vms.remote.ts b/apps/dashboard/src/lib/remote/admin-vms.remote.ts index c623741..10ebaa0 100644 --- a/apps/dashboard/src/lib/remote/admin-vms.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-vms.remote.ts @@ -7,6 +7,10 @@ import { initDrizzle } from '$lib/server/db'; import { member, organization, user, vms, vmTypes } from '$lib/server/db/schema'; import { getBackend, type VmInfo } from '$lib/server/backends'; import { queueVmDeletion } from '$lib/server/vm-deletion'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureAdminVms +} from '$lib/server/accessibility-fixtures'; export type AdminVm = { id: string; @@ -50,6 +54,7 @@ async function requireCurrentAdmin() { } export const listAllAdminVms = query(async (): Promise => { + if (accessibilityFixtureEnabled) return accessibilityFixtureAdminVms; const db = await requireCurrentAdmin(); const [rows, owners] = await Promise.all([ diff --git a/apps/dashboard/src/lib/remote/billing.remote.ts b/apps/dashboard/src/lib/remote/billing.remote.ts index e597f0c..2c8cf4f 100644 --- a/apps/dashboard/src/lib/remote/billing.remote.ts +++ b/apps/dashboard/src/lib/remote/billing.remote.ts @@ -10,6 +10,10 @@ import { import { getProjectBillingOverview, refreshProjectBilling } from '$lib/server/billing/overview'; import { runInBackground } from '$lib/server/background'; import { initDrizzle } from '$lib/server/db'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureBillingOverview +} from '$lib/server/accessibility-fixtures'; const projectParams = type({ projectId: 'string' }); const setupParams = type({ projectId: 'string', returnTo: 'string?', discountCode: 'string?' }); @@ -24,6 +28,8 @@ export const getProjectBilling = query(projectParams, async (params) => { const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) return accessibilityFixtureBillingOverview; + const db = initDrizzle(); await requireProjectAccess(db, event.locals.user.id, params.projectId, 'admin'); runInBackground(refreshProjectBilling(params.projectId), 'refreshProjectBilling'); diff --git a/apps/dashboard/src/lib/remote/images.remote.ts b/apps/dashboard/src/lib/remote/images.remote.ts index ac4bf2e..8cc37d0 100644 --- a/apps/dashboard/src/lib/remote/images.remote.ts +++ b/apps/dashboard/src/lib/remote/images.remote.ts @@ -6,6 +6,10 @@ import { initDrizzle } from '$lib/server/db'; import { baseImages } from '$lib/server/db/schema'; import { getBackend } from '$lib/server/backends'; import { requireAdmin } from '$lib/server/auth-context'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureImages +} from '$lib/server/accessibility-fixtures'; type ImageRow = { id: string; @@ -53,6 +57,8 @@ export const listImages = query(async () => { const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) return accessibilityFixtureImages; + const db = initDrizzle(); const rows = await db.query.baseImages.findMany({ orderBy: [asc(baseImages.sortOrder), asc(baseImages.id)] diff --git a/apps/dashboard/src/lib/remote/ipam.remote.ts b/apps/dashboard/src/lib/remote/ipam.remote.ts index 56341ec..811c7f6 100644 --- a/apps/dashboard/src/lib/remote/ipam.remote.ts +++ b/apps/dashboard/src/lib/remote/ipam.remote.ts @@ -6,6 +6,10 @@ import { initDrizzle } from '$lib/server/db'; import { ipamAllocations, ipamPrefixes } from '$lib/server/db/schema'; import { requireAdmin } from '$lib/server/auth-context'; import { listIpamPrefixesWithStats, normalizeIpamPrefixInput } from '$lib/server/ipam'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureIpamPrefixes +} from '$lib/server/accessibility-fixtures'; async function requireCurrentAdmin() { const event = getRequestEvent(); @@ -18,6 +22,7 @@ async function requireCurrentAdmin() { } export const listIpamPrefixes = query(async () => { + if (accessibilityFixtureEnabled) return accessibilityFixtureIpamPrefixes; const db = await requireCurrentAdmin(); return listIpamPrefixesWithStats(db); }); diff --git a/apps/dashboard/src/lib/remote/vm-types.remote.ts b/apps/dashboard/src/lib/remote/vm-types.remote.ts index e9f727d..c5f15f5 100644 --- a/apps/dashboard/src/lib/remote/vm-types.remote.ts +++ b/apps/dashboard/src/lib/remote/vm-types.remote.ts @@ -5,6 +5,10 @@ import { asc, desc, eq } from 'drizzle-orm'; import { initDrizzle } from '$lib/server/db'; import { vmTypes } from '$lib/server/db/schema'; import { requireAdmin } from '$lib/server/auth-context'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureVmTypes +} from '$lib/server/accessibility-fixtures'; type VmTypeRow = { id: string; @@ -22,6 +26,8 @@ export const listVmTypes = query(async () => { const event = getRequestEvent(); if (!event?.locals.user) error(401, 'Authentication required'); + if (accessibilityFixtureEnabled) return accessibilityFixtureVmTypes; + const db = initDrizzle(); const rows = await db.query.vmTypes.findMany({ orderBy: [asc(vmTypes.sortOrder), asc(vmTypes.id)] diff --git a/apps/dashboard/src/lib/server/accessibility-fixtures.ts b/apps/dashboard/src/lib/server/accessibility-fixtures.ts index c7a6f1c..31ed433 100644 --- a/apps/dashboard/src/lib/server/accessibility-fixtures.ts +++ b/apps/dashboard/src/lib/server/accessibility-fixtures.ts @@ -116,3 +116,149 @@ export const accessibilityFixtureServers = [ } } ]; + +export const accessibilityFixtureBillingOverview = { + customer: null, + status: 'active' as const, + statusLabel: 'Ready', + planLabel: 'Project billing', + setupRequired: false, + syncError: null, + lastUpdatedAt: now.getTime(), + activeResourceCount: 1, + activeResources: [ + { + id: 'starter', + label: 'Starter', + resourceType: 'vm' as const, + count: 1, + hours: 720, + cost: 36.0 + } + ] +}; + +export const accessibilityFixtureAdminUsers = [ + { + id: accessibilityFixtureUser.id, + name: accessibilityFixtureUser.name, + email: accessibilityFixtureUser.email, + image: accessibilityFixtureUser.image, + emailVerified: accessibilityFixtureUser.emailVerified, + role: accessibilityFixtureUser.role, + isAdmin: accessibilityFixtureUser.isAdmin, + disabled: false, + billingExempt: false, + twoFactorEnabled: false, + createdAt: now, + updatedAt: now, + sessionCount: 1, + accountCount: 1, + orgCount: 1, + sshKeyCount: 0, + apiTokenCount: 0, + passkeyCount: 0 + } +]; + +export const accessibilityFixtureAdminProjects = [ + { + id: accessibilityFixtureProject.id, + name: accessibilityFixtureProject.projectName, + slug: 'accessibility-project', + createdAt: now.getTime(), + ownerId: accessibilityFixtureUser.id, + ownerName: accessibilityFixtureUser.name, + ownerEmail: accessibilityFixtureUser.email, + ownerBillingExempt: false, + memberCount: 2, + vmCount: 1, + volumeCount: 0, + billingStatus: 'configured' as const, + billingExempt: false, + disabled: false + } +]; + +export const accessibilityFixtureAdminVms = [ + { + id: accessibilityFixtureServers[0].id, + name: accessibilityFixtureServers[0].name, + proxmoxId: accessibilityFixtureServers[0].proxmoxId, + proxmoxNode: accessibilityFixtureServers[0].live.proxmoxNode, + active: accessibilityFixtureServers[0].active, + status: accessibilityFixtureServers[0].status, + statusError: null, + liveStatus: accessibilityFixtureServers[0].live.status, + uptime: accessibilityFixtureServers[0].live.uptime, + cpuUsage: accessibilityFixtureServers[0].live.metrics.cpu, + memoryUsageBytes: accessibilityFixtureServers[0].live.memory, + memoryTotalBytes: accessibilityFixtureServers[0].live.memory, + createdAt: now.getTime(), + deletedAt: null, + lastKnownAt: now.getTime(), + lastKnownIpv4: accessibilityFixtureServers[0].live.networkInterfaces.eth0.ipAddresses[0], + lastKnownIpv6: accessibilityFixtureServers[0].live.networkInterfaces.eth0.ipAddresses[1], + projectId: accessibilityFixtureProject.id, + projectName: accessibilityFixtureProject.projectName, + projectBillingExempt: false, + ownerName: accessibilityFixtureUser.name, + ownerEmail: accessibilityFixtureUser.email, + ownerBillingExempt: false, + vmTypeName: accessibilityFixtureServers[0].vmType.name, + vmTypeCores: accessibilityFixtureServers[0].vmType.cores, + vmTypeRamCapacity: accessibilityFixtureServers[0].vmType.ramCapacity, + vmTypeStorageAmount: accessibilityFixtureServers[0].vmType.storageAmount, + vmTypeRate: '0.05' + } +]; + +export const accessibilityFixtureIpamPrefixes = [ + { + id: 'accessibility-prefix-v4', + name: 'Test IPv4', + cidr: '192.0.2.0/24', + family: 'ipv4' as const, + disabled: false, + ipv6UseTransitAddress: false, + whitelistStart: null, + whitelistEnd: null, + gatewayAddress: '192.0.2.1', + allocated: 2, + capacity: '254', + available: '252', + hasCapacity: true + } +]; + +export const accessibilityFixtureVmTypes = [ + { + id: 'starter', + name: 'Starter', + isa: 'x86', + cores: 2, + ramCapacity: 4, + storageAmount: 50, + rate: '0.05', + cap: '50.00', + autumnFeatureId: 'starter' + } +]; + +export const accessibilityFixtureImages = [ + { + id: 'ubuntu-2404', + name: 'Ubuntu', + version: '24.04 LTS', + description: 'Ubuntu 24.04 LTS', + icon: 'ubuntu', + color: 'bg-orange-500', + isOfficial: true, + logoSvg: null, + accentColor: '#E95420', + imageType: 'qcow2', + secureBoot: false, + filePath: 'local:iso/ubuntu-24.04.qcow2', + isa: 'x86' + } +]; diff --git a/apps/dashboard/src/lib/server/auth-context.ts b/apps/dashboard/src/lib/server/auth-context.ts index 68e2fa1..0c8bb07 100644 --- a/apps/dashboard/src/lib/server/auth-context.ts +++ b/apps/dashboard/src/lib/server/auth-context.ts @@ -3,6 +3,11 @@ import { and, eq } from 'drizzle-orm'; import { getRequestEvent } from '$app/server'; import { hasProjectRole, type PermissionLevel } from '$lib/auth/organization-permissions'; import { member, organization, user } from '$lib/server/db/schema'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureProject, + accessibilityFixtureUser +} from '$lib/server/accessibility-fixtures'; export function hasAdminRole(role: string | null | undefined): boolean { return role?.split(',').includes('admin') ?? false; @@ -45,6 +50,8 @@ function loadProjectAccess(db: any, userId: string, projectId: string): Promise< } export async function requireAdmin(db: any, userId: string): Promise { + if (accessibilityFixtureEnabled && userId === accessibilityFixtureUser.id) return; + const isAdmin = await cachedLookup(`is-admin:${userId}`, async () => { const currentUser = await db.query.user.findFirst({ where: eq(user.id, userId) @@ -69,6 +76,10 @@ export async function requireProjectAccess( projectId: string, minLevel: PermissionLevel | 'owner' = 'read' ): Promise { + if (accessibilityFixtureEnabled && userId === accessibilityFixtureUser.id) { + if (projectId === accessibilityFixtureProject.id) return; + } + const projectAccess = await loadProjectAccess(db, userId, projectId); if (!projectAccess || projectAccess.deletedAt != null) { @@ -89,6 +100,10 @@ export async function getProjectMemberRole( userId: string, projectId: string ): Promise { + if (accessibilityFixtureEnabled && userId === accessibilityFixtureUser.id) { + if (projectId === accessibilityFixtureProject.id) return 'owner'; + } + const projectAccess = await loadProjectAccess(db, userId, projectId); return projectAccess?.role ?? null; } diff --git a/apps/dashboard/src/lib/server/billing/overview.ts b/apps/dashboard/src/lib/server/billing/overview.ts index 1bf6962..17c2355 100644 --- a/apps/dashboard/src/lib/server/billing/overview.ts +++ b/apps/dashboard/src/lib/server/billing/overview.ts @@ -6,6 +6,10 @@ import { } from './autumn'; import { initDrizzle } from '$lib/server/db'; import { billingMeters, billingUsageEvents, vmTypes } from '$lib/server/db/schema'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureBillingOverview +} from '$lib/server/accessibility-fixtures'; function statusLabel(status: Awaited>['status']) { if (status === 'active') return 'Ready'; @@ -24,6 +28,8 @@ export async function refreshProjectBilling(projectId: string) { } export async function getProjectBillingOverview(projectId: string) { + if (accessibilityFixtureEnabled) return accessibilityFixtureBillingOverview; + const db = initDrizzle(); const now = Date.now(); const billingState = await getProjectBillingState(projectId); diff --git a/apps/dashboard/src/lib/server/env.ts b/apps/dashboard/src/lib/server/env.ts index 3b4b8a0..adccb98 100644 --- a/apps/dashboard/src/lib/server/env.ts +++ b/apps/dashboard/src/lib/server/env.ts @@ -2,6 +2,7 @@ import type { Fetcher, KVNamespace, SendEmail } from '@cloudflare/workers-types' import { getRequestEvent } from '$app/server'; import { dev } from '$app/environment'; import { env as privateEnv } from '$env/dynamic/private'; +import { accessibilityFixtureEnabled } from '$lib/server/accessibility-fixtures'; export type RuntimeEnv = { ORIGIN: string; @@ -34,7 +35,10 @@ export type RuntimeEnv = { }; function required(name: keyof RuntimeEnv, value: string | undefined): string { - if (!value) throw new Error(`${name} is not set`); + if (!value) { + if (accessibilityFixtureEnabled) return `fixture-${name}`; + throw new Error(`${name} is not set`); + } return value; } diff --git a/apps/dashboard/src/routes/(app)/admin/+layout.server.ts b/apps/dashboard/src/routes/(app)/admin/+layout.server.ts index f356330..08f8b8f 100644 --- a/apps/dashboard/src/routes/(app)/admin/+layout.server.ts +++ b/apps/dashboard/src/routes/(app)/admin/+layout.server.ts @@ -9,6 +9,16 @@ import { listIpamPrefixes } from '$lib/remote/ipam.remote'; import { initDrizzle } from '$lib/server/db'; import { requireAdmin } from '$lib/server/auth-context'; import { getFeatureFlags } from '$lib/server/feature-flags'; +import { + accessibilityFixtureEnabled, + accessibilityFixtureVmTypes, + accessibilityFixtureImages, + accessibilityFixtureFeatureFlags, + accessibilityFixtureAdminUsers, + accessibilityFixtureIpamPrefixes, + accessibilityFixtureAdminVms, + accessibilityFixtureAdminProjects +} from '$lib/server/accessibility-fixtures'; export const load: LayoutServerLoad = async ({ depends }) => { depends('app:feature-flags'); @@ -30,6 +40,26 @@ export const load: LayoutServerLoad = async ({ depends }) => { }; } + if (accessibilityFixtureEnabled) { + const [vmTypes, images, adminUsers, ipamPrefixes, adminVms, adminProjects] = await Promise.all([ + listVmTypes(), + listImages(), + listAdminUsers(), + listIpamPrefixes(), + listAllAdminVms(), + listAdminProjects() + ]); + return { + vmTypes, + images, + featureFlags: accessibilityFixtureFeatureFlags, + adminUsers, + ipamPrefixes, + adminVms, + adminProjects + }; + } + await requireAdmin(initDrizzle(), userId); const [vmTypes, images, featureFlags, adminUsers, ipamPrefixes, adminVms, adminProjects] = diff --git a/apps/dashboard/src/routes/(app)/admin/+layout.svelte b/apps/dashboard/src/routes/(app)/admin/+layout.svelte index 81be8be..ab3a6f7 100644 --- a/apps/dashboard/src/routes/(app)/admin/+layout.svelte +++ b/apps/dashboard/src/routes/(app)/admin/+layout.svelte @@ -73,6 +73,7 @@
+

Admin

{#each tabs as tab (tab.href)} {@const active = diff --git a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte index f065097..94d4497 100644 --- a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte @@ -368,6 +368,7 @@ bind:value={subject} placeholder="Subject line" disabled={sending} + aria-label="Subject" class="h-9 w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none" />
@@ -469,6 +470,7 @@ event.currentTarget.value as AudienceField ))} disabled={sending} + aria-label="Condition field" class="h-8 border border-border bg-muted px-2 text-xs text-foreground focus:border-ring focus:outline-none" > {#each Object.entries(audienceFieldDefs) as [value, def] (value)} @@ -478,6 +480,7 @@ {#each vmTypeOptions as typeName (typeName)} @@ -513,6 +519,7 @@