diff --git a/AGENTS.md b/AGENTS.md index a2ead822e2f..b31363316a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ pnpm run reboot - Controller/Parser (в Docker): хосты по именам контейнеров из docker-compose (порт БД 5432) - Boot (на хосте): `127.0.0.1`, PG порт `5532`, mongo через `/etc/hosts` - Desktop: `127.0.0.1` + - Controller: `BACKEND_URL` (публичный URL API) и `FRONTEND_URL` (публичный URL SPA), см. `components/controller/.env-example` - **CHAIN_ID**: берётся из `curl http://localhost:8888/v1/chain/get_info` после старта ноды - Controller требует `VAPID_PUBLIC_KEY` и `VAPID_PRIVATE_KEY` diff --git a/components/controller/.env-example b/components/controller/.env-example index 37674a023be..d49a0bbe5dc 100644 --- a/components/controller/.env-example +++ b/components/controller/.env-example @@ -1,7 +1,9 @@ NODE_ENV=development -# Заменить на адрес домена (используйтся в ссылке для восстановления доступа) -BASE_URL=http://localhost:3005 +# Публичный URL контроллера (API): на деве обычно порт 2998; на проде может быть https://домен/backend +BACKEND_URL=http://127.0.0.1:2998 +# Публичный URL рабочего стола (SPA): письма, приглашения, deep links в ICS +FRONTEND_URL=http://127.0.0.1:2999 # Port number PORT=2998 diff --git a/components/controller/AGENTS.md b/components/controller/AGENTS.md index 58bb90eb4be..9f68206a5b7 100644 --- a/components/controller/AGENTS.md +++ b/components/controller/AGENTS.md @@ -709,7 +709,7 @@ this.providerPort.registerProvider(this.name, this); Все переменные валидируются Zod-схемой в `src/config/config.ts`. При ошибке — процесс завершается. **Обязательные**: -- `NODE_ENV`, `BASE_URL`, `SERVER_SECRET`, `PORT` +- `NODE_ENV`, `BACKEND_URL`, `FRONTEND_URL`, `SERVER_SECRET`, `PORT` - `MONGODB_URL`, `COOPNAME` - `JWT_SECRET` - Переменные PG-подключения: `HOST, PORT, USER, PASS, DB` diff --git a/components/controller/src/application/agenda/services/agenda-notification.service.ts b/components/controller/src/application/agenda/services/agenda-notification.service.ts index 7c7538b3126..bba4a412d12 100644 --- a/components/controller/src/application/agenda/services/agenda-notification.service.ts +++ b/components/controller/src/application/agenda/services/agenda-notification.service.ts @@ -74,7 +74,7 @@ export class AgendaNotificationService implements OnModuleInit { itemDescription: ``, authorName, decision_id: agendaId, - agendaUrl: `${config.base_url}/${action.coopname}/soviet/agenda`, + agendaUrl: `${config.frontend_url}/${action.coopname}/soviet/agenda`, }; // Отправляем уведомления каждому члену совета в цикле diff --git a/components/controller/src/application/agenda/services/decision-notification.service.ts b/components/controller/src/application/agenda/services/decision-notification.service.ts index 78dd80a3240..4034dc3695d 100644 --- a/components/controller/src/application/agenda/services/decision-notification.service.ts +++ b/components/controller/src/application/agenda/services/decision-notification.service.ts @@ -72,7 +72,7 @@ export class DecisionNotificationService implements OnModuleInit { decisionTitle: `Решение №${decisionId}`, coopname: action.coopname, decision_id: decisionId, - decisionUrl: `${config.base_url}`, + decisionUrl: `${config.frontend_url}`, }; // Отправляем уведомление diff --git a/components/controller/src/application/auth/interactors/auth.interactor.ts b/components/controller/src/application/auth/interactors/auth.interactor.ts index 636d743a29d..e6b711313e8 100644 --- a/components/controller/src/application/auth/interactors/auth.interactor.ts +++ b/components/controller/src/application/auth/interactors/auth.interactor.ts @@ -63,7 +63,7 @@ export class AuthInteractor { throw new Error('User not found'); } - const resetUrl = `${config.base_url}/${config.coopname}/auth/reset-key?token=${resetKeyToken}`; + const resetUrl = `${config.frontend_url}/${config.coopname}/auth/reset-key?token=${resetKeyToken}`; await this.notificationSenderService.sendNotificationToUser(user.username, Workflows.ResetKey.id, { resetUrl }); } @@ -71,7 +71,7 @@ export class AuthInteractor { async sendVerificationEmail(username: string): Promise { const user = await this.userDomainService.getUserByUsername(username); const verifyEmailToken = await this.tokenApplicationService.generateVerifyEmailToken(user.id); - const verificationUrl = `${config.base_url}/${config.coopname}/auth/verify-email?token=${verifyEmailToken}`; + const verificationUrl = `${config.frontend_url}/${config.coopname}/auth/verify-email?token=${verifyEmailToken}`; await this.notificationSenderService.sendNotificationToUser(user.username, Workflows.EmailVerification.id, { verificationUrl, diff --git a/components/controller/src/application/gateway/services/payment-notification.service.ts b/components/controller/src/application/gateway/services/payment-notification.service.ts index 176b677163f..f5e2d4f34bc 100644 --- a/components/controller/src/application/gateway/services/payment-notification.service.ts +++ b/components/controller/src/application/gateway/services/payment-notification.service.ts @@ -61,7 +61,7 @@ export class PaymentNotificationService implements OnModuleInit { paymentAmount: payment.quantity.toFixed(2), paymentCurrency: payment.symbol, paymentDate: payment.created_at.toLocaleString('ru-RU'), - paymentUrl: `${config.base_url}`, //TODO: точную ссылку потом + paymentUrl: `${config.frontend_url}`, //TODO: точную ссылку потом }; // Отправляем уведомление diff --git a/components/controller/src/application/participant/interactors/participant.interactor.ts b/components/controller/src/application/participant/interactors/participant.interactor.ts index a52ace3a28f..e8287d4dbc1 100644 --- a/components/controller/src/application/participant/interactors/participant.interactor.ts +++ b/components/controller/src/application/participant/interactors/participant.interactor.ts @@ -317,7 +317,7 @@ export class ParticipantInteractor { throw new HttpApiError(http.NOT_FOUND, 'Пользователь не найден'); } const token = await this.tokenApplicationService.generateInviteToken(data.email, user.id); - const inviteUrl = `${config.base_url}/${config.coopname}/auth/invite?token=${token}`; + const inviteUrl = `${config.frontend_url}/${config.coopname}/auth/invite?token=${token}`; await this.notificationSenderService.sendNotificationToUser(data.username, Workflows.Invite.id, { inviteUrl }); diff --git a/components/controller/src/application/participant/services/participant-notification.service.ts b/components/controller/src/application/participant/services/participant-notification.service.ts index 09a56043b1e..2b363d9a057 100644 --- a/components/controller/src/application/participant/services/participant-notification.service.ts +++ b/components/controller/src/application/participant/services/participant-notification.service.ts @@ -104,7 +104,7 @@ export class ParticipantNotificationService implements OnModuleInit { paymentCurrency, paymentType: 'Вступительный и минимальный паевой взнос', coopname, - paymentUrl: `${config.base_url}`, + paymentUrl: `${config.frontend_url}`, }; // Отправляем уведомление diff --git a/components/controller/src/application/system/interactors/install.interactor.ts b/components/controller/src/application/system/interactors/install.interactor.ts index 9990bc1dd4d..8ce71fcaaf1 100644 --- a/components/controller/src/application/system/interactors/install.interactor.ts +++ b/components/controller/src/application/system/interactors/install.interactor.ts @@ -205,7 +205,7 @@ export class InstallInteractor { throw new Error(`Пользователь с email ${member.individual_data.email} не найден`); } const token = await this.tokenApplicationService.generateInviteToken(member.individual_data.email, user.id); - const inviteUrl = `${config.base_url}/${config.coopname}/auth/invite?token=${token}`; + const inviteUrl = `${config.frontend_url}/${config.coopname}/auth/invite?token=${token}`; const payload: Workflows.Invite.IPayload = { inviteUrl, diff --git a/components/controller/src/application/wallet/services/wallet-notification.service.ts b/components/controller/src/application/wallet/services/wallet-notification.service.ts index ba7d404d674..0e9d37450c3 100644 --- a/components/controller/src/application/wallet/services/wallet-notification.service.ts +++ b/components/controller/src/application/wallet/services/wallet-notification.service.ts @@ -64,7 +64,7 @@ export class WalletNotificationService implements OnModuleInit { paymentCurrency, paymentType: 'Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк"', coopname, - paymentUrl: `${config.base_url}`, + paymentUrl: `${config.frontend_url}`, }; // Отправляем уведомление diff --git a/components/controller/src/config/config.ts b/components/controller/src/config/config.ts index a08db49c3b8..0517c82874b 100644 --- a/components/controller/src/config/config.ts +++ b/components/controller/src/config/config.ts @@ -6,7 +6,14 @@ dotenv.config({ path: path.join(__dirname, '../../.env') }); const envVarsSchema = z.object({ NODE_ENV: z.enum(['production', 'development', 'test']), - BASE_URL: z.string(), + BACKEND_URL: z + .string() + .min(1) + .describe('Публичный базовый URL API (GraphQL/REST), без завершающего слэша; ICS-лента, интеграции'), + FRONTEND_URL: z + .string() + .min(1) + .describe('Публичный базовый URL рабочего стола (SPA); ссылки в письмах и deep links'), SERVER_SECRET: z.string(), PORT: z .string() @@ -149,7 +156,8 @@ if (!envVars.success) { // Экспорт настроек export default { env: envVars.data.NODE_ENV, - base_url: envVars.data.BASE_URL, + backend_url: envVars.data.BACKEND_URL, + frontend_url: envVars.data.FRONTEND_URL, port: envVars.data.PORT, server_secret: envVars.data.SERVER_SECRET, timezone: envVars.data.TIMEZONE, diff --git a/components/controller/src/extensions/1ccoop/oneccoop-extension.module.ts b/components/controller/src/extensions/1ccoop/oneccoop-extension.module.ts index 5a0de33839b..9de2c6f0133 100644 --- a/components/controller/src/extensions/1ccoop/oneccoop-extension.module.ts +++ b/components/controller/src/extensions/1ccoop/oneccoop-extension.module.ts @@ -48,7 +48,7 @@ function extractHostname(url: string): string { } // Извлекаем hostname из конфигурации -const hostname = extractHostname(config.base_url); +const hostname = extractHostname(config.backend_url); // Дефолтные параметры конфигурации export const defaultConfig = { diff --git a/components/controller/src/extensions/capital/application/services/generation.service.ts b/components/controller/src/extensions/capital/application/services/generation.service.ts index 1d5f98dc37c..d42707d4c21 100644 --- a/components/controller/src/extensions/capital/application/services/generation.service.ts +++ b/components/controller/src/extensions/capital/application/services/generation.service.ts @@ -275,7 +275,7 @@ export class GenerationService { return null; } const pathPrefix = project.isComponent() ? 'components' : 'projects'; - const baseUrl = config.base_url.replace(/\/$/, ''); + const baseUrl = config.frontend_url.replace(/\/$/, ''); const path = `/${encodeURIComponent(coopname)}/capital/${pathPrefix}/${encodeURIComponent(anchorProjectHash)}/requirements/${encodeURIComponent(story.story_hash)}`; const desktopUrl = `${baseUrl}/#${path}`; return [`${story.title}`, desktopUrl].join('\n'); diff --git a/components/controller/src/extensions/chairman/application/services/approval-notification.service.ts b/components/controller/src/extensions/chairman/application/services/approval-notification.service.ts index f02821c9d99..c9fc9d8337c 100644 --- a/components/controller/src/extensions/chairman/application/services/approval-notification.service.ts +++ b/components/controller/src/extensions/chairman/application/services/approval-notification.service.ts @@ -87,7 +87,7 @@ export class ApprovalNotificationService implements OnModuleInit { authorName, coopname: approvalData.coopname, approval_hash: approvalData.approval_hash, - approvalUrl: `${config.base_url}/${approvalData.coopname}/chairman/approvals`, + approvalUrl: `${config.frontend_url}/${approvalData.coopname}/chairman/approvals`, }; // Отправляем уведомление diff --git a/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts b/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts index 876e5454d7a..c926fda7ce0 100644 --- a/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts +++ b/components/controller/src/extensions/chairman/application/services/approval-response-notification.service.ts @@ -119,7 +119,7 @@ export class ApprovalResponseNotificationService implements OnModuleInit { approvalId: approvalHash, coopname: config.coopname, coopShortName, - approvalUrl: `${config.base_url}`, + approvalUrl: `${config.frontend_url}`, }; // Отправляем уведомление diff --git a/components/controller/src/extensions/chatcoop/application/services/chatcoop-calendar-application.service.ts b/components/controller/src/extensions/chatcoop/application/services/chatcoop-calendar-application.service.ts index 9228f3908dd..6199a16205f 100644 --- a/components/controller/src/extensions/chatcoop/application/services/chatcoop-calendar-application.service.ts +++ b/components/controller/src/extensions/chatcoop/application/services/chatcoop-calendar-application.service.ts @@ -144,8 +144,8 @@ export class ChatCoopCalendarApplicationService { const rawSecret = crypto.randomBytes(32).toString('hex'); const hash = sha256Hex(rawSecret); const sub = await this.icsSubs.rotateSecretForUser(coopUsername, hash); - const base = config.base_url.replace(/\/$/, ''); - return `${base}/v1/extensions/chatcoop/calendar/feed.ics?id=${encodeURIComponent(sub.id)}&secret=${encodeURIComponent(rawSecret)}`; + const apiBase = config.backend_url.replace(/\/$/, ''); + return `${apiBase}/v1/extensions/chatcoop/calendar/feed.ics?id=${encodeURIComponent(sub.id)}&secret=${encodeURIComponent(rawSecret)}`; } async buildIcsDocumentForSubscription(subscriptionId: string, rawSecret: string): Promise { @@ -158,7 +158,7 @@ export class ChatCoopCalendarApplicationService { } const all = await this.events.listAll(); const coopname = config.coopname; - const baseUrl = config.base_url.replace(/\/$/, ''); + const frontendBase = config.frontend_url.replace(/\/$/, ''); const lines: string[] = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', @@ -171,7 +171,7 @@ export class ChatCoopCalendarApplicationService { for (const ev of all) { const room = await this.managedRooms.findByMatrixRoomId(ev.matrixRoomId); const roomLabel = room?.displayLabel ?? ev.matrixRoomId; - const deepLink = `${baseUrl}/#/${coopname}/chatcoop/chat?matrix_room=${encodeURIComponent(ev.matrixRoomId)}`; + const deepLink = `${frontendBase}/#/${coopname}/chatcoop/chat?matrix_room=${encodeURIComponent(ev.matrixRoomId)}`; const descParts = [ ev.description ? escapeIcsText(ev.description) : '', escapeIcsText(`Комната: ${roomLabel}`), @@ -187,6 +187,8 @@ export class ChatCoopCalendarApplicationService { lines.push(`SEQUENCE:${ev.icsSequence}`); lines.push(foldIcsLine(`SUMMARY:${escapeIcsText(ev.title)}`)); lines.push(foldIcsLine(`DESCRIPTION:${description}`)); + // Без VALARM; явно отключаем дефолтное напоминание клиента (часто «за 30 мин») в Apple Calendar. + lines.push('X-APPLE-DEFAULT-ALARM-COMPONENTS:NONE'); lines.push('END:VEVENT'); } lines.push('END:VCALENDAR'); diff --git a/components/controller/src/extensions/participant/meet-workflow-notification.service.ts b/components/controller/src/extensions/participant/meet-workflow-notification.service.ts index 27c42889104..f559fd565dc 100644 --- a/components/controller/src/extensions/participant/meet-workflow-notification.service.ts +++ b/components/controller/src/extensions/participant/meet-workflow-notification.service.ts @@ -48,7 +48,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit { // Формирование URL для уведомлений private getNotificationUrl(meet: TrackedMeet): string { - return `${config.base_url}/${meet.coopname}/user/meets/${meet.hash.toUpperCase()}`; + return `${config.frontend_url}/${meet.coopname}/user/meets/${meet.hash.toUpperCase()}`; } // Форматирование сообщения о часовом поясе diff --git a/components/controller/src/extensions/participant/notification-sender.service.ts b/components/controller/src/extensions/participant/notification-sender.service.ts index b5a598b9818..d874bce2808 100644 --- a/components/controller/src/extensions/participant/notification-sender.service.ts +++ b/components/controller/src/extensions/participant/notification-sender.service.ts @@ -81,7 +81,7 @@ export class NotificationSenderService { // Формирование URL для уведомлений private getNotificationUrl(meet: TrackedMeet): string { - return `${config.base_url}/${meet.coopname}/user/meets/${meet.hash.toUpperCase()}`; + return `${config.frontend_url}/${meet.coopname}/user/meets/${meet.hash.toUpperCase()}`; } // Форматирование сообщения о часовом поясе diff --git a/components/desktop/extensions/chatcoop/install.ts b/components/desktop/extensions/chatcoop/install.ts index 63df995a16e..a33cbfa3094 100644 --- a/components/desktop/extensions/chatcoop/install.ts +++ b/components/desktop/extensions/chatcoop/install.ts @@ -35,12 +35,12 @@ export default async function (): Promise { children: [], }, { - path: 'calendar', - name: 'chatcoop-calendar', - component: markRaw(CalendarPage), + path: 'mobile', + name: 'chatcoop-mobile', + component: markRaw(MobileClientPage), meta: { - title: 'Календарь событий', - icon: 'fa-solid fa-calendar-days', + title: 'Мобильный клиент', + icon: 'fa-solid fa-mobile-alt', roles: ['chairman', 'member', 'user'], agreements: agreementsBase, requiresAuth: true, @@ -48,12 +48,12 @@ export default async function (): Promise { children: [], }, { - path: 'mobile', - name: 'chatcoop-mobile', - component: markRaw(MobileClientPage), + path: 'calendar', + name: 'chatcoop-calendar', + component: markRaw(CalendarPage), meta: { - title: 'Мобильный клиент', - icon: 'fa-solid fa-mobile-alt', + title: 'Календарь событий', + icon: 'fa-solid fa-calendar-days', roles: ['chairman', 'member', 'user'], agreements: agreementsBase, requiresAuth: true,