feat(mvp): phase 2 - database schema & services layer

Define full Prisma schema (10 models), run initial migration, build core
services (auth, user, group, app, board, permission), Zod validators,
type definitions, API response envelope, constants, and seed script.
This commit is contained in:
2026-03-24 20:00:21 +03:00
parent cf6bde238c
commit f1b1aa5975
28 changed files with 2936 additions and 28 deletions
+169
View File
@@ -0,0 +1,169 @@
import { z } from 'zod';
import {
UserRole,
AuthMode,
WidgetType,
IconType,
PermissionLevel,
EntityType,
TargetType,
HealthcheckMethod
} from './constants.js';
// --- Auth ---
export const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(1, 'Password is required')
});
export const registerSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters'),
displayName: z.string().min(1, 'Display name is required').max(100)
});
// --- User ---
export const createUserSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(6).optional(),
displayName: z.string().min(1).max(100),
avatarUrl: z.string().url().optional(),
authProvider: z.enum([AuthMode.LOCAL, AuthMode.OAUTH]).optional(),
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional()
});
export const updateUserSchema = z.object({
displayName: z.string().min(1).max(100).optional(),
avatarUrl: z.string().url().nullable().optional(),
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional()
});
// --- Group ---
export const createGroupSchema = z.object({
name: z.string().min(1, 'Group name is required').max(100),
description: z.string().max(500).optional(),
isDefault: z.boolean().optional()
});
export const updateGroupSchema = z.object({
name: z.string().min(1).max(100).optional(),
description: z.string().max(500).nullable().optional(),
isDefault: z.boolean().optional()
});
// --- App ---
export const createAppSchema = z.object({
name: z.string().min(1, 'App name is required').max(200),
url: z.string().url('Invalid URL'),
icon: z.string().max(500).optional(),
iconType: z.enum([IconType.LUCIDE, IconType.SIMPLE, IconType.URL, IconType.EMOJI]).optional(),
description: z.string().max(1000).optional(),
category: z.string().max(100).optional(),
tags: z.string().max(500).optional(),
healthcheckEnabled: z.boolean().optional(),
healthcheckInterval: z.number().int().min(30).max(86400).optional(),
healthcheckMethod: z.enum([HealthcheckMethod.GET, HealthcheckMethod.HEAD]).optional(),
healthcheckExpectedStatus: z.number().int().min(100).max(599).optional(),
healthcheckTimeout: z.number().int().min(1000).max(30000).optional()
});
export const updateAppSchema = z.object({
name: z.string().min(1).max(200).optional(),
url: z.string().url().optional(),
icon: z.string().max(500).nullable().optional(),
iconType: z.enum([IconType.LUCIDE, IconType.SIMPLE, IconType.URL, IconType.EMOJI]).optional(),
description: z.string().max(1000).nullable().optional(),
category: z.string().max(100).nullable().optional(),
tags: z.string().max(500).optional(),
healthcheckEnabled: z.boolean().optional(),
healthcheckInterval: z.number().int().min(30).max(86400).optional(),
healthcheckMethod: z.enum([HealthcheckMethod.GET, HealthcheckMethod.HEAD]).optional(),
healthcheckExpectedStatus: z.number().int().min(100).max(599).optional(),
healthcheckTimeout: z.number().int().min(1000).max(30000).optional()
});
// --- Board ---
export const createBoardSchema = z.object({
name: z.string().min(1, 'Board name is required').max(200),
icon: z.string().max(500).optional(),
description: z.string().max(1000).optional(),
isDefault: z.boolean().optional(),
isGuestAccessible: z.boolean().optional(),
backgroundConfig: z.string().optional()
});
export const updateBoardSchema = z.object({
name: z.string().min(1).max(200).optional(),
icon: z.string().max(500).nullable().optional(),
description: z.string().max(1000).nullable().optional(),
isDefault: z.boolean().optional(),
isGuestAccessible: z.boolean().optional(),
backgroundConfig: z.string().nullable().optional()
});
// --- Section ---
export const createSectionSchema = z.object({
boardId: z.string().cuid(),
title: z.string().min(1, 'Section title is required').max(200),
icon: z.string().max(500).optional(),
order: z.number().int().min(0).optional(),
isExpandedByDefault: z.boolean().optional()
});
export const updateSectionSchema = z.object({
title: z.string().min(1).max(200).optional(),
icon: z.string().max(500).nullable().optional(),
order: z.number().int().min(0).optional(),
isExpandedByDefault: z.boolean().optional()
});
// --- Widget ---
export const createWidgetSchema = z.object({
sectionId: z.string().cuid(),
type: z.enum([WidgetType.APP, WidgetType.BOOKMARK, WidgetType.NOTE, WidgetType.EMBED, WidgetType.STATUS]),
order: z.number().int().min(0).optional(),
config: z.string().optional(),
appId: z.string().cuid().optional()
});
export const updateWidgetSchema = z.object({
type: z
.enum([WidgetType.APP, WidgetType.BOOKMARK, WidgetType.NOTE, WidgetType.EMBED, WidgetType.STATUS])
.optional(),
order: z.number().int().min(0).optional(),
config: z.string().optional(),
appId: z.string().cuid().nullable().optional()
});
// --- Permission ---
export const createPermissionSchema = z.object({
entityType: z.enum([EntityType.BOARD, EntityType.APP]),
entityId: z.string().cuid(),
targetType: z.enum([TargetType.USER, TargetType.GROUP]),
targetId: z.string().cuid(),
level: z.enum([PermissionLevel.VIEW, PermissionLevel.EDIT, PermissionLevel.ADMIN])
});
// --- System Settings ---
export const updateSystemSettingsSchema = z.object({
authMode: z.enum([AuthMode.LOCAL, AuthMode.OAUTH, AuthMode.BOTH]).optional(),
registrationEnabled: z.boolean().optional(),
oauthClientId: z.string().nullable().optional(),
oauthClientSecret: z.string().nullable().optional(),
oauthDiscoveryUrl: z.string().url().nullable().optional(),
defaultTheme: z.enum(['dark', 'light']).optional(),
defaultPrimaryColor: z
.string()
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
.optional(),
healthcheckDefaults: z.string().optional()
});