diff --git a/docs/content/dev/db-schema.plantuml b/docs/content/dev/db-schema.plantuml index b64bce37f..707ec9c89 100644 --- a/docs/content/dev/db-schema.plantuml +++ b/docs/content/dev/db-schema.plantuml @@ -119,6 +119,11 @@ entity "note_group_permission" { *canEdit : boolean } +entity "group_members_user" { + *group : number <> + *member : uuid <> +} + entity "tag" { *id: number <> *name: text @@ -144,16 +149,16 @@ entity "history_entry" { user "1" -- "0..*" note: owner user "1" -u- "1..*" identity -user "1" - "1..*" auth_token: authTokens -user "1" -l- "1..*" session -user "1" - "0..*" media_upload +user "1" -l- "1..*" auth_token: authTokens +user "1" -r- "1..*" session +user "1" -- "0..*" media_upload user "1" - "0..*" history_entry user "0..*" -- "0..*" note -user "1" - "0..*" authorship +user "1" -- "0..*" authorship (user, note) . author_colors -revision "0..*" - "0..*" authorship +revision "0..*" -- "0..*" authorship (revision, authorship) .. revision_authorship media_upload "0..*" -- "1" note @@ -161,9 +166,11 @@ note "1" - "1..*" revision note "1" - "0..*" history_entry note "0..*" -l- "0..*" tag note "0..*" -- "0..*" group +user "1..*" -- "0..*" group user "0..*" -- "0..*" note (user, note) . note_user_permission (note, group) . note_group_permission +(user, group) . group_members_user @enduml diff --git a/src/api/public/me/me.controller.spec.ts b/src/api/public/me/me.controller.spec.ts index a71d73dcf..43110f49c 100644 --- a/src/api/public/me/me.controller.spec.ts +++ b/src/api/public/me/me.controller.spec.ts @@ -20,6 +20,8 @@ import { User } from '../../../users/user.entity'; import { UsersModule } from '../../../users/users.module'; import { MeController } from './me.controller'; import { HistoryEntry } from '../../../history/history-entry.entity'; +import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; describe('Me Controller', () => { let controller: MeController; @@ -47,6 +49,10 @@ describe('Me Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(HistoryEntry)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .compile(); controller = module.get(MeController); diff --git a/src/api/public/media/media.controller.spec.ts b/src/api/public/media/media.controller.spec.ts index eb699f1c2..7da3ca771 100644 --- a/src/api/public/media/media.controller.spec.ts +++ b/src/api/public/media/media.controller.spec.ts @@ -22,6 +22,8 @@ import { AuthToken } from '../../../auth/auth-token.entity'; import { Identity } from '../../../users/identity.entity'; import { User } from '../../../users/user.entity'; import { MediaController } from './media.controller'; +import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; describe('Media Controller', () => { let controller: MediaController; @@ -57,6 +59,10 @@ describe('Media Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .compile(); controller = module.get(MediaController); diff --git a/src/api/public/notes/notes.controller.spec.ts b/src/api/public/notes/notes.controller.spec.ts index 6df9c73cb..18f28a708 100644 --- a/src/api/public/notes/notes.controller.spec.ts +++ b/src/api/public/notes/notes.controller.spec.ts @@ -19,8 +19,11 @@ import { Identity } from '../../../users/identity.entity'; import { User } from '../../../users/user.entity'; import { UsersModule } from '../../../users/users.module'; import { NotesController } from './notes.controller'; +import { PermissionsModule } from '../../../permissions/permissions.module'; import { HistoryModule } from '../../../history/history.module'; import { HistoryEntry } from '../../../history/history-entry.entity'; +import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; describe('Notes Controller', () => { let controller: NotesController; @@ -39,7 +42,13 @@ describe('Notes Controller', () => { useValue: {}, }, ], - imports: [RevisionsModule, UsersModule, LoggerModule, HistoryModule], + imports: [ + RevisionsModule, + UsersModule, + LoggerModule, + PermissionsModule, + HistoryModule, + ], }) .overrideProvider(getRepositoryToken(Note)) .useValue({}) @@ -61,6 +70,10 @@ describe('Notes Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(HistoryEntry)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .compile(); controller = module.get(NotesController); diff --git a/src/groups/group.entity.ts b/src/groups/group.entity.ts index 427709718..0574e243e 100644 --- a/src/groups/group.entity.ts +++ b/src/groups/group.entity.ts @@ -4,7 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + Entity, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { User } from '../users/user.entity'; @Entity() export class Group { @@ -26,4 +33,11 @@ export class Group { */ @Column() special: boolean; + + @ManyToMany((_) => User, (user) => user.groups, { + eager: true, + cascade: true, + }) + @JoinTable() + members: User[]; } diff --git a/src/history/history.service.spec.ts b/src/history/history.service.spec.ts index ccf0ba04e..caa177c36 100644 --- a/src/history/history.service.spec.ts +++ b/src/history/history.service.spec.ts @@ -21,6 +21,8 @@ import { AuthToken } from '../auth/auth-token.entity'; import { Revision } from '../revisions/revision.entity'; import { Repository } from 'typeorm'; import { NotInDBError } from '../errors/errors'; +import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../permissions/note-user-permission.entity'; describe('HistoryService', () => { let service: HistoryService; @@ -54,6 +56,10 @@ describe('HistoryService', () => { .useClass(Repository) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .compile(); service = module.get(HistoryService); @@ -99,7 +105,7 @@ describe('HistoryService', () => { describe('createOrUpdateHistoryEntry', () => { describe('works', () => { it('without an preexisting entry', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest @@ -118,7 +124,7 @@ describe('HistoryService', () => { }); it('with an preexisting entry', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; const historyEntry = HistoryEntry.create( user, @@ -148,7 +154,7 @@ describe('HistoryService', () => { describe('updateHistoryEntry', () => { describe('works', () => { it('with an entry', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); @@ -173,7 +179,7 @@ describe('HistoryService', () => { }); it('without an entry', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); @@ -192,7 +198,7 @@ describe('HistoryService', () => { describe('deleteHistoryEntry', () => { describe('works', () => { it('with an entry', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); const historyEntry = HistoryEntry.create(user, note); @@ -208,7 +214,7 @@ describe('HistoryService', () => { }); it('without an entry', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; const note = Note.create(user, alias); jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); @@ -225,7 +231,7 @@ describe('HistoryService', () => { describe('toHistoryEntryDto', () => { describe('works', () => { it('with aliased note', async () => { - const user = new User(); + const user = {} as User; const alias = 'alias'; const title = 'title'; const tags = ['tag1', 'tag2']; @@ -247,7 +253,7 @@ describe('HistoryService', () => { }); it('with regular note', async () => { - const user = new User(); + const user = {} as User; const title = 'title'; const id = 'id'; const tags = ['tag1', 'tag2']; diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts index 82821b857..9c2c0535e 100644 --- a/src/media/media.service.spec.ts +++ b/src/media/media.service.spec.ts @@ -25,6 +25,8 @@ import { MediaService } from './media.service'; import { Repository } from 'typeorm'; import { promises as fs } from 'fs'; import { ClientError, NotInDBError, PermissionError } from '../errors/errors'; +import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../permissions/note-user-permission.entity'; describe('MediaService', () => { let service: MediaService; @@ -70,6 +72,10 @@ describe('MediaService', () => { .useClass(Repository) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .overrideProvider(getRepositoryToken(MediaUpload)) .useClass(Repository) .compile(); diff --git a/src/notes/notes.module.ts b/src/notes/notes.module.ts index fc8bd92ac..cba204cf6 100644 --- a/src/notes/notes.module.ts +++ b/src/notes/notes.module.ts @@ -13,10 +13,18 @@ import { AuthorColor } from './author-color.entity'; import { Note } from './note.entity'; import { NotesService } from './notes.service'; import { Tag } from './tag.entity'; +import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../permissions/note-user-permission.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Note, AuthorColor, Tag]), + TypeOrmModule.forFeature([ + Note, + AuthorColor, + Tag, + NoteGroupPermission, + NoteUserPermission, + ]), forwardRef(() => RevisionsModule), UsersModule, LoggerModule, diff --git a/src/notes/notes.service.spec.ts b/src/notes/notes.service.spec.ts index 2c623796a..d08cc8f03 100644 --- a/src/notes/notes.service.spec.ts +++ b/src/notes/notes.service.spec.ts @@ -18,6 +18,8 @@ import { AuthorColor } from './author-color.entity'; import { Note } from './note.entity'; import { NotesService } from './notes.service'; import { Tag } from './tag.entity'; +import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../permissions/note-user-permission.entity'; describe('NotesService', () => { let service: NotesService; @@ -53,6 +55,10 @@ describe('NotesService', () => { .useValue({}) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .compile(); service = module.get(NotesService); }); diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index af14b8174..107403e82 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -159,6 +159,7 @@ export class NotesService { historyEntries: [], updatedAt: new Date(), userName: 'Testy', + groups: [], }, description: 'Very descriptive text.', userPermissions: [], diff --git a/src/revisions/revisions.service.spec.ts b/src/revisions/revisions.service.spec.ts index 5c1d9d437..43799ae0d 100644 --- a/src/revisions/revisions.service.spec.ts +++ b/src/revisions/revisions.service.spec.ts @@ -17,6 +17,8 @@ import { Authorship } from './authorship.entity'; import { Revision } from './revision.entity'; import { RevisionsService } from './revisions.service'; import { Tag } from '../notes/tag.entity'; +import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; +import { NoteUserPermission } from '../permissions/note-user-permission.entity'; describe('RevisionsService', () => { let service: RevisionsService; @@ -48,6 +50,10 @@ describe('RevisionsService', () => { .useValue({}) .overrideProvider(getRepositoryToken(Tag)) .useValue({}) + .overrideProvider(getRepositoryToken(NoteGroupPermission)) + .useValue({}) + .overrideProvider(getRepositoryToken(NoteUserPermission)) + .useValue({}) .compile(); service = module.get(RevisionsService); diff --git a/src/users/user.entity.ts b/src/users/user.entity.ts index ba8016495..ca753c7bd 100644 --- a/src/users/user.entity.ts +++ b/src/users/user.entity.ts @@ -7,6 +7,7 @@ import { CreateDateColumn, Entity, + ManyToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; @@ -14,6 +15,7 @@ import { Column, OneToMany } from 'typeorm'; import { Note } from '../notes/note.entity'; import { AuthToken } from '../auth/auth-token.entity'; import { Identity } from './identity.entity'; +import { Group } from '../groups/group.entity'; import { HistoryEntry } from '../history/history-entry.entity'; @Entity() @@ -52,9 +54,15 @@ export class User { @OneToMany((_) => Identity, (identity) => identity.user) identities: Identity[]; + @ManyToMany((_) => Group, (group) => group.members) + groups: Group[]; + @OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user) historyEntries: HistoryEntry[]; + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + public static create( userName: string, displayName: string,