change readonly boolean to role enum

This commit is contained in:
mgabor
2024-04-17 09:31:56 +02:00
parent 9126bf2520
commit 87bc244b68
30 changed files with 297 additions and 141 deletions

View File

@@ -1,5 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto';
import { AlbumUserRole } from 'src/entities/album-user.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
@@ -219,7 +220,7 @@ export class AccessCore {
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
'read',
AlbumUserRole.Viewer,
);
return setUnion(isOwner, isShared);
}
@@ -229,7 +230,7 @@ export class AccessCore {
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
'write',
AlbumUserRole.Editor,
);
return setUnion(isOwner, isShared);
}
@@ -251,7 +252,7 @@ export class AccessCore {
const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id,
setDifference(ids, isOwner),
'read',
AlbumUserRole.Viewer,
);
return setUnion(isOwner, isShared);
}

View File

@@ -3,6 +3,7 @@ import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AlbumUserRole } from 'src/entities/album-user.entity';
import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
@@ -84,13 +85,15 @@ export class AlbumCountResponseDto {
}
export class UpdateAlbumUserDto {
@ValidateBoolean()
readonly!: boolean;
@IsEnum(AlbumUserRole)
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
role!: AlbumUserRole;
}
export class AlbumUserResponseDto {
user!: UserResponseDto;
readonly!: boolean;
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
role!: AlbumUserRole;
}
export class AlbumResponseDto {
@@ -128,7 +131,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
sharedUsers.push(mapUser(permission.user));
sharedUsersV2.push({
user: mapUser(permission.user),
readonly: permission.readonly,
role: permission.role,
});
}
}

View File

@@ -2,6 +2,11 @@ import { AlbumEntity } from 'src/entities/album.entity';
import { UserEntity } from 'src/entities/user.entity';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
export enum AlbumUserRole {
Editor = 'editor',
Viewer = 'viewer',
}
@Entity('albums_shared_users_users')
// Pre-existing indices from original album <--> user ManyToMany mapping
@Index('IDX_427c350ad49bd3935a50baab73', ['album'])
@@ -17,6 +22,6 @@ export class AlbumUserEntity {
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
user!: UserEntity;
@Column({ default: true })
readonly!: boolean;
@Column({ type: 'varchar', default: 'viewer' })
role!: AlbumUserRole;
}

View File

@@ -1,3 +1,5 @@
import { AlbumUserRole } from 'src/entities/album-user.entity';
export const IAccessRepository = 'IAccessRepository';
export type ReadWrite = 'read' | 'write';
@@ -22,7 +24,7 @@ export interface IAccessRepository {
album: {
checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, readWrite: ReadWrite): Promise<Set<string>>;
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>;
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
};

View File

@@ -1,15 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAlbumUserReadonly1713298646379 implements MigrationInterface {
name = 'AddAlbumUserReadonly1713298646379'
export class AddAlbumUserRole1713337511945 implements MigrationInterface {
name = 'AddAlbumUserRole1713337511945'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "readonly" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ALTER COLUMN "readonly" SET DEFAULT true`);
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "role" character varying NOT NULL DEFAULT 'editor'`);
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ALTER COLUMN "role" SET DEFAULT 'viewer'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "readonly"`);
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "role"`);
}
}

View File

@@ -70,7 +70,7 @@ SELECT
"AlbumEntity"."id" AS "AlbumEntity_id",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly"
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role"
FROM
"albums" "AlbumEntity"
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
@@ -83,6 +83,9 @@ WHERE
(
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" = $2
)
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."role" IN ($3, $4)
)
)
)
)

View File

@@ -34,7 +34,7 @@ FROM
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly",
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -114,7 +114,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly",
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -176,7 +176,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly",
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -296,7 +296,7 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly",
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -373,7 +373,7 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly",
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -489,7 +489,7 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly",
"AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",

View File

@@ -1,6 +1,7 @@
import { InjectRepository } from '@nestjs/typeorm';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { ActivityEntity } from 'src/entities/activity.entity';
import { AlbumUserRole } from 'src/entities/album-user.entity';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
@@ -10,7 +11,7 @@ import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { IAccessRepository, ReadWrite } from 'src/interfaces/access.interface';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { Brackets, In, Repository } from 'typeorm';
@@ -119,7 +120,7 @@ class AlbumAccess implements IAlbumAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 })
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, readWrite: ReadWrite): Promise<Set<string>> {
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
if (albumIds.size === 0) {
return new Set();
}
@@ -132,8 +133,9 @@ class AlbumAccess implements IAlbumAccess {
id: In([...albumIds]),
sharedUsers: {
user: { id: userId },
// If write is needed we check for it, otherwise both are accepted
readonly: readWrite === 'write' ? false : undefined,
// If editor access is needed we check for it, otherwise both are accepted
role:
access === AlbumUserRole.Editor ? AlbumUserRole.Editor : In([AlbumUserRole.Editor, AlbumUserRole.Viewer]),
},
},
})

View File

@@ -273,7 +273,7 @@ export class AlbumService {
throw new BadRequestException('Album not shared with user');
}
await this.albumPermissionRepository.update({ albumId: id, userId }, { readonly: dto.readonly });
await this.albumPermissionRepository.update({ albumId: id, userId }, { role: dto.role });
}
private async findOrFail(id: string, options: AlbumInfoOptions) {