Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Ant 9b922b00c1 coopgram base plugin 2025-09-06 01:17:31 +05:00
24 changed files with 876 additions and 0 deletions
@@ -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;
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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',
}
@@ -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');
@@ -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');
@@ -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);
}
}
@@ -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;
}
}
@@ -0,0 +1 @@
export const COOPGRAM_CONNECTION_NAME = 'coopgram';
@@ -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 {}
@@ -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;
}
@@ -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;
}
@@ -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,
};
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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'),
},
};