calendar of events
This commit is contained in:
@@ -58,6 +58,7 @@ pnpm run reboot
|
|||||||
- Controller/Parser (в Docker): хосты по именам контейнеров из docker-compose (порт БД 5432)
|
- Controller/Parser (в Docker): хосты по именам контейнеров из docker-compose (порт БД 5432)
|
||||||
- Boot (на хосте): `127.0.0.1`, PG порт `5532`, mongo через `/etc/hosts`
|
- Boot (на хосте): `127.0.0.1`, PG порт `5532`, mongo через `/etc/hosts`
|
||||||
- Desktop: `127.0.0.1`
|
- 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` после старта ноды
|
- **CHAIN_ID**: берётся из `curl http://localhost:8888/v1/chain/get_info` после старта ноды
|
||||||
- Controller требует `VAPID_PUBLIC_KEY` и `VAPID_PRIVATE_KEY`
|
- Controller требует `VAPID_PUBLIC_KEY` и `VAPID_PRIVATE_KEY`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# Заменить на адрес домена (используйтся в ссылке для восстановления доступа)
|
# Публичный URL контроллера (API): на деве обычно порт 2998; на проде может быть https://домен/backend
|
||||||
BASE_URL=http://localhost:3005
|
BACKEND_URL=http://127.0.0.1:2998
|
||||||
|
# Публичный URL рабочего стола (SPA): письма, приглашения, deep links в ICS
|
||||||
|
FRONTEND_URL=http://127.0.0.1:2999
|
||||||
|
|
||||||
# Port number
|
# Port number
|
||||||
PORT=2998
|
PORT=2998
|
||||||
|
|||||||
@@ -709,7 +709,7 @@ this.providerPort.registerProvider(this.name, this);
|
|||||||
Все переменные валидируются Zod-схемой в `src/config/config.ts`. При ошибке — процесс завершается.
|
Все переменные валидируются Zod-схемой в `src/config/config.ts`. При ошибке — процесс завершается.
|
||||||
|
|
||||||
**Обязательные**:
|
**Обязательные**:
|
||||||
- `NODE_ENV`, `BASE_URL`, `SERVER_SECRET`, `PORT`
|
- `NODE_ENV`, `BACKEND_URL`, `FRONTEND_URL`, `SERVER_SECRET`, `PORT`
|
||||||
- `MONGODB_URL`, `COOPNAME`
|
- `MONGODB_URL`, `COOPNAME`
|
||||||
- `JWT_SECRET`
|
- `JWT_SECRET`
|
||||||
- Переменные PG-подключения: `HOST, PORT, USER, PASS, DB`
|
- Переменные PG-подключения: `HOST, PORT, USER, PASS, DB`
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ export class AgendaNotificationService implements OnModuleInit {
|
|||||||
itemDescription: ``,
|
itemDescription: ``,
|
||||||
authorName,
|
authorName,
|
||||||
decision_id: agendaId,
|
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}`,
|
decisionTitle: `Решение №${decisionId}`,
|
||||||
coopname: action.coopname,
|
coopname: action.coopname,
|
||||||
decision_id: decisionId,
|
decision_id: decisionId,
|
||||||
decisionUrl: `${config.base_url}`,
|
decisionUrl: `${config.frontend_url}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправляем уведомление
|
// Отправляем уведомление
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class AuthInteractor {
|
|||||||
throw new Error('User not found');
|
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 });
|
await this.notificationSenderService.sendNotificationToUser(user.username, Workflows.ResetKey.id, { resetUrl });
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export class AuthInteractor {
|
|||||||
async sendVerificationEmail(username: string): Promise<void> {
|
async sendVerificationEmail(username: string): Promise<void> {
|
||||||
const user = await this.userDomainService.getUserByUsername(username);
|
const user = await this.userDomainService.getUserByUsername(username);
|
||||||
const verifyEmailToken = await this.tokenApplicationService.generateVerifyEmailToken(user.id);
|
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, {
|
await this.notificationSenderService.sendNotificationToUser(user.username, Workflows.EmailVerification.id, {
|
||||||
verificationUrl,
|
verificationUrl,
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ export class PaymentNotificationService implements OnModuleInit {
|
|||||||
paymentAmount: payment.quantity.toFixed(2),
|
paymentAmount: payment.quantity.toFixed(2),
|
||||||
paymentCurrency: payment.symbol,
|
paymentCurrency: payment.symbol,
|
||||||
paymentDate: payment.created_at.toLocaleString('ru-RU'),
|
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, 'Пользователь не найден');
|
throw new HttpApiError(http.NOT_FOUND, 'Пользователь не найден');
|
||||||
}
|
}
|
||||||
const token = await this.tokenApplicationService.generateInviteToken(data.email, user.id);
|
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 });
|
await this.notificationSenderService.sendNotificationToUser(data.username, Workflows.Invite.id, { inviteUrl });
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -104,7 +104,7 @@ export class ParticipantNotificationService implements OnModuleInit {
|
|||||||
paymentCurrency,
|
paymentCurrency,
|
||||||
paymentType: 'Вступительный и минимальный паевой взнос',
|
paymentType: 'Вступительный и минимальный паевой взнос',
|
||||||
coopname,
|
coopname,
|
||||||
paymentUrl: `${config.base_url}`,
|
paymentUrl: `${config.frontend_url}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправляем уведомление
|
// Отправляем уведомление
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export class InstallInteractor {
|
|||||||
throw new Error(`Пользователь с email ${member.individual_data.email} не найден`);
|
throw new Error(`Пользователь с email ${member.individual_data.email} не найден`);
|
||||||
}
|
}
|
||||||
const token = await this.tokenApplicationService.generateInviteToken(member.individual_data.email, user.id);
|
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 = {
|
const payload: Workflows.Invite.IPayload = {
|
||||||
inviteUrl,
|
inviteUrl,
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ export class WalletNotificationService implements OnModuleInit {
|
|||||||
paymentCurrency,
|
paymentCurrency,
|
||||||
paymentType: 'Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк"',
|
paymentType: 'Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк"',
|
||||||
coopname,
|
coopname,
|
||||||
paymentUrl: `${config.base_url}`,
|
paymentUrl: `${config.frontend_url}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Отправляем уведомление
|
// Отправляем уведомление
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ dotenv.config({ path: path.join(__dirname, '../../.env') });
|
|||||||
|
|
||||||
const envVarsSchema = z.object({
|
const envVarsSchema = z.object({
|
||||||
NODE_ENV: z.enum(['production', 'development', 'test']),
|
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(),
|
SERVER_SECRET: z.string(),
|
||||||
PORT: z
|
PORT: z
|
||||||
.string()
|
.string()
|
||||||
@@ -149,7 +156,8 @@ if (!envVars.success) {
|
|||||||
// Экспорт настроек
|
// Экспорт настроек
|
||||||
export default {
|
export default {
|
||||||
env: envVars.data.NODE_ENV,
|
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,
|
port: envVars.data.PORT,
|
||||||
server_secret: envVars.data.SERVER_SECRET,
|
server_secret: envVars.data.SERVER_SECRET,
|
||||||
timezone: envVars.data.TIMEZONE,
|
timezone: envVars.data.TIMEZONE,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function extractHostname(url: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Извлекаем hostname из конфигурации
|
// Извлекаем hostname из конфигурации
|
||||||
const hostname = extractHostname(config.base_url);
|
const hostname = extractHostname(config.backend_url);
|
||||||
|
|
||||||
// Дефолтные параметры конфигурации
|
// Дефолтные параметры конфигурации
|
||||||
export const defaultConfig = {
|
export const defaultConfig = {
|
||||||
|
|||||||
+1
-1
@@ -275,7 +275,7 @@ export class GenerationService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const pathPrefix = project.isComponent() ? 'components' : 'projects';
|
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 path = `/${encodeURIComponent(coopname)}/capital/${pathPrefix}/${encodeURIComponent(anchorProjectHash)}/requirements/${encodeURIComponent(story.story_hash)}`;
|
||||||
const desktopUrl = `${baseUrl}/#${path}`;
|
const desktopUrl = `${baseUrl}/#${path}`;
|
||||||
return [`${story.title}`, desktopUrl].join('\n');
|
return [`${story.title}`, desktopUrl].join('\n');
|
||||||
|
|||||||
+1
-1
@@ -87,7 +87,7 @@ export class ApprovalNotificationService implements OnModuleInit {
|
|||||||
authorName,
|
authorName,
|
||||||
coopname: approvalData.coopname,
|
coopname: approvalData.coopname,
|
||||||
approval_hash: approvalData.approval_hash,
|
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,
|
approvalId: approvalHash,
|
||||||
coopname: config.coopname,
|
coopname: config.coopname,
|
||||||
coopShortName,
|
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 rawSecret = crypto.randomBytes(32).toString('hex');
|
||||||
const hash = sha256Hex(rawSecret);
|
const hash = sha256Hex(rawSecret);
|
||||||
const sub = await this.icsSubs.rotateSecretForUser(coopUsername, hash);
|
const sub = await this.icsSubs.rotateSecretForUser(coopUsername, hash);
|
||||||
const base = config.base_url.replace(/\/$/, '');
|
const apiBase = config.backend_url.replace(/\/$/, '');
|
||||||
return `${base}/v1/extensions/chatcoop/calendar/feed.ics?id=${encodeURIComponent(sub.id)}&secret=${encodeURIComponent(rawSecret)}`;
|
return `${apiBase}/v1/extensions/chatcoop/calendar/feed.ics?id=${encodeURIComponent(sub.id)}&secret=${encodeURIComponent(rawSecret)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildIcsDocumentForSubscription(subscriptionId: string, rawSecret: string): Promise<string | null> {
|
async buildIcsDocumentForSubscription(subscriptionId: string, rawSecret: string): Promise<string | null> {
|
||||||
@@ -158,7 +158,7 @@ export class ChatCoopCalendarApplicationService {
|
|||||||
}
|
}
|
||||||
const all = await this.events.listAll();
|
const all = await this.events.listAll();
|
||||||
const coopname = config.coopname;
|
const coopname = config.coopname;
|
||||||
const baseUrl = config.base_url.replace(/\/$/, '');
|
const frontendBase = config.frontend_url.replace(/\/$/, '');
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
'BEGIN:VCALENDAR',
|
'BEGIN:VCALENDAR',
|
||||||
'VERSION:2.0',
|
'VERSION:2.0',
|
||||||
@@ -171,7 +171,7 @@ export class ChatCoopCalendarApplicationService {
|
|||||||
for (const ev of all) {
|
for (const ev of all) {
|
||||||
const room = await this.managedRooms.findByMatrixRoomId(ev.matrixRoomId);
|
const room = await this.managedRooms.findByMatrixRoomId(ev.matrixRoomId);
|
||||||
const roomLabel = room?.displayLabel ?? 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 = [
|
const descParts = [
|
||||||
ev.description ? escapeIcsText(ev.description) : '',
|
ev.description ? escapeIcsText(ev.description) : '',
|
||||||
escapeIcsText(`Комната: ${roomLabel}`),
|
escapeIcsText(`Комната: ${roomLabel}`),
|
||||||
@@ -187,6 +187,8 @@ export class ChatCoopCalendarApplicationService {
|
|||||||
lines.push(`SEQUENCE:${ev.icsSequence}`);
|
lines.push(`SEQUENCE:${ev.icsSequence}`);
|
||||||
lines.push(foldIcsLine(`SUMMARY:${escapeIcsText(ev.title)}`));
|
lines.push(foldIcsLine(`SUMMARY:${escapeIcsText(ev.title)}`));
|
||||||
lines.push(foldIcsLine(`DESCRIPTION:${description}`));
|
lines.push(foldIcsLine(`DESCRIPTION:${description}`));
|
||||||
|
// Без VALARM; явно отключаем дефолтное напоминание клиента (часто «за 30 мин») в Apple Calendar.
|
||||||
|
lines.push('X-APPLE-DEFAULT-ALARM-COMPONENTS:NONE');
|
||||||
lines.push('END:VEVENT');
|
lines.push('END:VEVENT');
|
||||||
}
|
}
|
||||||
lines.push('END:VCALENDAR');
|
lines.push('END:VCALENDAR');
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export class MeetWorkflowNotificationService implements OnModuleInit {
|
|||||||
|
|
||||||
// Формирование URL для уведомлений
|
// Формирование URL для уведомлений
|
||||||
private getNotificationUrl(meet: TrackedMeet): string {
|
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 для уведомлений
|
// Формирование URL для уведомлений
|
||||||
private getNotificationUrl(meet: TrackedMeet): string {
|
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: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'calendar',
|
path: 'mobile',
|
||||||
name: 'chatcoop-calendar',
|
name: 'chatcoop-mobile',
|
||||||
component: markRaw(CalendarPage),
|
component: markRaw(MobileClientPage),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Календарь событий',
|
title: 'Мобильный клиент',
|
||||||
icon: 'fa-solid fa-calendar-days',
|
icon: 'fa-solid fa-mobile-alt',
|
||||||
roles: ['chairman', 'member', 'user'],
|
roles: ['chairman', 'member', 'user'],
|
||||||
agreements: agreementsBase,
|
agreements: agreementsBase,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
@@ -48,12 +48,12 @@ export default async function (): Promise<IWorkspaceConfig[]> {
|
|||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mobile',
|
path: 'calendar',
|
||||||
name: 'chatcoop-mobile',
|
name: 'chatcoop-calendar',
|
||||||
component: markRaw(MobileClientPage),
|
component: markRaw(CalendarPage),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Мобильный клиент',
|
title: 'Календарь событий',
|
||||||
icon: 'fa-solid fa-mobile-alt',
|
icon: 'fa-solid fa-calendar-days',
|
||||||
roles: ['chairman', 'member', 'user'],
|
roles: ['chairman', 'member', 'user'],
|
||||||
agreements: agreementsBase,
|
agreements: agreementsBase,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user