Full-stack implementation across 7 sprints: - Backend: 5 Prisma models (StrawPoll, Option, Vote, Comment, Challenge), 4 enums, POLLS_ADMIN role, admin CRUD routes, public voting/SSE/widget endpoints, BullMQ auto-close queue, rate limiting - Admin: StrawPollsPage with inline drawers (campaigns pattern), PollResults bar chart, sidebar under Advocacy - Public: dedicated poll lander with real-time SSE updates, browse page, anonymous voting with token dedup - MkDocs: straw-poll-widget.js hydration (inline vote + card link modes), GrapesJS block types - Social: feed activity (poll_voted), friend badge integration, challenge notifications, notification preferences - Feature flag: enablePolls toggle in Settings, FeatureGate, Zod schema Bunker Admin
142 lines
5.3 KiB
TypeScript
142 lines
5.3 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Must be a hex color (e.g. #ff00ff)');
|
|
|
|
export const updateSiteSettingsSchema = z.object({
|
|
// Organization
|
|
organizationName: z.string().min(1).max(100).optional(),
|
|
organizationShortName: z.string().min(1).max(10).optional(),
|
|
organizationLogoUrl: z.string().url().nullable().optional().or(z.literal('')),
|
|
organizationFaviconUrl: z.string().url().nullable().optional().or(z.literal('')),
|
|
|
|
// Admin theme
|
|
adminColorPrimary: hexColor.optional(),
|
|
adminColorBgBase: hexColor.optional(),
|
|
|
|
// Public theme
|
|
publicColorPrimary: hexColor.optional(),
|
|
publicColorBgBase: hexColor.optional(),
|
|
publicColorBgContainer: hexColor.optional(),
|
|
publicHeaderGradient: z.string().max(500).optional(),
|
|
|
|
// Text
|
|
footerText: z.string().max(200).optional(),
|
|
loginSubtitle: z.string().max(50).optional(),
|
|
|
|
// Email branding
|
|
emailFromName: z.string().min(1).max(100).optional(),
|
|
|
|
// SMTP configuration
|
|
smtpHost: z.string().max(255).optional(),
|
|
smtpPort: z.number().int().min(0).max(65535).optional(),
|
|
smtpUser: z.string().max(255).optional(),
|
|
smtpPass: z.string().max(500).optional(),
|
|
smtpFromAddress: z.string().max(255).optional(),
|
|
smtpActiveProvider: z.enum(['mailhog', 'production']).optional(),
|
|
emailTestMode: z.boolean().optional(),
|
|
testEmailRecipient: z.string().max(255).optional(),
|
|
|
|
// Registration settings
|
|
enablePublicRegistration: z.boolean().optional(),
|
|
enableEmailVerification: z.boolean().optional(),
|
|
autoApproveVerifiedUsers: z.boolean().optional(),
|
|
|
|
// Feature toggles
|
|
enableInfluence: z.boolean().optional(),
|
|
enableMap: z.boolean().optional(),
|
|
enableNewsletter: z.boolean().optional(),
|
|
enableLandingPages: z.boolean().optional(),
|
|
enableMediaFeatures: z.boolean().optional(),
|
|
enablePayments: z.boolean().optional(),
|
|
enableGalleryAds: z.boolean().optional(),
|
|
enableChat: z.boolean().optional(),
|
|
enableEvents: z.boolean().optional(),
|
|
enableDocsComments: z.boolean().optional(),
|
|
enableSms: z.boolean().optional(),
|
|
enablePeople: z.boolean().optional(),
|
|
enableSocial: z.boolean().optional(),
|
|
enableMeet: z.boolean().optional(),
|
|
enableMeetingPlanner: z.boolean().optional(),
|
|
enableTicketedEvents: z.boolean().optional(),
|
|
enablePolls: z.boolean().optional(),
|
|
enableSocialCalendar: z.boolean().optional(),
|
|
enableDocsCollaboration: z.boolean().optional(),
|
|
requireEventApproval: z.boolean().optional(),
|
|
autoSyncPeopleToMap: z.boolean().optional(),
|
|
|
|
// SMS connection config
|
|
smsTermuxApiUrl: z.string().max(500).optional(),
|
|
smsTermuxApiKey: z.string().max(500).optional(),
|
|
smsTailscaleApiKey: z.string().max(500).optional(),
|
|
smsTailscaleTailnet: z.string().max(200).optional(),
|
|
smsTailscaleDeviceId: z.string().max(200).optional(),
|
|
smsTailscaleDeviceName: z.string().max(200).optional(),
|
|
|
|
// Gitea Docs Comments
|
|
giteaApiToken: z.string().max(500).optional(),
|
|
giteaCommentsRepoOwner: z.string().max(100).optional(),
|
|
giteaCommentsRepoName: z.string().max(100).optional(),
|
|
giteaOauthClientId: z.string().max(500).optional(),
|
|
giteaOauthClientSecret: z.string().max(500).optional(),
|
|
giteaSetupComplete: z.boolean().optional(),
|
|
|
|
// User Provisioning
|
|
enableUserProvisioning: z.boolean().optional(),
|
|
provisionGitea: z.boolean().optional(),
|
|
provisionGiteaTiming: z.enum(['lazy', 'eager']).optional(),
|
|
provisionVaultwarden: z.boolean().optional(),
|
|
provisionVaultwardenTiming: z.enum(['lazy', 'eager']).optional(),
|
|
provisionListmonk: z.boolean().optional(),
|
|
provisionListmonkTiming: z.enum(['lazy', 'eager']).optional(),
|
|
|
|
// Auto-upgrade settings
|
|
enableAutoUpgrade: z.boolean().optional(),
|
|
autoUpgradeSchedule: z.enum([
|
|
'daily-3am', 'daily-4am', 'daily-5am',
|
|
'weekly-sun-3am', 'weekly-mon-3am', '12h', '24h',
|
|
]).optional(),
|
|
autoUpgradePullServices: z.boolean().optional(),
|
|
notifyAdminAutoUpgrade: z.boolean().optional(),
|
|
|
|
// Navigation configuration (supports one level of nesting via groups)
|
|
navConfig: z.object({
|
|
items: z.array(z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
path: z.string(),
|
|
icon: z.string(),
|
|
enabled: z.boolean(),
|
|
order: z.number(),
|
|
type: z.enum(['builtin', 'custom', 'group']),
|
|
featureFlag: z.string().optional(),
|
|
external: z.boolean().optional(),
|
|
children: z.array(z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
path: z.string(),
|
|
icon: z.string(),
|
|
enabled: z.boolean(),
|
|
order: z.number(),
|
|
type: z.enum(['builtin', 'custom']),
|
|
featureFlag: z.string().optional(),
|
|
external: z.boolean().optional(),
|
|
})).optional(),
|
|
})),
|
|
}).optional(),
|
|
|
|
// Notification settings
|
|
notifyAdminShiftSignup: z.boolean().optional(),
|
|
notifyAdminResponseSubmitted: z.boolean().optional(),
|
|
notifyAdminSignRequested: z.boolean().optional(),
|
|
notifyAdminShiftCancellation: z.boolean().optional(),
|
|
notifyVolunteerSessionSummary: z.boolean().optional(),
|
|
notifyVolunteerCancellation: z.boolean().optional(),
|
|
notifyVolunteerShiftReminder: z.boolean().optional(),
|
|
notifyVolunteerShiftThankYou: z.boolean().optional(),
|
|
notifyVolunteerReengagement: z.boolean().optional(),
|
|
reengagementInactiveDays: z.number().int().min(1).max(365).optional(),
|
|
reengagementCooldownDays: z.number().int().min(1).max(365).optional(),
|
|
});
|
|
|
|
export type UpdateSiteSettingsInput = z.infer<typeof updateSiteSettingsSchema>;
|