'use server'; import { ValidatorMap } from '@/types/validator-map'; import { db } from '@/db'; import { ArcadeCab, countryValidator, getArcadeCabs, getArcadePermissions } from '@/data/arcade'; import { requireUser } from '@/actions/auth'; import { requireArcadePermission } from '@/helpers/permissions'; import { ArcadePermissions } from '@/types/permissions'; import type { Entries } from 'type-fest'; import { revalidatePath } from 'next/cache'; export type CabUpdate = Omit; const CAB_VALIDATORS: ValidatorMap = new Map(); CAB_VALIDATORS.set('country', countryValidator); const booleanValidator = (val: number | null | undefined) => { val = +(val as any); if (val !== 0 && val !== 1) throw new Error('Invalid boolean value'); return val; } CAB_VALIDATORS.set('board', async (val, cab) => { val = val!.toUpperCase().replace(/-/g, '').trim(); if (!/^[A-Z\d]{15}$/.test(val!)) throw new Error('Invalid board number, must be 15 alphanumeric characters'); const existing = await db.selectFrom('machine') .where(({ eb, and }) => and([ eb('board', '=', val!), ...(cab === null ? [] : [ eb('id', '!=', cab) ]) ])) .select('board') .executeTakeFirst(); if (existing) throw new Error('That board is already in use'); return val; }); const getMachineBySerial = async (serial: string, excludeCab: number | null = null) => { return db.selectFrom('machine') .where(({ eb, and }) => and([ eb('serial', 'like', `${serial.slice(0, 11)}%`), ...(excludeCab === null ? [] : [ eb('id', '!=', excludeCab) ]) ])) .select('serial') .executeTakeFirst(); } CAB_VALIDATORS.set('serial', async (val, cab) => { val = val!.toUpperCase().replace(/-/g, '').trim(); // https://gitea.tendokyu.moe/Hay1tsme/artemis/src/commit/87c7c91e3a7158aabfd2b8dbf69d6a5ca0249da1/core/adb_handlers/base.py#L123 if (!/^A\d{2}[EX]\d{2}[A-HJ-NP-Z]\d{8}$/.test(val)) throw new Error('Invalid keychip format'); if (await getMachineBySerial(val, cab)) throw new Error('That serial is already in use'); return val; }); CAB_VALIDATORS.set('is_cab', booleanValidator); CAB_VALIDATORS.set('ota_enable', booleanValidator); CAB_VALIDATORS.set('game', val => { val = val!.trim().toUpperCase(); // https://gitea.tendokyu.moe/Hay1tsme/artemis/src/commit/87c7c91e3a7158aabfd2b8dbf69d6a5ca0249da1/core/adb_handlers/base.py#L117 if (!/^S[A-Z\d]{3}P?$/.test(val)) throw new Error('Game must be in the format Sxxx or SxxxP'); }); CAB_VALIDATORS.set('timezone', () => {}); CAB_VALIDATORS.set('memo', () => {}); export const deleteMachine = async (arcade: number, machine: number) => { const user = await requireUser(); const arcadePermissions = await getArcadePermissions(user, arcade); requireArcadePermission(arcadePermissions, user.permissions, ArcadePermissions.REGISTRAR); await db.deleteFrom('machine') .where('arcade', '=', arcade) .where('id', '=', machine) .executeTakeFirst(); revalidatePath('/arcade', 'page'); revalidatePath('/arcade/[arcadeId]', 'page'); }; const validateUpdate = async (update: CabUpdate, cab: number | null) => { if (!('serial' in update)) return { error: true, message: 'Keychip is required' }; for (let [key, val] of (Object.entries(update) as Entries)) { if (!CAB_VALIDATORS.has(key)) return { error: true, message: `Unknown key ${key}` }; if (key === 'serial' && !val) return { error: true, message: 'Keychip is required' }; try { if (val === undefined) val = null; if (val !== null) (update as any)[key] = ((await CAB_VALIDATORS.get(key)!(val, cab)) ?? val) as any; else (update as any)[key] = null; } catch (e: any) { return { error: true, message: e?.message ?? 'Unknown error' }; } } } export const updateMachine = async ({ arcade, machine, update }: { arcade: number, machine: number, update: CabUpdate }) => { const user = await requireUser(); const arcadePermissions = await getArcadePermissions(user, arcade); requireArcadePermission(arcadePermissions, user.permissions, ArcadePermissions.REGISTRAR); const res = await validateUpdate(update, machine); if (res?.error) return res; await db.updateTable('machine') .where('machine.id', '=', machine) .where('machine.arcade', '=', arcade) .set(update) .executeTakeFirst(); revalidatePath('/arcade', 'page'); revalidatePath('/arcade/[arcadeId]', 'page'); }; export const createMachine = async ({ arcade, update }: { arcade: number, update: CabUpdate }) => { const user = await requireUser(); const arcadePermissions = await getArcadePermissions(user, arcade); requireArcadePermission(arcadePermissions, user.permissions, ArcadePermissions.REGISTRAR); const res = await validateUpdate(update, null); if (res?.error) return { ...res, data: [] }; await db.insertInto('machine') .values({ ...update, arcade }) .executeTakeFirst(); revalidatePath('/arcade', 'page'); revalidatePath('/arcade/[arcadeId]', 'page'); return { error: false, message: '', data: structuredClone(await getArcadeCabs({ arcade, user, permissions: arcadePermissions })) }; }