Compare commits
1 Commits
refactoring
...
element
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b922b00c1 |
@@ -92,6 +92,11 @@ const envVarsSchema = z.object({
|
||||
.string()
|
||||
.default('4')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
|
||||
// Параметры Matrix
|
||||
MATRIX_HOMESERVER_URL: z.string().default('https://matrix.coopenomics.world'),
|
||||
MATRIX_ADMIN_USERNAME: z.string(),
|
||||
MATRIX_ADMIN_PASSWORD: z.string(),
|
||||
});
|
||||
|
||||
// Валидация переменных окружения
|
||||
@@ -177,4 +182,9 @@ export default {
|
||||
private_key: envVars.data.VAPID_PRIVATE_KEY,
|
||||
subject: envVars.data.VAPID_SUBJECT,
|
||||
},
|
||||
matrix: {
|
||||
homeserver_url: envVars.data.MATRIX_HOMESERVER_URL,
|
||||
admin_username: envVars.data.MATRIX_ADMIN_USERNAME,
|
||||
admin_password: envVars.data.MATRIX_ADMIN_PASSWORD,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Как установить Coopgram?
|
||||
|
||||
## Простая установка
|
||||
|
||||
🎯 **Для установки Coopgram нужно всего одно действие:**
|
||||
|
||||
1. **Найдите расширение** в списке доступных расширений системы
|
||||
2. **Нажмите кнопку "Установить"**
|
||||
3. **Готово!** Расширение заработает автоматически
|
||||
|
||||
## Что произойдет после установки?
|
||||
|
||||
✨ **Система автоматически:**
|
||||
- Подключится к серверу коммуникаций
|
||||
- Создаст аккаунты для всех участников кооператива
|
||||
- Настроит все необходимые права доступа
|
||||
|
||||
## Для участников кооператива
|
||||
|
||||
👥 **Вам не нужно ничего делать дополнительно!**
|
||||
|
||||
После установки расширения:
|
||||
- В вашем личном кабинете появится раздел "Коммуникации"
|
||||
- Вы сможете сразу начать общаться с другими участниками
|
||||
- Все сообщения будут защищены и конфиденциальны
|
||||
|
||||
## Поддержка
|
||||
|
||||
Если возникнут вопросы, обратитесь к администратору системы. Установка максимально простая и не требует специальных знаний!
|
||||
@@ -0,0 +1,35 @@
|
||||
# Coopgram - Коммуникации кооператива
|
||||
|
||||
Современная система общения для членов кооператива на базе Matrix протокола.
|
||||
|
||||
## Что это такое?
|
||||
|
||||
Coopgram позволяет всем членам кооператива общаться в единой защищенной системе. Это как современный мессенджер, но специально настроенный для нужд кооператива.
|
||||
|
||||
## Возможности
|
||||
|
||||
✨ **Удобное общение** - групповые чаты, личные сообщения, голосовые звонки
|
||||
🔐 **Безопасность** - все сообщения зашифрованы и защищены
|
||||
👥 **Для всех участников** - каждый член кооператива автоматически получает доступ
|
||||
📱 **На всех устройствах** - работает на компьютере, телефоне и планшете
|
||||
⚡ **Быстрая установка** - просто установите расширение, и оно заработает
|
||||
|
||||
## Как это работает?
|
||||
|
||||
1. **Установка** - расширение устанавливается одним нажатием
|
||||
2. **Автоматическая регистрация** - система сама создаст аккаунт для каждого участника
|
||||
3. **Готово к работе** - можно сразу начинать общаться
|
||||
|
||||
## Для чего это нужно?
|
||||
|
||||
- Обсуждение рабочих вопросов
|
||||
- Координация проектов
|
||||
- Обмен важной информацией
|
||||
- Быстрое решение вопросов между участниками
|
||||
|
||||
## Преимущества
|
||||
|
||||
- **Надежность** - сообщения не теряются, история сохраняется
|
||||
- **Конфиденциальность** - только участники кооператива имеют доступ
|
||||
- **Простота** - не нужно настраивать ничего дополнительно
|
||||
- **Современность** - все возможности современных мессенджеров
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class IframeTokenResponseDTO {
|
||||
@Field()
|
||||
token!: string;
|
||||
|
||||
@Field()
|
||||
expiresAt!: Date;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Resolver, Query } from '@nestjs/graphql';
|
||||
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { CurrentUser } from '~/modules/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { CoopgramApplicationService } from '../services/coopgram-application.service';
|
||||
import { IframeTokenResponseDTO } from '../dto/get-iframe-token.dto';
|
||||
|
||||
@Resolver('matrix')
|
||||
export class CoopgramResolver {
|
||||
constructor(private readonly coopgramAppService: CoopgramApplicationService) {}
|
||||
|
||||
@Query(() => IframeTokenResponseDTO, {
|
||||
name: 'getToken',
|
||||
description: 'Получить токен для входа в Matrix через iframe',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getMatrixToken(@CurrentUser() currentUser: MonoAccountDomainInterface): Promise<IframeTokenResponseDTO> {
|
||||
return this.coopgramAppService.getIframeToken(currentUser.username);
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MatrixUserManagementService } from '../../domain/services/matrix-user-management.service';
|
||||
import { MatrixTokenManagementService } from '../../domain/services/matrix-token-management.service';
|
||||
import { MatrixApiService } from './matrix-api.service';
|
||||
import { MatrixUserDomainEntity } from '../../domain/entities/matrix-user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CoopgramApplicationService {
|
||||
constructor(
|
||||
private readonly matrixUserManagementService: MatrixUserManagementService,
|
||||
private readonly matrixTokenManagementService: MatrixTokenManagementService,
|
||||
private readonly matrixApiService: MatrixApiService
|
||||
) {}
|
||||
|
||||
async getIframeToken(coopUsername: string): Promise<{ token: string; expiresAt: Date }> {
|
||||
// Проверяем, существует ли пользователь Matrix
|
||||
let matrixUser = await this.matrixUserManagementService.getMatrixUserByCoopUsername(coopUsername);
|
||||
|
||||
if (!matrixUser) {
|
||||
// Регистрируем пользователя в Matrix
|
||||
matrixUser = await this.registerUserInMatrix(coopUsername);
|
||||
}
|
||||
|
||||
// Генерируем токен для iframe входа
|
||||
const tokenEntity = await this.matrixTokenManagementService.generateToken(coopUsername, matrixUser.matrixUserId);
|
||||
|
||||
return {
|
||||
token: tokenEntity.token,
|
||||
expiresAt: tokenEntity.expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
async validateIframeToken(token: string): Promise<{ coopUsername: string; matrixUserId: string } | null> {
|
||||
const tokenEntity = await this.matrixTokenManagementService.validateToken(token);
|
||||
|
||||
if (!tokenEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
coopUsername: tokenEntity.coopUsername,
|
||||
matrixUserId: tokenEntity.matrixUserId,
|
||||
};
|
||||
}
|
||||
|
||||
private async registerUserInMatrix(coopUsername: string): Promise<MatrixUserDomainEntity> {
|
||||
// Генерируем уникальный пароль для Matrix
|
||||
const matrixPassword = this.generateMatrixPassword(coopUsername);
|
||||
const matrixUsername = coopUsername.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
||||
|
||||
// Регистрируем пользователя в Matrix через API
|
||||
const registerResponse = await this.matrixApiService.registerUser(matrixUsername, matrixPassword);
|
||||
|
||||
// Сохраняем информацию о пользователе
|
||||
return this.matrixUserManagementService.createMatrixUser({
|
||||
coopUsername,
|
||||
matrixUserId: registerResponse.user_id,
|
||||
matrixUsername,
|
||||
matrixAccessToken: registerResponse.access_token,
|
||||
matrixDeviceId: registerResponse.device_id,
|
||||
matrixHomeServer: registerResponse.home_server,
|
||||
});
|
||||
}
|
||||
|
||||
private generateMatrixPassword(username: string): string {
|
||||
// Генерируем сложный пароль на основе username и текущего времени
|
||||
const timestamp = Date.now().toString();
|
||||
return `Coop${username}${timestamp}Matrix!`;
|
||||
}
|
||||
|
||||
async getMatrixUserInfo(coopUsername: string): Promise<MatrixUserDomainEntity | null> {
|
||||
return this.matrixUserManagementService.getMatrixUserByCoopUsername(coopUsername);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import config from '~/config/config';
|
||||
|
||||
interface MatrixLoginResponse {
|
||||
user_id: string;
|
||||
access_token: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}
|
||||
|
||||
interface MatrixRegisterResponse {
|
||||
user_id: string;
|
||||
access_token: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MatrixApiService {
|
||||
private readonly logger = new Logger(MatrixApiService.name);
|
||||
private readonly homeserverUrl: string;
|
||||
private readonly adminUsername: string;
|
||||
private readonly adminPassword: string;
|
||||
private adminAccessToken: string | null = null;
|
||||
private tokenExpiry: Date | null = null;
|
||||
private readonly httpClient: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.homeserverUrl = config.matrix.homeserver_url;
|
||||
this.adminUsername = config.matrix.admin_username;
|
||||
this.adminPassword = config.matrix.admin_password;
|
||||
this.httpClient = axios.create({
|
||||
baseURL: this.homeserverUrl,
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async loginAdmin(): Promise<string> {
|
||||
try {
|
||||
// Проверяем, действителен ли текущий токен
|
||||
if (this.adminAccessToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
||||
if (!this.adminAccessToken) {
|
||||
throw new Error('Admin access token is null');
|
||||
}
|
||||
return this.adminAccessToken;
|
||||
}
|
||||
|
||||
const response = await this.httpClient.post<MatrixLoginResponse>('/_matrix/client/r0/login', {
|
||||
type: 'm.login.password',
|
||||
user: this.adminUsername,
|
||||
password: this.adminPassword,
|
||||
});
|
||||
|
||||
this.adminAccessToken = response.data.access_token;
|
||||
// Токен Matrix обычно действителен долго, но будем обновлять каждые 24 часа для безопасности
|
||||
this.tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
|
||||
this.logger.log('Admin logged in to Matrix successfully');
|
||||
return this.adminAccessToken;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to login admin to Matrix', error);
|
||||
throw new Error('Не удалось войти в Matrix как администратор');
|
||||
}
|
||||
}
|
||||
|
||||
async registerUser(username: string, password: string): Promise<MatrixRegisterResponse> {
|
||||
try {
|
||||
const adminToken = await this.loginAdmin();
|
||||
|
||||
const response = await this.httpClient.post<MatrixRegisterResponse>(
|
||||
'/_matrix/client/r0/register',
|
||||
{
|
||||
username,
|
||||
password,
|
||||
auth: {
|
||||
type: 'm.login.password',
|
||||
user: this.adminUsername,
|
||||
password: this.adminPassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.logger.log(`User ${username} registered in Matrix successfully`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to register user ${username} in Matrix`, error);
|
||||
throw new Error('Не удалось зарегистрировать пользователя в Matrix');
|
||||
}
|
||||
}
|
||||
|
||||
async loginUser(username: string, password: string): Promise<MatrixLoginResponse> {
|
||||
try {
|
||||
const response = await this.httpClient.post<MatrixLoginResponse>('/_matrix/client/r0/login', {
|
||||
type: 'm.login.password',
|
||||
user: username,
|
||||
password: password,
|
||||
});
|
||||
|
||||
this.logger.log(`User ${username} logged in to Matrix successfully`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to login user ${username} to Matrix`, error);
|
||||
throw new Error('Не удалось войти в Matrix');
|
||||
}
|
||||
}
|
||||
|
||||
getHomeserverUrl(): string {
|
||||
return this.homeserverUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BaseExtModule } from '../base.extension.module';
|
||||
import { CoopgramDatabaseModule } from './infrastructure/database/coopgram-database.module';
|
||||
import { CoopgramApplicationService } from './application/services/coopgram-application.service';
|
||||
import { MatrixApiService } from './application/services/matrix-api.service';
|
||||
import { MatrixUserManagementService } from './domain/services/matrix-user-management.service';
|
||||
import { MatrixTokenManagementService } from './domain/services/matrix-token-management.service';
|
||||
import { CoopgramResolver } from './application/resolvers/coopgram.resolver';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { WinstonLoggerService } from '~/modules/logger/logger-app.service';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Пустая схема для расширения без конфигурации
|
||||
export const Schema = z.object({});
|
||||
|
||||
// Репозитории
|
||||
import { MatrixUserTypeormRepository } from './infrastructure/repositories/matrix-user.typeorm-repository';
|
||||
import { MatrixTokenTypeormRepository } from './infrastructure/repositories/matrix-token.typeorm-repository';
|
||||
|
||||
// Символы для DI
|
||||
import { MATRIX_USER_REPOSITORY } from './domain/repositories/matrix-user.repository';
|
||||
import { MATRIX_TOKEN_REPOSITORY } from './domain/repositories/matrix-token.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CoopgramPlugin extends BaseExtModule {
|
||||
constructor(private readonly logger: WinstonLoggerService, private readonly matrixApiService: MatrixApiService) {
|
||||
super();
|
||||
this.logger.setContext(CoopgramPlugin.name);
|
||||
}
|
||||
|
||||
name = 'coopgram';
|
||||
plugin!: any;
|
||||
configSchemas = Schema;
|
||||
defaultConfig = {};
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
try {
|
||||
this.logger.log('Coopgram module initializing...');
|
||||
|
||||
// Выполняем логин администратора при инициализации
|
||||
await this.matrixApiService.loginAdmin();
|
||||
|
||||
this.logger.log('Coopgram module initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Coopgram module', JSON.stringify(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [CoopgramDatabaseModule, ConfigModule],
|
||||
providers: [
|
||||
// Plugin
|
||||
CoopgramPlugin,
|
||||
|
||||
// Application Services
|
||||
CoopgramApplicationService,
|
||||
MatrixApiService,
|
||||
|
||||
// Domain Services
|
||||
MatrixUserManagementService,
|
||||
MatrixTokenManagementService,
|
||||
|
||||
// Repositories
|
||||
{
|
||||
provide: MATRIX_USER_REPOSITORY,
|
||||
useClass: MatrixUserTypeormRepository,
|
||||
},
|
||||
{
|
||||
provide: MATRIX_TOKEN_REPOSITORY,
|
||||
useClass: MatrixTokenTypeormRepository,
|
||||
},
|
||||
|
||||
// GraphQL Resolvers
|
||||
CoopgramResolver,
|
||||
],
|
||||
exports: [CoopgramPlugin],
|
||||
})
|
||||
export class CoopgramPluginModule {
|
||||
constructor(private readonly coopgramPlugin: CoopgramPlugin) {}
|
||||
|
||||
async initialize() {
|
||||
await this.coopgramPlugin.initialize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface MatrixTokenDomainEntity {
|
||||
id: string;
|
||||
coopUsername: string;
|
||||
matrixUserId: string;
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
isUsed: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface MatrixUserDomainEntity {
|
||||
id: string;
|
||||
coopUsername: string; // username из кооператива
|
||||
matrixUserId: string; // @username:homeserver
|
||||
matrixUsername: string; // username без @
|
||||
matrixAccessToken: string;
|
||||
matrixDeviceId: string;
|
||||
matrixHomeServer: string;
|
||||
isRegistered: boolean;
|
||||
lastTokenRefresh: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export enum MatrixUserStatus {
|
||||
REGISTERED = 'registered',
|
||||
PENDING = 'pending',
|
||||
BLOCKED = 'blocked',
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { MatrixTokenDomainEntity } from '../entities/matrix-token.entity';
|
||||
|
||||
export interface MatrixTokenRepository {
|
||||
create(token: Omit<MatrixTokenDomainEntity, 'id' | 'createdAt'>): Promise<MatrixTokenDomainEntity>;
|
||||
findByToken(token: string): Promise<MatrixTokenDomainEntity | null>;
|
||||
findByCoopUsername(coopUsername: string): Promise<MatrixTokenDomainEntity[]>;
|
||||
findValidByCoopUsername(coopUsername: string): Promise<MatrixTokenDomainEntity | null>;
|
||||
markAsUsed(id: string): Promise<void>;
|
||||
deleteExpired(): Promise<void>;
|
||||
findById(id: string): Promise<MatrixTokenDomainEntity | null>;
|
||||
}
|
||||
|
||||
export const MATRIX_TOKEN_REPOSITORY = Symbol('MatrixTokenRepository');
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { MatrixUserDomainEntity } from '../entities/matrix-user.entity';
|
||||
|
||||
export interface MatrixUserRepository {
|
||||
create(user: Omit<MatrixUserDomainEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<MatrixUserDomainEntity>;
|
||||
findByCoopUsername(coopUsername: string): Promise<MatrixUserDomainEntity | null>;
|
||||
findByMatrixUserId(matrixUserId: string): Promise<MatrixUserDomainEntity | null>;
|
||||
findById(id: string): Promise<MatrixUserDomainEntity | null>;
|
||||
update(id: string, user: Partial<MatrixUserDomainEntity>): Promise<MatrixUserDomainEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
findAll(): Promise<MatrixUserDomainEntity[]>;
|
||||
}
|
||||
|
||||
export const MATRIX_USER_REPOSITORY = Symbol('MatrixUserRepository');
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MatrixTokenDomainEntity } from '../entities/matrix-token.entity';
|
||||
import { MATRIX_TOKEN_REPOSITORY, MatrixTokenRepository } from '../repositories/matrix-token.repository';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class MatrixTokenManagementService {
|
||||
constructor(@Inject(MATRIX_TOKEN_REPOSITORY) private readonly matrixTokenRepository: MatrixTokenRepository) {}
|
||||
|
||||
async generateToken(coopUsername: string, matrixUserId: string): Promise<MatrixTokenDomainEntity> {
|
||||
// Удаляем просроченные токены
|
||||
await this.matrixTokenRepository.deleteExpired();
|
||||
|
||||
// Проверяем, есть ли действующий токен для пользователя
|
||||
const existingToken = await this.matrixTokenRepository.findValidByCoopUsername(coopUsername);
|
||||
if (existingToken) {
|
||||
return existingToken;
|
||||
}
|
||||
|
||||
// Генерируем новый токен на 10 секунд
|
||||
const token = uuidv4();
|
||||
const expiresAt = new Date(Date.now() + 10 * 1000); // 10 секунд
|
||||
|
||||
return this.matrixTokenRepository.create({
|
||||
coopUsername,
|
||||
matrixUserId,
|
||||
token,
|
||||
expiresAt,
|
||||
isUsed: false,
|
||||
});
|
||||
}
|
||||
|
||||
async validateToken(token: string): Promise<MatrixTokenDomainEntity | null> {
|
||||
const tokenEntity = await this.matrixTokenRepository.findByToken(token);
|
||||
|
||||
if (!tokenEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Проверяем срок действия
|
||||
if (tokenEntity.expiresAt < new Date() || tokenEntity.isUsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Помечаем токен как использованный
|
||||
await this.matrixTokenRepository.markAsUsed(tokenEntity.id);
|
||||
|
||||
return tokenEntity;
|
||||
}
|
||||
|
||||
async getValidTokenForUser(coopUsername: string): Promise<MatrixTokenDomainEntity | null> {
|
||||
return this.matrixTokenRepository.findValidByCoopUsername(coopUsername);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MatrixUserDomainEntity } from '../entities/matrix-user.entity';
|
||||
import { MATRIX_USER_REPOSITORY, MatrixUserRepository } from '../repositories/matrix-user.repository';
|
||||
|
||||
@Injectable()
|
||||
export class MatrixUserManagementService {
|
||||
constructor(@Inject(MATRIX_USER_REPOSITORY) private readonly matrixUserRepository: MatrixUserRepository) {}
|
||||
|
||||
async createMatrixUser(data: {
|
||||
coopUsername: string;
|
||||
matrixUserId: string;
|
||||
matrixUsername: string;
|
||||
matrixAccessToken: string;
|
||||
matrixDeviceId: string;
|
||||
matrixHomeServer: string;
|
||||
}): Promise<MatrixUserDomainEntity> {
|
||||
// Проверяем, существует ли уже пользователь
|
||||
const existingUser = await this.matrixUserRepository.findByCoopUsername(data.coopUsername);
|
||||
if (existingUser) {
|
||||
throw new Error('Matrix пользователь для данного кооперативного пользователя уже существует');
|
||||
}
|
||||
|
||||
const matrixUser = await this.matrixUserRepository.create({
|
||||
coopUsername: data.coopUsername,
|
||||
matrixUserId: data.matrixUserId,
|
||||
matrixUsername: data.matrixUsername,
|
||||
matrixAccessToken: data.matrixAccessToken,
|
||||
matrixDeviceId: data.matrixDeviceId,
|
||||
matrixHomeServer: data.matrixHomeServer,
|
||||
isRegistered: true,
|
||||
lastTokenRefresh: new Date(),
|
||||
});
|
||||
|
||||
return matrixUser;
|
||||
}
|
||||
|
||||
async getMatrixUserByCoopUsername(coopUsername: string): Promise<MatrixUserDomainEntity | null> {
|
||||
return this.matrixUserRepository.findByCoopUsername(coopUsername);
|
||||
}
|
||||
|
||||
async updateAccessToken(coopUsername: string, newAccessToken: string): Promise<MatrixUserDomainEntity> {
|
||||
const user = await this.matrixUserRepository.findByCoopUsername(coopUsername);
|
||||
if (!user) {
|
||||
throw new Error('Matrix пользователь не найден');
|
||||
}
|
||||
|
||||
return this.matrixUserRepository.update(user.id, {
|
||||
matrixAccessToken: newAccessToken,
|
||||
lastTokenRefresh: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
async ensureMatrixUserExists(coopUsername: string): Promise<MatrixUserDomainEntity> {
|
||||
let matrixUser = await this.matrixUserRepository.findByCoopUsername(coopUsername);
|
||||
|
||||
if (!matrixUser) {
|
||||
// Создаем временную запись до регистрации в Matrix
|
||||
matrixUser = await this.matrixUserRepository.create({
|
||||
coopUsername,
|
||||
matrixUserId: '',
|
||||
matrixUsername: '',
|
||||
matrixAccessToken: '',
|
||||
matrixDeviceId: '',
|
||||
matrixHomeServer: '',
|
||||
isRegistered: false,
|
||||
lastTokenRefresh: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
return matrixUser;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const COOPGRAM_CONNECTION_NAME = 'coopgram';
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MatrixUserTypeormEntity } from '../entities/matrix-user.typeorm-entity';
|
||||
import { MatrixTokenTypeormEntity } from '../entities/matrix-token.typeorm-entity';
|
||||
import { config } from '~/config';
|
||||
import { COOPGRAM_CONNECTION_NAME } from './coopgram-database.constants';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
name: COOPGRAM_CONNECTION_NAME,
|
||||
useFactory: () => ({
|
||||
type: 'postgres',
|
||||
host: config.postgres.host,
|
||||
port: Number(config.postgres.port),
|
||||
username: config.postgres.username,
|
||||
password: config.postgres.password,
|
||||
database: config.postgres.database,
|
||||
|
||||
entities: [MatrixUserTypeormEntity, MatrixTokenTypeormEntity],
|
||||
synchronize: true, // В продакшене использовать миграции
|
||||
logging: false,
|
||||
}),
|
||||
}),
|
||||
TypeOrmModule.forFeature([MatrixUserTypeormEntity, MatrixTokenTypeormEntity], COOPGRAM_CONNECTION_NAME),
|
||||
],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class CoopgramDatabaseModule {}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('matrix_tokens')
|
||||
export class MatrixTokenTypeormEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'coop_username' })
|
||||
coopUsername!: string;
|
||||
|
||||
@Column({ name: 'matrix_user_id' })
|
||||
matrixUserId!: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
token!: string;
|
||||
|
||||
@Column({ name: 'expires_at', type: 'timestamp' })
|
||||
expiresAt!: Date;
|
||||
|
||||
@Column({ name: 'is_used', default: false })
|
||||
isUsed!: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('matrix_users')
|
||||
export class MatrixUserTypeormEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'coop_username', unique: true })
|
||||
coopUsername!: string;
|
||||
|
||||
@Column({ name: 'matrix_user_id' })
|
||||
matrixUserId!: string;
|
||||
|
||||
@Column({ name: 'matrix_username' })
|
||||
matrixUsername!: string;
|
||||
|
||||
@Column({ name: 'matrix_access_token' })
|
||||
matrixAccessToken!: string;
|
||||
|
||||
@Column({ name: 'matrix_device_id' })
|
||||
matrixDeviceId!: string;
|
||||
|
||||
@Column({ name: 'matrix_home_server' })
|
||||
matrixHomeServer!: string;
|
||||
|
||||
@Column({ name: 'is_registered', default: false })
|
||||
isRegistered!: boolean;
|
||||
|
||||
@Column({ name: 'last_token_refresh', type: 'timestamp' })
|
||||
lastTokenRefresh!: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { MatrixTokenDomainEntity } from '../../domain/entities/matrix-token.entity';
|
||||
import { MatrixTokenTypeormEntity } from '../entities/matrix-token.typeorm-entity';
|
||||
|
||||
export class MatrixTokenMapper {
|
||||
static toDomain(entity: MatrixTokenTypeormEntity): MatrixTokenDomainEntity {
|
||||
return {
|
||||
id: entity.id,
|
||||
coopUsername: entity.coopUsername,
|
||||
matrixUserId: entity.matrixUserId,
|
||||
token: entity.token,
|
||||
expiresAt: entity.expiresAt,
|
||||
isUsed: entity.isUsed,
|
||||
createdAt: entity.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
static toEntity(domain: Omit<MatrixTokenDomainEntity, 'id' | 'createdAt'>): Partial<MatrixTokenTypeormEntity> {
|
||||
return {
|
||||
coopUsername: domain.coopUsername,
|
||||
matrixUserId: domain.matrixUserId,
|
||||
token: domain.token,
|
||||
expiresAt: domain.expiresAt,
|
||||
isUsed: domain.isUsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { MatrixUserDomainEntity } from '../../domain/entities/matrix-user.entity';
|
||||
import { MatrixUserTypeormEntity } from '../entities/matrix-user.typeorm-entity';
|
||||
|
||||
export class MatrixUserMapper {
|
||||
static toDomain(entity: MatrixUserTypeormEntity): MatrixUserDomainEntity {
|
||||
return {
|
||||
id: entity.id,
|
||||
coopUsername: entity.coopUsername,
|
||||
matrixUserId: entity.matrixUserId,
|
||||
matrixUsername: entity.matrixUsername,
|
||||
matrixAccessToken: entity.matrixAccessToken,
|
||||
matrixDeviceId: entity.matrixDeviceId,
|
||||
matrixHomeServer: entity.matrixHomeServer,
|
||||
isRegistered: entity.isRegistered,
|
||||
lastTokenRefresh: entity.lastTokenRefresh,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
static toEntity(domain: Omit<MatrixUserDomainEntity, 'id' | 'createdAt' | 'updatedAt'>): Partial<MatrixUserTypeormEntity> {
|
||||
return {
|
||||
coopUsername: domain.coopUsername,
|
||||
matrixUserId: domain.matrixUserId,
|
||||
matrixUsername: domain.matrixUsername,
|
||||
matrixAccessToken: domain.matrixAccessToken,
|
||||
matrixDeviceId: domain.matrixDeviceId,
|
||||
matrixHomeServer: domain.matrixHomeServer,
|
||||
isRegistered: domain.isRegistered,
|
||||
lastTokenRefresh: domain.lastTokenRefresh,
|
||||
};
|
||||
}
|
||||
|
||||
static toUpdateEntity(domain: Partial<MatrixUserDomainEntity>): Partial<MatrixUserTypeormEntity> {
|
||||
const update: Partial<MatrixUserTypeormEntity> = {};
|
||||
|
||||
if (domain.coopUsername !== undefined) update.coopUsername = domain.coopUsername;
|
||||
if (domain.matrixUserId !== undefined) update.matrixUserId = domain.matrixUserId;
|
||||
if (domain.matrixUsername !== undefined) update.matrixUsername = domain.matrixUsername;
|
||||
if (domain.matrixAccessToken !== undefined) update.matrixAccessToken = domain.matrixAccessToken;
|
||||
if (domain.matrixDeviceId !== undefined) update.matrixDeviceId = domain.matrixDeviceId;
|
||||
if (domain.matrixHomeServer !== undefined) update.matrixHomeServer = domain.matrixHomeServer;
|
||||
if (domain.isRegistered !== undefined) update.isRegistered = domain.isRegistered;
|
||||
if (domain.lastTokenRefresh !== undefined) update.lastTokenRefresh = domain.lastTokenRefresh;
|
||||
|
||||
return update;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { MatrixTokenRepository } from '../../domain/repositories/matrix-token.repository';
|
||||
import { MatrixTokenDomainEntity } from '../../domain/entities/matrix-token.entity';
|
||||
import { MatrixTokenTypeormEntity } from '../entities/matrix-token.typeorm-entity';
|
||||
import { MatrixTokenMapper } from '../mappers/matrix-token.mapper';
|
||||
import { COOPGRAM_CONNECTION_NAME } from '../database/coopgram-database.constants';
|
||||
|
||||
@Injectable()
|
||||
export class MatrixTokenTypeormRepository implements MatrixTokenRepository {
|
||||
constructor(
|
||||
@InjectRepository(MatrixTokenTypeormEntity, COOPGRAM_CONNECTION_NAME)
|
||||
private readonly repository: Repository<MatrixTokenTypeormEntity>
|
||||
) {}
|
||||
|
||||
async create(token: Omit<MatrixTokenDomainEntity, 'id' | 'createdAt'>): Promise<MatrixTokenDomainEntity> {
|
||||
const entity = this.repository.create(MatrixTokenMapper.toEntity(token));
|
||||
const savedEntity = await this.repository.save(entity);
|
||||
return MatrixTokenMapper.toDomain(savedEntity);
|
||||
}
|
||||
|
||||
async findByToken(token: string): Promise<MatrixTokenDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { token } });
|
||||
return entity ? MatrixTokenMapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findByCoopUsername(coopUsername: string): Promise<MatrixTokenDomainEntity[]> {
|
||||
const entities = await this.repository.find({
|
||||
where: { coopUsername },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
return entities.map(MatrixTokenMapper.toDomain);
|
||||
}
|
||||
|
||||
async findValidByCoopUsername(coopUsername: string): Promise<MatrixTokenDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({
|
||||
where: {
|
||||
coopUsername,
|
||||
isUsed: false,
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Проверяем срок действия
|
||||
if (entity.expiresAt < new Date()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MatrixTokenMapper.toDomain(entity);
|
||||
}
|
||||
|
||||
async markAsUsed(id: string): Promise<void> {
|
||||
await this.repository.update(id, { isUsed: true });
|
||||
}
|
||||
|
||||
async deleteExpired(): Promise<void> {
|
||||
await this.repository.delete({
|
||||
expiresAt: new Date(Date.now()),
|
||||
isUsed: false,
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<MatrixTokenDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { id } });
|
||||
return entity ? MatrixTokenMapper.toDomain(entity) : null;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { MatrixUserRepository } from '../../domain/repositories/matrix-user.repository';
|
||||
import { MatrixUserDomainEntity } from '../../domain/entities/matrix-user.entity';
|
||||
import { MatrixUserTypeormEntity } from '../entities/matrix-user.typeorm-entity';
|
||||
import { MatrixUserMapper } from '../mappers/matrix-user.mapper';
|
||||
import { COOPGRAM_CONNECTION_NAME } from '../database/coopgram-database.constants';
|
||||
|
||||
@Injectable()
|
||||
export class MatrixUserTypeormRepository implements MatrixUserRepository {
|
||||
constructor(
|
||||
@InjectRepository(MatrixUserTypeormEntity, COOPGRAM_CONNECTION_NAME)
|
||||
private readonly repository: Repository<MatrixUserTypeormEntity>
|
||||
) {}
|
||||
|
||||
async create(user: Omit<MatrixUserDomainEntity, 'id' | 'createdAt' | 'updatedAt'>): Promise<MatrixUserDomainEntity> {
|
||||
const entity = this.repository.create(MatrixUserMapper.toEntity(user));
|
||||
const savedEntity = await this.repository.save(entity);
|
||||
return MatrixUserMapper.toDomain(savedEntity);
|
||||
}
|
||||
|
||||
async findByCoopUsername(coopUsername: string): Promise<MatrixUserDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { coopUsername } });
|
||||
return entity ? MatrixUserMapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findByMatrixUserId(matrixUserId: string): Promise<MatrixUserDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { matrixUserId } });
|
||||
return entity ? MatrixUserMapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<MatrixUserDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { id } });
|
||||
return entity ? MatrixUserMapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async update(id: string, user: Partial<MatrixUserDomainEntity>): Promise<MatrixUserDomainEntity> {
|
||||
const updateData = MatrixUserMapper.toUpdateEntity(user);
|
||||
await this.repository.update(id, updateData);
|
||||
|
||||
const updatedEntity = await this.repository.findOne({ where: { id } });
|
||||
if (!updatedEntity) {
|
||||
throw new Error('Matrix user not found after update');
|
||||
}
|
||||
|
||||
return MatrixUserMapper.toDomain(updatedEntity);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
async findAll(): Promise<MatrixUserDomainEntity[]> {
|
||||
const entities = await this.repository.find();
|
||||
return entities.map(MatrixUserMapper.toDomain);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { SberpollPluginModule } from './sberpoll/sberpoll-extension.module';
|
||||
import { QrPayPluginModule } from './qrpay/qrpay-extension.module';
|
||||
import { BuiltinPluginModule } from './builtin/builtin-extension.module';
|
||||
import { ParticipantPluginModule } from './participant/participant-extension.module';
|
||||
import { CoopgramPluginModule } from './coopgram/coopgram-extension.module';
|
||||
import { ExtensionDomainModule } from '~/domain/extension/extension-domain.module';
|
||||
|
||||
@Module({})
|
||||
@@ -25,6 +26,7 @@ export class ExtensionsModule {
|
||||
SberpollPluginModule,
|
||||
QrPayPluginModule,
|
||||
ParticipantPluginModule,
|
||||
CoopgramPluginModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ========== ./extensions.registry.ts ==========
|
||||
|
||||
import { PowerupPluginModule, Schema as PowerupSchema } from './powerup/powerup-extension.module';
|
||||
import { CoopgramPluginModule, Schema as CoopgramSchema } from './coopgram/coopgram-extension.module';
|
||||
import fs from 'node:fs/promises';
|
||||
import { YookassaPluginModule, Schema as YookassaSchema } from './yookassa/yookassa-extension.module';
|
||||
import { SberpollPluginModule, Schema as SberpollSchema } from './sberpoll/sberpoll-extension.module';
|
||||
@@ -173,4 +174,19 @@ export const AppRegistry: INamedExtension = {
|
||||
readme: getReadmeContent('./qrpay'),
|
||||
instructions: getInstructionsContent('./qrpay'),
|
||||
},
|
||||
coopgram: {
|
||||
is_builtin: false,
|
||||
is_internal: true,
|
||||
is_available: true,
|
||||
is_desktop: false,
|
||||
title: 'Coopgram',
|
||||
description:
|
||||
'Расширение для интеграции с Matrix & Synapse & Element. Организация входа через iframe с временными токенами.',
|
||||
image: 'https://i.ibb.co/Q3NmVvzN/Chat-GPT-Image-10-2025-20-40-44.png',
|
||||
class: CoopgramPluginModule,
|
||||
schema: CoopgramSchema,
|
||||
tags: ['коммуникации', 'matrix'],
|
||||
readme: getReadmeContent('./coopgram'),
|
||||
instructions: getInstructionsContent('./coopgram'),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user