calendar of events

This commit is contained in:
Alex Ant
2026-03-29 17:50:44 +05:00
parent 48ecb49a59
commit 646b1c90a2
20 changed files with 47 additions and 34 deletions
+1
View File
@@ -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`
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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`
@@ -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`,
};
// Отправляем уведомления каждому члену совета в цикле
@@ -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}`,
};
// Отправляем уведомление
@@ -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<void> {
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,
@@ -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: точную ссылку потом
};
// Отправляем уведомление
@@ -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 });
@@ -104,7 +104,7 @@ export class ParticipantNotificationService implements OnModuleInit {
paymentCurrency,
paymentType: 'Вступительный и минимальный паевой взнос',
coopname,
paymentUrl: `${config.base_url}`,
paymentUrl: `${config.frontend_url}`,
};
// Отправляем уведомление
@@ -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,
@@ -64,7 +64,7 @@ export class WalletNotificationService implements OnModuleInit {
paymentCurrency,
paymentType: 'Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк"',
coopname,
paymentUrl: `${config.base_url}`,
paymentUrl: `${config.frontend_url}`,
};
// Отправляем уведомление
+10 -2
View File
@@ -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,
@@ -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 = {
@@ -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');
@@ -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`,
};
// Отправляем уведомление
@@ -119,7 +119,7 @@ export class ApprovalResponseNotificationService implements OnModuleInit {
approvalId: approvalHash,
coopname: config.coopname,
coopShortName,
approvalUrl: `${config.base_url}`,
approvalUrl: `${config.frontend_url}`,
};
// Отправляем уведомление
@@ -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<string | null> {
@@ -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');
@@ -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()}`;
}
// Форматирование сообщения о часовом поясе
@@ -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()}`;
}
// Форматирование сообщения о часовом поясе
@@ -35,12 +35,12 @@ export default async function (): Promise<IWorkspaceConfig[]> {
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<IWorkspaceConfig[]> {
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,