diff --git a/packages/syncing-server/bin/server.ts b/packages/syncing-server/bin/server.ts index e0e177910..12783cb13 100644 --- a/packages/syncing-server/bin/server.ts +++ b/packages/syncing-server/bin/server.ts @@ -7,6 +7,7 @@ import '../src/Controller/ItemsController' import '../src/Controller/LinksController' import '../src/Controller/GroupsController' import '../src/Controller/GroupUsersController' +import '../src/Controller/GroupInvitesController' import '../src/Controller/ContactsController' import helmet from 'helmet' diff --git a/packages/syncing-server/migrations/mysql/1684173023883-create_group_user.ts b/packages/syncing-server/migrations/mysql/1684173023883-create_group_user.ts index b8384983c..670791e2a 100644 --- a/packages/syncing-server/migrations/mysql/1684173023883-create_group_user.ts +++ b/packages/syncing-server/migrations/mysql/1684173023883-create_group_user.ts @@ -25,12 +25,6 @@ export class CreateGroupUser1684173023883 implements MigrationInterface { length: '36', isNullable: false, }), - new TableColumn({ - name: 'inviter_uuid', - type: 'varchar', - length: '36', - isNullable: false, - }), new TableColumn({ name: 'permissions', type: 'varchar', diff --git a/packages/syncing-server/migrations/mysql/1684528646896-create_group_invites.ts b/packages/syncing-server/migrations/mysql/1684528646896-create_group_invites.ts new file mode 100644 index 000000000..38b4f6bdd --- /dev/null +++ b/packages/syncing-server/migrations/mysql/1684528646896-create_group_invites.ts @@ -0,0 +1,68 @@ +import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm' + +export class CreateGroupInvites1684528646896 implements MigrationInterface { + name = 'createGroupInvites1684528646896' + + public async up(queryRunner: QueryRunner): Promise { + const table = new Table({ + name: 'group_invites', + columns: [ + new TableColumn({ + name: 'uuid', + type: 'varchar', + length: '36', + isPrimary: true, + }), + new TableColumn({ + name: 'group_uuid', + type: 'varchar', + length: '36', + isNullable: false, + }), + new TableColumn({ + name: 'user_uuid', + type: 'varchar', + length: '36', + isNullable: false, + }), + new TableColumn({ + name: 'inviter_uuid', + type: 'varchar', + length: '255', + isNullable: false, + }), + new TableColumn({ + name: 'invite_type', + type: 'varchar', + length: '255', + isNullable: false, + }), + new TableColumn({ + name: 'inviter_public_key', + type: 'varchar', + length: '255', + isNullable: false, + }), + new TableColumn({ + name: 'encrypted_group_key', + type: 'text', + isNullable: false, + }), + new TableColumn({ + name: 'created_at_timestamp', + type: 'bigint', + }), + new TableColumn({ + name: 'updated_at_timestamp', + type: 'bigint', + }), + ], + }) + + await queryRunner.createTable(table) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('group_invites') + } +} diff --git a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts index f066ed9cb..251066744 100644 --- a/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts +++ b/packages/syncing-server/src/Bootstrap/ServerContainerConfigLoader.ts @@ -56,6 +56,9 @@ import { SQSNewRelicEventMessageHandler, } from '@standardnotes/domain-events-infra' import { UserCredentialsChangedEventHandler } from '../Domain/Handler/UserCredentialsChangedEventHandler' +import { GroupInviteServiceInterface } from '../Domain/GroupInvite/Service/GroupInviteServiceInterface' +import { GroupInviteService } from '../Domain/GroupInvite/Service/GroupInviteService' +import { GroupInviteFactory } from '../Domain/GroupInvite/Factory/GroupInviteFactory' export class ServerContainerConfigLoader extends CommonContainerConfigLoader { private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 @@ -182,6 +185,17 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader { context.container.get(TYPES.Timer), ) }) + container + .bind(TYPES.GroupInviteService) + .toDynamicValue((context: interfaces.Context) => { + return new GroupInviteService( + context.container.get(TYPES.GroupRepository), + context.container.get(TYPES.GroupInviteRepository), + context.container.get(TYPES.GroupInviteFactory), + context.container.get(TYPES.Timer), + ) + }) + container .bind(TYPES.SyncResponseFactory20161215) .toDynamicValue((context: interfaces.Context) => { @@ -219,6 +233,9 @@ export class ServerContainerConfigLoader extends CommonContainerConfigLoader { container.bind(TYPES.GroupUserFactory).toDynamicValue((context: interfaces.Context) => { return new GroupUserFactory(context.container.get(TYPES.Timer)) }) + container.bind(TYPES.GroupInviteFactory).toDynamicValue((context: interfaces.Context) => { + return new GroupInviteFactory(context.container.get(TYPES.Timer)) + }) container .bind(TYPES.UserCredentialsChangedEventHandler) diff --git a/packages/syncing-server/src/Bootstrap/Types.ts b/packages/syncing-server/src/Bootstrap/Types.ts index 264ecdcf5..a52ab0f81 100644 --- a/packages/syncing-server/src/Bootstrap/Types.ts +++ b/packages/syncing-server/src/Bootstrap/Types.ts @@ -13,6 +13,7 @@ const TYPES = { GroupRepository: Symbol.for('GroupRepository'), GroupUserRepository: Symbol.for('GroupUserRepository'), ContactRepository: Symbol.for('ContactRepository'), + GroupInviteRepository: Symbol.for('GroupInviteRepository'), // ORM ORMItemRepository: Symbol.for('ORMItemRepository'), @@ -20,6 +21,7 @@ const TYPES = { ORMGroupRepository: Symbol.for('ORMGroupRepository'), ORMGroupUserRepository: Symbol.for('ORMGroupUserRepository'), ORMContactRepository: Symbol.for('ORMContactRepository'), + ORMGroupInviteRepository: Symbol.for('ORMGroupInviteRepository'), // Middleware AuthMiddleware: Symbol.for('AuthMiddleware'), @@ -31,13 +33,21 @@ const TYPES = { GroupProjector: Symbol.for('GroupProjector'), GroupUserProjector: Symbol.for('GroupUserProjector'), ContactProjector: Symbol.for('ContactProjector'), + GroupInviteProjector: Symbol.for('GroupInviteProjector'), - // factories + // Services + GroupService: Symbol.for('GroupService'), + GroupUserService: Symbol.for('GroupUserService'), + ContactService: Symbol.for('ContactService'), + GroupInviteService: Symbol.for('GroupInviteService'), + + // Factories ItemFactory: Symbol.for('ItemFactory'), ItemLinkFactory: Symbol.for('ItemLinkFactory'), GroupFactory: Symbol.for('GroupFactory'), GroupUserFactory: Symbol.for('GroupUserFactory'), ContactFactory: Symbol.for('ContactFactory'), + GroupInviteFactory: Symbol.for('GroupInviteFactory'), // env vars REDIS_URL: Symbol.for('REDIS_URL'), @@ -74,11 +84,6 @@ const TYPES = { EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'), ItemRevisionCreationRequestedEventHandler: Symbol.for('ItemRevisionCreationRequestedEventHandler'), - // Services - GroupService: Symbol.for('GroupService'), - GroupUserService: Symbol.for('GroupUserService'), - ContactService: Symbol.for('ContactService'), - ContentDecoder: Symbol.for('ContentDecoder'), DomainEventPublisher: Symbol.for('DomainEventPublisher'), DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'), diff --git a/packages/syncing-server/src/Controller/GroupInvitesController.ts b/packages/syncing-server/src/Controller/GroupInvitesController.ts new file mode 100644 index 000000000..f3edb5bb9 --- /dev/null +++ b/packages/syncing-server/src/Controller/GroupInvitesController.ts @@ -0,0 +1,84 @@ +import { Request, Response } from 'express' +import { BaseHttpController, controller, httpPost, results, httpDelete, httpGet } from 'inversify-express-utils' +import TYPES from '../Bootstrap/Types' +import { inject } from 'inversify' +import { GroupInviteServiceInterface } from '../Domain/GroupInvite/Service/GroupInviteServiceInterface' +import { GroupInviteProjector } from '../Projection/GroupInviteProjector' + +@controller('/groups/invites') +export class GroupInvitesController extends BaseHttpController { + constructor( + @inject(TYPES.GroupInviteService) private groupInviteService: GroupInviteServiceInterface, + @inject(TYPES.GroupInviteProjector) private groupInviteProjector: GroupInviteProjector, + ) { + super() + } + + @httpPost('/', TYPES.AuthMiddleware) + public async addUserToGroup( + request: Request, + response: Response, + ): Promise { + const result = await this.groupInviteService.createGroupInvite({ + originatorUuid: response.locals.user.uuid, + groupUuid: request.params.groupUuid, + userUuid: request.body.invitee_uuid, + inviterUuid: response.locals.user.uuid, + permissions: request.body.permissions, + }) + + if (!result) { + return this.errorResponse(500, 'Could not add user to group') + } + + return this.json({ groupInvite: await this.groupInviteProjector.projectFull(result) }) + } + + @httpGet('/', TYPES.AuthMiddleware) + public async getGroupInvites( + request: Request, + response: Response, + ): Promise { + const result = await this.groupInviteService.getGroupInvites({ + originatorUuid: response.locals.user.uuid, + groupUuid: request.params.groupUuid, + }) + + if (!result) { + return this.errorResponse(500, 'Could not get group users') + } + + const { users } = result + + const projected = users.map((groupInvite) => this.groupInviteProjector.projectFull(groupInvite)) + + return this.json({ users: projected }) + } + + @httpDelete('/:userUuid', TYPES.AuthMiddleware) + public async deleteGroupInvite( + request: Request, + response: Response, + ): Promise { + const result = await this.groupInviteService.deleteGroupInvite({ + groupUuid: request.params.groupUuid, + userUuid: request.params.userUuid, + originatorUuid: response.locals.user.uuid, + }) + + if (!result) { + return this.errorResponse(500, 'Could not delete user') + } + + return this.json({ success: true }) + } + + private errorResponse(status: number, message?: string, tag?: string) { + return this.json( + { + error: { message, tag }, + }, + status, + ) + } +} diff --git a/packages/syncing-server/src/Controller/GroupUsersController.ts b/packages/syncing-server/src/Controller/GroupUsersController.ts index ef88b60d3..399c49cad 100644 --- a/packages/syncing-server/src/Controller/GroupUsersController.ts +++ b/packages/syncing-server/src/Controller/GroupUsersController.ts @@ -23,7 +23,6 @@ export class GroupUsersController extends BaseHttpController { originatorUuid: response.locals.user.uuid, groupUuid: request.params.groupUuid, userUuid: request.body.invitee_uuid, - inviterUuid: response.locals.user.uuid, permissions: request.body.permissions, }) diff --git a/packages/syncing-server/src/Controller/GroupsController.ts b/packages/syncing-server/src/Controller/GroupsController.ts index 9d0280dcd..1b5820810 100644 --- a/packages/syncing-server/src/Controller/GroupsController.ts +++ b/packages/syncing-server/src/Controller/GroupsController.ts @@ -38,7 +38,6 @@ export class GroupsController extends BaseHttpController { groupUuid: result.uuid, originatorUuid: response.locals.user.uuid, userUuid: response.locals.user.uuid, - inviterUuid: response.locals.user.uuid, permissions: 'write', }) diff --git a/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteFactory.ts b/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteFactory.ts new file mode 100644 index 000000000..9c9a5ef9d --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteFactory.ts @@ -0,0 +1,31 @@ +import { TimerInterface } from '@standardnotes/time' +import { GroupInviteFactoryInterface } from './GroupInviteFactoryInterface' +import { GroupInviteHash } from './GroupInviteHash' +import { GroupInvite } from '../Model/GroupInvite' + +export class GroupInviteFactory implements GroupInviteFactoryInterface { + constructor(private timer: TimerInterface) {} + + create(dto: GroupInviteHash): GroupInvite { + const newGroupInvite = new GroupInvite() + newGroupInvite.uuid = dto.uuid + newGroupInvite.userUuid = dto.user_uuid + newGroupInvite.groupUuid = dto.group_uuid + newGroupInvite.inviterUuid = dto.inviter_uuid + + const now = this.timer.getTimestampInMicroseconds() + newGroupInvite.updatedAtTimestamp = now + newGroupInvite.createdAtTimestamp = now + + if (dto.created_at_timestamp) { + newGroupInvite.createdAtTimestamp = dto.created_at_timestamp + } + + return newGroupInvite + } + + createStub(dto: GroupInviteHash): GroupInvite { + const item = this.create(dto) + return item + } +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteFactoryInterface.ts b/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteFactoryInterface.ts new file mode 100644 index 000000000..cc20a6929 --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteFactoryInterface.ts @@ -0,0 +1,7 @@ +import { GroupInvite } from '../Model/GroupInvite' +import { GroupInviteHash } from './GroupInviteHash' + +export interface GroupInviteFactoryInterface { + create(dto: GroupInviteHash): GroupInvite + createStub(dto: GroupInviteHash): GroupInvite +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteHash.ts b/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteHash.ts new file mode 100644 index 000000000..8e6c3d0a8 --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Factory/GroupInviteHash.ts @@ -0,0 +1,9 @@ +export type GroupInviteHash = { + uuid: string + user_uuid: string + group_uuid: string + inviter_uuid: string + permissions: string + created_at_timestamp?: number + updated_at_timestamp?: number +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Model/GroupInvite.ts b/packages/syncing-server/src/Domain/GroupInvite/Model/GroupInvite.ts new file mode 100644 index 000000000..dc584c217 --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Model/GroupInvite.ts @@ -0,0 +1,51 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'group_invites' }) +export class GroupInvite { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + name: 'group_uuid', + length: 36, + }) + declare groupUuid: string + + @Column({ + name: 'user_uuid', + length: 36, + }) + declare userUuid: string + + @Column({ + name: 'inviter_uuid', + }) + declare inviterUuid: string + + @Column({ + name: 'invite_type', + }) + declare inviteType: 'join' | 'key-change' + + @Column({ + name: 'inviter_public_key', + }) + declare inviterPublicKey: string + + @Column({ + name: 'encrypted_group_key', + }) + declare encryptedGroupKey: string + + @Column({ + name: 'created_at_timestamp', + type: 'bigint', + }) + declare createdAtTimestamp: number + + @Column({ + name: 'updated_at_timestamp', + type: 'bigint', + }) + declare updatedAtTimestamp: number +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Repository/GroupInviteRepositoryInterface.ts b/packages/syncing-server/src/Domain/GroupInvite/Repository/GroupInviteRepositoryInterface.ts new file mode 100644 index 000000000..aecc52602 --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Repository/GroupInviteRepositoryInterface.ts @@ -0,0 +1,26 @@ +import { GroupInvite } from '../Model/GroupInvite' + +export type GroupInviteQuery = { + userUuid: string +} + +export type GroupInviteFindAllForUserQuery = { + userUuid: string + lastSyncTime?: number + inviterUuid?: string + includeSentAndReceived?: boolean +} + +export type GroupInviteFindAllForGroup = { + groupUuid: string +} + +export interface GroupInviteRepositoryInterface { + findByUuid(groupInviteUuid: string): Promise + create(group: GroupInvite): Promise + save(groupInvite: GroupInvite): Promise + remove(group: GroupInvite): Promise + findAllForUser(query: GroupInviteFindAllForUserQuery): Promise + findAllForGroup(query: GroupInviteFindAllForGroup): Promise + findByUserUuidAndGroupUuid(userUuid: string, groupUuid: string): Promise +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Repository/TypeORMGroupInviteRepository.ts b/packages/syncing-server/src/Domain/GroupInvite/Repository/TypeORMGroupInviteRepository.ts new file mode 100644 index 000000000..13b200158 --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Repository/TypeORMGroupInviteRepository.ts @@ -0,0 +1,75 @@ +import { GroupInvite } from '../Model/GroupInvite' +import { Repository, SelectQueryBuilder } from 'typeorm' +import { + GroupInviteFindAllForGroup, + GroupInviteFindAllForUserQuery, + GroupInviteRepositoryInterface, +} from './GroupInviteRepositoryInterface' + +export class TypeORMGroupInviteRepository implements GroupInviteRepositoryInterface { + constructor(private ormRepository: Repository) {} + + async create(groupInvite: GroupInvite): Promise { + return this.ormRepository.save(groupInvite) + } + + async save(groupInvite: GroupInvite): Promise { + return this.ormRepository.save(groupInvite) + } + + findByUuid(uuid: string): Promise { + return this.ormRepository + .createQueryBuilder('group_invite') + .where('group_invite.uuid = :uuid', { + uuid, + }) + .getOne() + } + + findByUserUuidAndGroupUuid(userUuid: string, groupUuid: string): Promise { + return this.ormRepository + .createQueryBuilder('group_invite') + .where('group_invite.user_uuid = :userUuid', { userUuid }) + .andWhere('group_invite.group_uuid = :groupUuid', { groupUuid }) + .getOne() + } + + async remove(groupInvite: GroupInvite): Promise { + return this.ormRepository.remove(groupInvite) + } + + findAllForGroup(query: GroupInviteFindAllForGroup): Promise { + return this.ormRepository + .createQueryBuilder('group_invite') + .where('group_invite.group_uuid = :groupUuid', { groupUuid: query.groupUuid }) + .getMany() + } + + async findAllForUser(query: GroupInviteFindAllForUserQuery): Promise { + return this.createFindAllQueryBuilder(query).getMany() + } + + private createFindAllQueryBuilder(query: GroupInviteFindAllForUserQuery): SelectQueryBuilder { + const queryBuilder = this.ormRepository.createQueryBuilder('group_invite') + + queryBuilder.where('group_invite.user_uuid = :userUuid', { userUuid: query.userUuid }) + + if (query.includeSentAndReceived) { + queryBuilder.orWhere('group_invite.sender_uuid = :userUuid', { userUuid: query.userUuid }) + } + + if (query.lastSyncTime) { + queryBuilder.andWhere('group_invite.updated_at_timestamp > :lastSyncTime', { + lastSyncTime: query.lastSyncTime, + }) + } + + if (query.inviterUuid) { + queryBuilder.andWhere('group_invite.inviter_uuid = :inviterUuid', { + inviterUuid: query.inviterUuid, + }) + } + + return queryBuilder + } +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Service/GetUserGroupInvitesDTO.ts b/packages/syncing-server/src/Domain/GroupInvite/Service/GetUserGroupInvitesDTO.ts new file mode 100644 index 000000000..a74fb269e --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Service/GetUserGroupInvitesDTO.ts @@ -0,0 +1,4 @@ +export interface GetUserGroupKeysDTO { + userUuid: string + lastSyncTime?: number +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Service/GroupInviteService.ts b/packages/syncing-server/src/Domain/GroupInvite/Service/GroupInviteService.ts new file mode 100644 index 000000000..1d6d0d18d --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Service/GroupInviteService.ts @@ -0,0 +1,103 @@ +import { TimerInterface } from '@standardnotes/time' +import { GroupInvite } from '../Model/GroupInvite' +import { GroupInviteFactoryInterface } from '../Factory/GroupInviteFactoryInterface' +import { v4 as uuidv4 } from 'uuid' +import { GroupInviteRepositoryInterface } from '../Repository/GroupInviteRepositoryInterface' +import { GroupInviteServiceInterface } from './GroupInviteServiceInterface' +import { GetUserGroupKeysDTO } from './GetUserGroupInvitesDTO' +import { GroupsRepositoryInterface } from '../../Group/Repository/GroupRepositoryInterface' + +export class GroupInviteService implements GroupInviteServiceInterface { + constructor( + private groupRepository: GroupsRepositoryInterface, + private groupInviteRepository: GroupInviteRepositoryInterface, + private groupInviteFactory: GroupInviteFactoryInterface, + private timer: TimerInterface, + ) {} + + async createGroupInvite(dto: { + originatorUuid: string + groupUuid: string + userUuid: string + inviterUuid: string + permissions: string + }): Promise { + const group = await this.groupRepository.findByUuid(dto.groupUuid) + if (!group || group.userUuid !== dto.originatorUuid) { + return null + } + + const groupInvite = this.groupInviteFactory.create({ + uuid: uuidv4(), + user_uuid: dto.userUuid, + group_uuid: dto.groupUuid, + inviter_uuid: dto.inviterUuid, + permissions: dto.permissions, + created_at_timestamp: this.timer.getTimestampInSeconds(), + updated_at_timestamp: this.timer.getTimestampInSeconds(), + }) + + return this.groupInviteRepository.create(groupInvite) + } + + getGroupInvitesForUser(dto: GetUserGroupKeysDTO): Promise { + return this.groupInviteRepository.findAllForUser(dto) + } + + async getGroupInvites(dto: { + groupUuid: string + originatorUuid: string + }): Promise<{ users: GroupInvite[]; isAdmin: boolean } | undefined> { + const group = await this.groupRepository.findByUuid(dto.groupUuid) + if (!group) { + return undefined + } + + const isUserGroupAdmin = group && group.userUuid === dto.originatorUuid + const doesUserBelongToGroup = await this.groupInviteRepository.findByUserUuidAndGroupUuid( + dto.originatorUuid, + dto.groupUuid, + ) + + if (!isUserGroupAdmin && !doesUserBelongToGroup) { + return undefined + } + + const users = await this.groupInviteRepository.findAllForGroup({ groupUuid: dto.groupUuid }) + + return { + users, + isAdmin: isUserGroupAdmin, + } + } + + async deleteGroupInvite(dto: { originatorUuid: string; groupUuid: string; userUuid: string }): Promise { + const groupInvite = await this.groupInviteRepository.findByUserUuidAndGroupUuid(dto.userUuid, dto.groupUuid) + if (!groupInvite) { + return false + } + + const isAuthorized = groupInvite.inviterUuid !== dto.originatorUuid && groupInvite.userUuid !== dto.originatorUuid + if (isAuthorized) { + return false + } + + await this.groupInviteRepository.remove(groupInvite) + + return true + } + + async deleteAllGroupInvitesForGroup(dto: { originatorUuid: string; groupUuid: string }): Promise { + const group = await this.groupRepository.findByUuid(dto.groupUuid) + if (!group || group.userUuid !== dto.originatorUuid) { + return false + } + + const groupInvites = await this.groupInviteRepository.findAllForGroup({ groupUuid: dto.groupUuid }) + for (const groupInvite of groupInvites) { + await this.groupInviteRepository.remove(groupInvite) + } + + return true + } +} diff --git a/packages/syncing-server/src/Domain/GroupInvite/Service/GroupInviteServiceInterface.ts b/packages/syncing-server/src/Domain/GroupInvite/Service/GroupInviteServiceInterface.ts new file mode 100644 index 000000000..9e6960578 --- /dev/null +++ b/packages/syncing-server/src/Domain/GroupInvite/Service/GroupInviteServiceInterface.ts @@ -0,0 +1,23 @@ +import { GroupInvite } from '../Model/GroupInvite' +import { GetUserGroupKeysDTO } from './GetUserGroupInvitesDTO' + +export interface GroupInviteServiceInterface { + createGroupInvite(dto: { + originatorUuid: string + groupUuid: string + userUuid: string + inviterUuid: string + permissions: string + }): Promise + + getGroupInvitesForUser(dto: GetUserGroupKeysDTO): Promise + + getGroupInvites(dto: { + groupUuid: string + originatorUuid: string + }): Promise<{ users: GroupInvite[]; isAdmin: boolean } | undefined> + + deleteGroupInvite(dto: { originatorUuid: string; groupUuid: string; userUuid: string }): Promise + + deleteAllGroupInvitesForGroup(dto: { originatorUuid: string; groupUuid: string }): Promise +} diff --git a/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserFactory.ts b/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserFactory.ts index 16ba69fd0..bbe177d50 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserFactory.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserFactory.ts @@ -10,7 +10,6 @@ export class GroupUserFactory implements GroupUserFactoryInterface { const newGroupUser = new GroupUser() newGroupUser.uuid = dto.uuid newGroupUser.userUuid = dto.user_uuid - newGroupUser.inviterUuid = dto.inviter_uuid newGroupUser.groupUuid = dto.group_uuid newGroupUser.permissions = dto.permissions diff --git a/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserHash.ts b/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserHash.ts index 9a5c3c8c0..0f02b79ec 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserHash.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Factory/GroupUserHash.ts @@ -2,7 +2,6 @@ export type GroupUserHash = { uuid: string user_uuid: string group_uuid: string - inviter_uuid: string permissions: string created_at_timestamp?: number updated_at_timestamp?: number diff --git a/packages/syncing-server/src/Domain/GroupUser/Model/GroupUser.ts b/packages/syncing-server/src/Domain/GroupUser/Model/GroupUser.ts index 79f87fb07..8c723ae32 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Model/GroupUser.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Model/GroupUser.ts @@ -17,11 +17,6 @@ export class GroupUser { }) declare userUuid: string - @Column({ - name: 'inviter_uuid', - }) - declare inviterUuid: string - @Column({ name: 'permissions', }) diff --git a/packages/syncing-server/src/Domain/GroupUser/Model/GroupPermission.ts b/packages/syncing-server/src/Domain/GroupUser/Model/GroupUserPermission.ts similarity index 62% rename from packages/syncing-server/src/Domain/GroupUser/Model/GroupPermission.ts rename to packages/syncing-server/src/Domain/GroupUser/Model/GroupUserPermission.ts index 2dfb3564e..544fc5767 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Model/GroupPermission.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Model/GroupUserPermission.ts @@ -1,4 +1,4 @@ -export enum GroupPermission { +export enum GroupUserPermission { Read = 'read', Write = 'write', Admin = 'admin', diff --git a/packages/syncing-server/src/Domain/GroupUser/Repository/GroupUserRepositoryInterface.ts b/packages/syncing-server/src/Domain/GroupUser/Repository/GroupUserRepositoryInterface.ts index b71e7cc29..f63769652 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Repository/GroupUserRepositoryInterface.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Repository/GroupUserRepositoryInterface.ts @@ -7,7 +7,6 @@ export type GroupUserQuery = { export type GroupUserFindAllForUserQuery = { userUuid: string lastSyncTime?: number - inviterUuid?: string includeSentAndReceived?: boolean } diff --git a/packages/syncing-server/src/Domain/GroupUser/Repository/TypeORMGroupUserRepository.ts b/packages/syncing-server/src/Domain/GroupUser/Repository/TypeORMGroupUserRepository.ts index 2c440dfa2..69c926ef3 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Repository/TypeORMGroupUserRepository.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Repository/TypeORMGroupUserRepository.ts @@ -64,12 +64,6 @@ export class TypeORMGroupUserRepository implements GroupUserRepositoryInterface }) } - if (query.inviterUuid) { - queryBuilder.andWhere('group_user.inviter_uuid = :inviterUuid', { - inviterUuid: query.inviterUuid, - }) - } - return queryBuilder } } diff --git a/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserService.ts b/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserService.ts index c6b8c9f66..0384e74ae 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserService.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserService.ts @@ -19,7 +19,6 @@ export class GroupUserService implements GroupUserServiceInterface { originatorUuid: string groupUuid: string userUuid: string - inviterUuid: string permissions: string }): Promise { const group = await this.groupRepository.findByUuid(dto.groupUuid) @@ -31,7 +30,6 @@ export class GroupUserService implements GroupUserServiceInterface { uuid: uuidv4(), user_uuid: dto.userUuid, group_uuid: dto.groupUuid, - inviter_uuid: dto.inviterUuid, permissions: dto.permissions, created_at_timestamp: this.timer.getTimestampInSeconds(), updated_at_timestamp: this.timer.getTimestampInSeconds(), @@ -72,13 +70,18 @@ export class GroupUserService implements GroupUserServiceInterface { } async deleteGroupUser(dto: { originatorUuid: string; groupUuid: string; userUuid: string }): Promise { - const groupUser = await this.groupUserRepository.findByUserUuidAndGroupUuid(dto.userUuid, dto.groupUuid) - if (!groupUser) { + const group = await this.groupRepository.findByUuid(dto.groupUuid) + if (!group) { return false } - const isAuthorized = groupUser.inviterUuid !== dto.originatorUuid && groupUser.userUuid !== dto.originatorUuid - if (isAuthorized) { + const isAuthorized = group.userUuid === dto.originatorUuid + if (!isAuthorized) { + return false + } + + const groupUser = await this.groupUserRepository.findByUserUuidAndGroupUuid(dto.userUuid, dto.groupUuid) + if (!groupUser) { return false } diff --git a/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserServiceInterface.ts b/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserServiceInterface.ts index ac8985e30..00df4cc8b 100644 --- a/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserServiceInterface.ts +++ b/packages/syncing-server/src/Domain/GroupUser/Service/GroupUserServiceInterface.ts @@ -6,7 +6,6 @@ export interface GroupUserServiceInterface { originatorUuid: string groupUuid: string userUuid: string - inviterUuid: string permissions: string }): Promise diff --git a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts index 53b415453..06c8b601d 100644 --- a/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts +++ b/packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.ts @@ -4,7 +4,7 @@ import { ItemSaveRuleInterface } from './ItemSaveRuleInterface' import { ConflictType } from '@standardnotes/responses' import { GroupUserServiceInterface } from '../../GroupUser/Service/GroupUserServiceInterface' import { ItemHash } from '../ItemHash' -import { GroupPermission } from '../../GroupUser/Model/GroupPermission' +import { GroupUserPermission } from '../../GroupUser/Model/GroupUserPermission' export class OwnershipFilter implements ItemSaveRuleInterface { constructor(private groupUserService: GroupUserServiceInterface) {} @@ -19,7 +19,7 @@ export class OwnershipFilter implements ItemSaveRuleInterface { const groupAuthorization = await this.itemBelongsToAuthorizedSharedGroup(dto.userUuid, dto.itemHash) if (groupAuthorization) { if (isItemBeingRemovedFromGroup) { - if (groupAuthorization === GroupPermission.Admin) { + if (groupAuthorization === GroupUserPermission.Admin) { return { passed: true, } @@ -48,12 +48,12 @@ export class OwnershipFilter implements ItemSaveRuleInterface { private async itemBelongsToAuthorizedSharedGroup( userUuid: string, itemHash: ItemHash, - ): Promise { + ): Promise { const groupUsers = await this.groupUserService.getGroupUsersForUser({ userUuid }) for (const groupUser of groupUsers) { if (itemHash.group_uuid === groupUser.groupUuid) { - return groupUser.permissions as GroupPermission + return groupUser.permissions as GroupUserPermission } } diff --git a/packages/syncing-server/src/Projection/GroupInviteProjection.ts b/packages/syncing-server/src/Projection/GroupInviteProjection.ts new file mode 100644 index 000000000..3ca2c5733 --- /dev/null +++ b/packages/syncing-server/src/Projection/GroupInviteProjection.ts @@ -0,0 +1,8 @@ +export type GroupInviteProjection = { + uuid: string + group_uuid: string + user_uuid: string + inviter_uuid: string + created_at_timestamp: number + updated_at_timestamp: number +} diff --git a/packages/syncing-server/src/Projection/GroupInviteProjector.ts b/packages/syncing-server/src/Projection/GroupInviteProjector.ts new file mode 100644 index 000000000..3bfea90b3 --- /dev/null +++ b/packages/syncing-server/src/Projection/GroupInviteProjector.ts @@ -0,0 +1,29 @@ +import { GroupInvite } from '../Domain/GroupInvite/Model/GroupInvite' +import { ProjectorInterface } from './ProjectorInterface' +import { GroupInviteProjection } from './GroupInviteProjection' + +export class GroupInviteProjector implements ProjectorInterface { + projectSimple(_groupInvite: GroupInvite): GroupInviteProjection { + throw Error('not implemented') + } + + projectCustom(_projectionType: string, groupInvite: GroupInvite): GroupInviteProjection { + const fullProjection = this.projectFull(groupInvite) + + return { + ...fullProjection, + user_uuid: groupInvite.userUuid, + } + } + + projectFull(groupInvite: GroupInvite): GroupInviteProjection { + return { + uuid: groupInvite.uuid, + group_uuid: groupInvite.groupUuid, + user_uuid: groupInvite.userUuid, + inviter_uuid: groupInvite.inviterUuid, + created_at_timestamp: groupInvite.createdAtTimestamp, + updated_at_timestamp: groupInvite.updatedAtTimestamp, + } + } +} diff --git a/packages/syncing-server/src/Projection/GroupUserProjection.ts b/packages/syncing-server/src/Projection/GroupUserProjection.ts index 415a6c7eb..70f3d6c77 100644 --- a/packages/syncing-server/src/Projection/GroupUserProjection.ts +++ b/packages/syncing-server/src/Projection/GroupUserProjection.ts @@ -2,7 +2,6 @@ export type GroupUserProjection = { uuid: string group_uuid: string user_uuid: string - inviter_uuid: string permissions: string created_at_timestamp: number updated_at_timestamp: number diff --git a/packages/syncing-server/src/Projection/GroupUserProjector.ts b/packages/syncing-server/src/Projection/GroupUserProjector.ts index 7ca8bcec1..7f557cff4 100644 --- a/packages/syncing-server/src/Projection/GroupUserProjector.ts +++ b/packages/syncing-server/src/Projection/GroupUserProjector.ts @@ -35,7 +35,6 @@ export class GroupUserProjector implements ProjectorInterface