mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 13:19:18 +02:00
214 lines
7.7 KiB
TypeScript
214 lines
7.7 KiB
TypeScript
import vine from '@vinejs/vine'
|
|
import { LINK_TILE_COLOR_IDS } from '../../constants/link_tile_colors.js'
|
|
|
|
export const installServiceValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
})
|
|
)
|
|
|
|
export const affectServiceValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
action: vine.enum(['start', 'stop', 'restart']),
|
|
})
|
|
)
|
|
|
|
export const subscribeToReleaseNotesValidator = vine.compile(
|
|
vine.object({
|
|
email: vine.string().email().trim(),
|
|
})
|
|
)
|
|
|
|
export const checkLatestVersionValidator = vine.compile(
|
|
vine.object({
|
|
force: vine.boolean().optional(), // Optional flag to force bypassing cache and checking for updates immediately
|
|
})
|
|
)
|
|
|
|
export const updateServiceValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
target_version: vine.string().trim(),
|
|
})
|
|
)
|
|
|
|
export const preflightValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
})
|
|
)
|
|
|
|
// Toggle per-app automatic updates (opt-in). The global master switch lives in
|
|
// the KVStore (`appAutoUpdate.enabled`) and flows through the settings endpoint.
|
|
export const setServiceAutoUpdateValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
enabled: vine.boolean(),
|
|
})
|
|
)
|
|
|
|
// Shared sub-schema for a volume bind mapping. A colon is Docker's bind delimiter
|
|
// (host:container:options) — forbid it in either field so a path can't smuggle in an
|
|
// extra segment that the guard reads as safe but Docker re-parses as a different mount.
|
|
const volumeSchema = vine.object({
|
|
host_path: vine.string().trim().regex(/^[^:]+$/),
|
|
container_path: vine.string().trim().regex(/^[^:]+$/),
|
|
})
|
|
|
|
// Environment variables must be KEY=value (value may be empty), matching Docker's Env format.
|
|
const envVarSchema = vine.string().trim().regex(/^[A-Za-z_][A-Za-z0-9_]*=[\s\S]*$/)
|
|
|
|
// Service-less preflight for the custom-app form: evaluates ports, volumes and image together.
|
|
export const preflightCustomValidator = vine.compile(
|
|
vine.object({
|
|
image: vine.string().trim().optional(),
|
|
ports: vine.array(vine.number().min(1).max(65535)).optional(),
|
|
volumes: vine.array(volumeSchema).optional(),
|
|
// When editing, ignore port conflicts caused by this app's own running container.
|
|
exclude_service: vine.string().trim().optional(),
|
|
})
|
|
)
|
|
|
|
export const customAppValidator = vine.compile(
|
|
vine.object({
|
|
friendly_name: vine.string().trim().minLength(1).maxLength(100),
|
|
image: vine.string().trim().minLength(1),
|
|
ports: vine
|
|
.array(
|
|
vine.object({
|
|
container: vine.number().min(1).max(65535),
|
|
host: vine.number().min(1024).max(65535),
|
|
})
|
|
)
|
|
.optional(),
|
|
volumes: vine.array(volumeSchema).optional(),
|
|
env: vine.array(envVarSchema).optional(),
|
|
category: vine
|
|
.enum(['productivity', 'media', 'security', 'networking', 'utility', 'ai', 'education', 'custom'])
|
|
.optional(),
|
|
icon: vine.string().trim().optional(),
|
|
// Optional resource caps (advanced). Default caps are applied when omitted.
|
|
memory_mb: vine.number().min(64).optional(),
|
|
cpus: vine.number().min(0.1).max(64).optional(),
|
|
// When true, bypass advisory preflight (port conflicts / guard warnings) and install anyway.
|
|
force: vine.boolean().optional(),
|
|
})
|
|
)
|
|
|
|
// Set or clear an app's custom launch URL. A null/empty value clears the override; a non-empty
|
|
// value is normalized + validated to a http(s) URL by normalizeCustomUrl in the controller.
|
|
export const setServiceCustomUrlValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
custom_url: vine.string().trim().maxLength(255).nullable(),
|
|
})
|
|
)
|
|
|
|
/**
|
|
* Normalize a user-supplied custom app URL (backend twin of the inertia helper in
|
|
* lib/navigation.ts). Accepts a bare host or a full URL; prepends http:// when no scheme is
|
|
* present. Returns the normalized href, or null when empty (clears the override) or not a valid
|
|
* http(s) URL. Restricting to http/https blocks javascript:/data: from ever being stored.
|
|
*/
|
|
export function normalizeCustomUrl(input: string | null | undefined): string | null {
|
|
const trimmed = (input ?? '').trim()
|
|
if (!trimmed) return null
|
|
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`
|
|
try {
|
|
const url = new URL(withScheme)
|
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
// services.custom_url uses the database driver's default VARCHAR(255) size.
|
|
// Check the normalized value because prepending a scheme or URL canonicalization
|
|
// can make a valid-looking input longer than the stored column.
|
|
return url.href.length <= 255 ? url.href : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dashboard link tile: a shortcut to something the user already runs, with no
|
|
* container behind it. One `url` field rather than separate host/port/path,
|
|
* because people paste URLs and it gets https and sub-paths for free. The URL is
|
|
* normalized and http(s)-restricted by normalizeCustomUrl in the controller,
|
|
* which is what keeps javascript:/data: out of an href.
|
|
*/
|
|
export const createLinkTileValidator = vine.compile(
|
|
vine.object({
|
|
friendly_name: vine.string().trim().minLength(1).maxLength(60),
|
|
url: vine.string().trim().minLength(1).maxLength(2048),
|
|
description: vine.string().trim().maxLength(200).nullable().optional(),
|
|
icon: vine.string().trim().maxLength(60).nullable().optional(),
|
|
display_order: vine.number().min(0).max(999).optional(),
|
|
link_color: vine.enum(LINK_TILE_COLOR_IDS).optional(),
|
|
})
|
|
)
|
|
|
|
/** Reconfigure an existing link tile. Identified by service_name, which is immutable. */
|
|
export const updateLinkTileValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
friendly_name: vine.string().trim().minLength(1).maxLength(60),
|
|
url: vine.string().trim().minLength(1).maxLength(2048),
|
|
description: vine.string().trim().maxLength(200).nullable().optional(),
|
|
icon: vine.string().trim().maxLength(60).nullable().optional(),
|
|
display_order: vine.number().min(0).max(999).optional(),
|
|
link_color: vine.enum(LINK_TILE_COLOR_IDS).optional(),
|
|
})
|
|
)
|
|
|
|
export const deleteLinkTileValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
})
|
|
)
|
|
|
|
export const deleteCustomAppValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
// When true, also remove the backing Docker image (best-effort).
|
|
remove_image: vine.boolean().optional(),
|
|
})
|
|
)
|
|
|
|
export const uninstallServiceValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
// When true, also remove the backing Docker image (best-effort).
|
|
remove_image: vine.boolean().optional(),
|
|
})
|
|
)
|
|
|
|
export const serviceLogsValidator = vine.compile(
|
|
vine.object({
|
|
tail: vine.number().min(1).max(2000).optional(),
|
|
})
|
|
)
|
|
|
|
// Reconfigure an existing custom app: the create shape plus the target service_name.
|
|
export const updateCustomAppValidator = vine.compile(
|
|
vine.object({
|
|
service_name: vine.string().trim(),
|
|
friendly_name: vine.string().trim().minLength(1).maxLength(100),
|
|
image: vine.string().trim().minLength(1),
|
|
ports: vine
|
|
.array(
|
|
vine.object({
|
|
container: vine.number().min(1).max(65535),
|
|
host: vine.number().min(1024).max(65535),
|
|
})
|
|
)
|
|
.optional(),
|
|
volumes: vine.array(volumeSchema).optional(),
|
|
env: vine.array(envVarSchema).optional(),
|
|
category: vine
|
|
.enum(['productivity', 'media', 'security', 'networking', 'utility', 'ai', 'education', 'custom'])
|
|
.optional(),
|
|
icon: vine.string().trim().optional(),
|
|
memory_mb: vine.number().min(64).optional(),
|
|
cpus: vine.number().min(0.1).max(64).optional(),
|
|
force: vine.boolean().optional(),
|
|
})
|
|
)
|