import { z } from 'zod'; import { STREAMING_SERVICES } from './services'; export const usernameSchema = z .string() .trim() .toLowerCase() .regex(/^[a-z0-9_]{3,32}$/, 'Username must be 3-32 characters: letters, numbers, underscores'); // Trim before validating: iOS keyboards often append a space after autocompleting an email. const emailSchema = z.string().trim().toLowerCase().pipe(z.email()); const newPasswordSchema = z.string().min(8, 'Password must be at least 8 characters').max(200); export const registerSchema = z.object({ username: usernameSchema, email: emailSchema, password: newPasswordSchema, }); export const forgotPasswordSchema = z.object({ email: emailSchema }); export const verifyEmailSchema = z.object({ token: z.string().min(20).max(200) }); export const changePasswordSchema = z.object({ currentPassword: z.string().min(1).max(200), newPassword: newPasswordSchema, }); export const changeEmailSchema = z.object({ email: emailSchema, password: z.string().min(1).max(200), }); export const resetPasswordSchema = z.object({ token: z.string().min(20).max(200), password: newPasswordSchema, }); export const loginSchema = z.object({ email: emailSchema, password: z.string().min(1).max(200), }); export const updateMeSchema = z.object({ displayName: z.string().trim().max(64).nullable().optional(), bio: z.string().trim().max(280).nullable().optional(), preferredService: z.enum(STREAMING_SERVICES).nullable().optional(), services: z .array(z.enum(STREAMING_SERVICES)) .max(STREAMING_SERVICES.length) .transform((s) => [...new Set(s)]) .nullable() .optional(), notifyPrefs: z .object({ reaction: z.boolean(), follow: z.boolean(), share: z.boolean() }) .partial() .optional(), /** Mark onboarding as finished. */ onboarded: z.literal(true).optional(), }); /** Send a song to people who follow you. */ export const sendSongSchema = z.object({ trackId: z.uuid(), to: z.array(z.string().min(1).max(32)).min(1).max(20), message: z.string().trim().max(280).nullable().optional(), }); /** A browser's or phone's push subscription (both use Web Push encryption keys). */ export const pushSubscriptionSchema = z.object({ kind: z.enum(['web', 'unifiedpush']), endpoint: z.url({ protocol: /^https?$/ }).max(2000), keys: z.object({ p256dh: z.string().min(20).max(200), auth: z.string().min(8).max(100) }), }); export const setFeaturedSchema = z.union([ z.object({ deezerId: z.string().regex(/^\d+$/), comment: z.string().trim().max(280).nullable().optional() }), z.object({ trackId: z.uuid(), comment: z.string().trim().max(280).nullable().optional() }), ]); export const connectUsernameSchema = z.object({ username: z.string().trim().min(1).max(64), }); export type RegisterInput = z.infer; export type LoginInput = z.infer; export type ResetPasswordInput = z.infer; export type ChangePasswordInput = z.infer; export type ChangeEmailInput = z.infer; export type UpdateMeInput = z.infer; export type SetFeaturedInput = z.infer; export type SendSongInput = z.infer; export type PushSubscriptionInput = z.infer; export const jellyfinConnectSchema = z.object({ serverUrl: z.string().trim().min(1).max(500), username: z.string().trim().min(1).max(128), password: z.string().max(256).default(''), }); /** Pause sharing: for an hour, until tomorrow, until turned back on, or resume (null). */ export const PAUSE_OPTIONS = ['1h', '8h', 'indefinitely'] as const; export const setSharingSchema = z.object({ pause: z.enum(PAUSE_OPTIONS).nullable() }); export const REPORT_REASONS = ['spam', 'harassment', 'inappropriate', 'impersonation', 'other'] as const; export type ReportReason = (typeof REPORT_REASONS)[number]; export const REPORT_REASON_LABELS: Record = { spam: 'Spam or fake account', harassment: 'Harassment or bullying', inappropriate: 'Inappropriate profile or picture', impersonation: 'Pretending to be someone else', other: 'Something else', }; export const reportSchema = z.object({ reason: z.enum(REPORT_REASONS), note: z.string().trim().max(500).nullish(), });