Этап 1a: фундамент

- Next 16.4 и Payload 3.91 в одном приложении, PostgreSQL с UUID,
  TypeScript 6, ESLint 9, Prettier, Vitest
- Payload: администраторы с ролями, multi-tenant по сайтам, локализация
  en/ru/de/fr/es, интерфейс админки ru/en, языки сайта (sites.locales)
- Коллекции sites, domains, currencies, countries, locations, tags,
  tariffs, payment-systems (manual), media и начальные данные (pnpm seed)
- Proxy: хост → домен, язык и валюта, внутренние маршруты по хосту;
  /admin и /api закрыты на доменах брендов
- Токены и темы брендов, оболочки сайта и кабинета, переключатели языка,
  валюты и темы, заглушки страниц
- /svc/health, юнит systemd veilix-dev.service
- Документация: решения D-022…D-024, команды в CLAUDE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreypr207andClaude Opus 5.5 committed 2026-10-21 05:08:05 +02:00
1 parent 3400f58a5a
commit 7e331c2c9d
140 files changed
+15885 -43

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
import type {
CollectionConfig,
SelectFieldManyValidation,
TextFieldSingleValidation,
} from 'payload'
import { select } from 'payload/shared'
import { DEFAULT_LOCALE, LOCALES } from '@/i18n/locales'
import { anyRole, managers, owners } from '../access/roles'
import { localeOptions } from '../fields/locales'
import { groups, label, message } from '../i18n'
/**
* Языки сайта — языки его контента. Домен показывает любую их часть.
* Английский обязателен: на него откатывается непереведённый контент.
*/
const validateLocales: SelectFieldManyValidation = async (value, args) => {
const base = select(value, args)
if (base !== true) return base
const locales = value ?? []
if (!locales.includes(DEFAULT_LOCALE)) {
return message(args.req, {
en: 'English is required: untranslated content falls back to it',
ru: 'Английский обязателен: на него откатывается непереведённый контент',
})
}
if (args.id) {
const { docs } = await args.req.payload.find({
collection: 'domains',
where: { site: { equals: args.id } },
depth: 0,
pagination: false,
overrideAccess: true,
req: args.req,
})
const used = new Set(docs.flatMap((domain) => domain.locales ?? []))
const missing = [...used].filter((locale) => !locales.includes(locale))
if (missing.length > 0) {
return message(args.req, {
en: `These languages are enabled on the site's domains, remove them there first: ${missing.join(', ')}`,
ru: `Эти языки включены на доменах сайта, сначала уберите их там: ${missing.join(', ')}`,
})
}
}
return true
}
const validateOrderPrefix: TextFieldSingleValidation = (value, { req }) =>
typeof value === 'string' && /^[A-Z]{2,4}$/.test(value)
? true
: message(req, {
en: '2–4 capital Latin letters, e.g. VX',
ru: '2–4 заглавные латинские буквы, например VX',
})
export const Sites: CollectionConfig = {
slug: 'sites',
labels: {
singular: label('Site', 'Сайт'),
plural: label('Sites', 'Сайты'),
},
admin: {
group: groups.settings,
useAsTitle: 'name',
defaultColumns: ['name', 'code', 'orderPrefix', 'locales'],
},
access: {
read: anyRole,
create: managers,
update: managers,
// вместе с сайтом плагин удаляет все его документы
delete: owners,
},
fields: [
{
name: 'code',
type: 'select',
required: true,
unique: true,
options: [
{ value: 'dedicated', label: label('Veilix (dedicated)', 'Вейликс (dedicated)') },
{ value: 'mass', label: label('VeilixVPN (mass)', 'ВейликсВПН (mass)') },
],
label: label('Brand', 'Бренд'),
// по коду выбираются тема и страницы бренда, после создания не меняется
access: { update: () => false },
},
{
name: 'name',
type: 'text',
required: true,
localized: true,
label: label('Name', 'Название'),
},
{
name: 'orderPrefix',
type: 'text',
required: true,
unique: true,
label: label('Order number prefix', 'Префикс номеров'),
validate: validateOrderPrefix,
},
{
name: 'locales',
type: 'select',
hasMany: true,
required: true,
options: localeOptions,
defaultValue: [...LOCALES],
label: label('Content languages', 'Языки контента'),
validate: validateLocales,
admin: {
description: label(
'Each domain of the site shows some of these languages.',
'Каждый домен сайта показывает часть этих языков.',
),
},
},
],
}