calendar of events
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
+1
-1
@@ -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`,
|
||||
};
|
||||
|
||||
// Отправляем уведомления каждому члену совета в цикле
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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: точную ссылку потом
|
||||
};
|
||||
|
||||
// Отправляем уведомление
|
||||
|
||||
+1
-1
@@ -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 });
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export class WalletNotificationService implements OnModuleInit {
|
||||
paymentCurrency,
|
||||
paymentType: 'Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк"',
|
||||
coopname,
|
||||
paymentUrl: `${config.base_url}`,
|
||||
paymentUrl: `${config.frontend_url}`,
|
||||
};
|
||||
|
||||
// Отправляем уведомление
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
+1
-1
@@ -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`,
|
||||
};
|
||||
|
||||
// Отправляем уведомление
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ export class ApprovalResponseNotificationService implements OnModuleInit {
|
||||
approvalId: approvalHash,
|
||||
coopname: config.coopname,
|
||||
coopShortName,
|
||||
approvalUrl: `${config.base_url}`,
|
||||
approvalUrl: `${config.frontend_url}`,
|
||||
};
|
||||
|
||||
// Отправляем уведомление
|
||||
|
||||
+6
-4
@@ -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');
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user