diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 59d70b2ae7451..1eb98428f0808 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -148,6 +148,26 @@ services: # volumes: # - grafana-data:/var/lib/grafana + # pgadmin: + # container_name: pgadmin + # image: dpage/pgadmin4 + # environment: + # PGADMIN_DEFAULT_EMAIL: admin@example.com + # PGADMIN_DEFAULT_PASSWORD: admin + # PGADMIN_CONFIG_SERVER_MODE: 'False' + # PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' + # PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: 'False' + # ports: + # - "5050:80" + # depends_on: + # - database + # volumes: + # - pgadmin-data:/var/lib/pgadmin + # logging: + # driver: "none" + # restart: always + + volumes: model-cache: prometheus-data: diff --git a/i18n/en.json b/i18n/en.json index 72e3e1e1bf73e..93f3fa59b2c3c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -452,7 +452,7 @@ "clear_all_recent_searches": "Clear all recent searches", "clear_message": "Clear message", "clear_value": "Clear value", - "clockwise": "Сlockwise", + "clockwise": "Clockwise", "close": "Close", "collapse": "Collapse", "collapse_all": "Collapse all", @@ -882,6 +882,7 @@ "no_results_description": "Try a synonym or more general keyword", "no_shared_albums_message": "Create an album to share photos and videos with people in your network", "not_in_any_album": "Not in any album", + "not_published": "NotPublished", "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", "note_unlimited_quota": "Note: Enter 0 for unlimited quota", "notes": "Notes", @@ -972,6 +973,7 @@ "profile_picture_set": "Profile picture set.", "public_album": "Public album", "public_share": "Public Share", + "published": "Published", "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Thank you for supporting Immich and open-source software", "purchase_activated_time": "Activated on {date, date}", @@ -1311,4 +1313,4 @@ "yes": "Yes", "you_dont_have_any_shared_links": "You don't have any shared links", "zoom_image": "Zoom Image" -} +} \ No newline at end of file diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index eb2bb7c0bd9cb..2e51a8140eeea 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -328,8 +328,10 @@ class AlbumsApi { /// * [String] assetId: /// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums /// + /// * [bool] published: + /// /// * [bool] shared: - Future getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async { + Future getAllAlbumsWithHttpInfo({ String? assetId, bool? published, bool? shared, }) async { // ignore: prefer_const_declarations final path = r'/albums'; @@ -343,6 +345,9 @@ class AlbumsApi { if (assetId != null) { queryParams.addAll(_queryParams('', 'assetId', assetId)); } + if (published != null) { + queryParams.addAll(_queryParams('', 'published', published)); + } if (shared != null) { queryParams.addAll(_queryParams('', 'shared', shared)); } @@ -366,9 +371,11 @@ class AlbumsApi { /// * [String] assetId: /// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums /// + /// * [bool] published: + /// /// * [bool] shared: - Future?> getAllAlbums({ String? assetId, bool? shared, }) async { - final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, ); + Future?> getAllAlbums({ String? assetId, bool? published, bool? shared, }) async { + final response = await getAllAlbumsWithHttpInfo( assetId: assetId, published: published, shared: shared, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 547a6a70fd221..163c006d292f8 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -28,6 +28,7 @@ class AlbumResponseDto { this.order, required this.owner, required this.ownerId, + required this.published, required this.shared, this.startDate, required this.updatedAt, @@ -81,6 +82,8 @@ class AlbumResponseDto { String ownerId; + bool published; + bool shared; /// @@ -110,6 +113,7 @@ class AlbumResponseDto { other.order == order && other.owner == owner && other.ownerId == ownerId && + other.published == published && other.shared == shared && other.startDate == startDate && other.updatedAt == updatedAt; @@ -132,12 +136,13 @@ class AlbumResponseDto { (order == null ? 0 : order!.hashCode) + (owner.hashCode) + (ownerId.hashCode) + + (published.hashCode) + (shared.hashCode) + (startDate == null ? 0 : startDate!.hashCode) + (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, owner=$owner, ownerId=$ownerId, published=$published, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -172,6 +177,7 @@ class AlbumResponseDto { } json[r'owner'] = this.owner; json[r'ownerId'] = this.ownerId; + json[r'published'] = this.published; json[r'shared'] = this.shared; if (this.startDate != null) { json[r'startDate'] = this.startDate!.toUtc().toIso8601String(); @@ -206,6 +212,7 @@ class AlbumResponseDto { order: AssetOrder.fromJson(json[r'order']), owner: UserResponseDto.fromJson(json[r'owner'])!, ownerId: mapValueOfType(json, r'ownerId')!, + published: mapValueOfType(json, r'published')!, shared: mapValueOfType(json, r'shared')!, startDate: mapDateTime(json, r'startDate', r''), updatedAt: mapDateTime(json, r'updatedAt', r'')!, @@ -268,6 +275,7 @@ class AlbumResponseDto { 'isActivityEnabled', 'owner', 'ownerId', + 'published', 'shared', 'updatedAt', }; diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index 92e2dc60676af..0cdaf93abd172 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -17,6 +17,7 @@ class SessionResponseDto { required this.current, required this.deviceOS, required this.deviceType, + this.features = const [], required this.id, required this.updatedAt, }); @@ -29,6 +30,8 @@ class SessionResponseDto { String deviceType; + List features; + String id; String updatedAt; @@ -39,6 +42,7 @@ class SessionResponseDto { other.current == current && other.deviceOS == deviceOS && other.deviceType == deviceType && + _deepEquality.equals(other.features, features) && other.id == id && other.updatedAt == updatedAt; @@ -49,11 +53,12 @@ class SessionResponseDto { (current.hashCode) + (deviceOS.hashCode) + (deviceType.hashCode) + + (features.hashCode) + (id.hashCode) + (updatedAt.hashCode); @override - String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]'; + String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, features=$features, id=$id, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -61,6 +66,7 @@ class SessionResponseDto { json[r'current'] = this.current; json[r'deviceOS'] = this.deviceOS; json[r'deviceType'] = this.deviceType; + json[r'features'] = this.features; json[r'id'] = this.id; json[r'updatedAt'] = this.updatedAt; return json; @@ -79,6 +85,9 @@ class SessionResponseDto { current: mapValueOfType(json, r'current')!, deviceOS: mapValueOfType(json, r'deviceOS')!, deviceType: mapValueOfType(json, r'deviceType')!, + features: json[r'features'] is Iterable + ? (json[r'features'] as Iterable).cast().toList(growable: false) + : const [], id: mapValueOfType(json, r'id')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); @@ -132,6 +141,7 @@ class SessionResponseDto { 'current', 'deviceOS', 'deviceType', + 'features', 'id', 'updatedAt', }; diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index e5ae8e1d4ef27..50b924eb249f7 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -19,6 +19,7 @@ class UserAdminResponseDto { required this.email, required this.id, required this.isAdmin, + this.isEAdmin, required this.license, required this.name, required this.oauthId, @@ -44,6 +45,14 @@ class UserAdminResponseDto { bool isAdmin; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isEAdmin; + UserLicense? license; String name; @@ -74,6 +83,7 @@ class UserAdminResponseDto { other.email == email && other.id == id && other.isAdmin == isAdmin && + other.isEAdmin == isEAdmin && other.license == license && other.name == name && other.oauthId == oauthId && @@ -95,6 +105,7 @@ class UserAdminResponseDto { (email.hashCode) + (id.hashCode) + (isAdmin.hashCode) + + (isEAdmin == null ? 0 : isEAdmin!.hashCode) + (license == null ? 0 : license!.hashCode) + (name.hashCode) + (oauthId.hashCode) + @@ -108,7 +119,7 @@ class UserAdminResponseDto { (updatedAt.hashCode); @override - String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, isEAdmin=$isEAdmin, license=$license, name=$name, oauthId=$oauthId, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -122,6 +133,11 @@ class UserAdminResponseDto { json[r'email'] = this.email; json[r'id'] = this.id; json[r'isAdmin'] = this.isAdmin; + if (this.isEAdmin != null) { + json[r'isEAdmin'] = this.isEAdmin; + } else { + // json[r'isEAdmin'] = null; + } if (this.license != null) { json[r'license'] = this.license; } else { @@ -167,6 +183,7 @@ class UserAdminResponseDto { email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, + isEAdmin: mapValueOfType(json, r'isEAdmin'), license: UserLicense.fromJson(json[r'license']), name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6f80b3be31741..7bdae1f78d085 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -580,6 +580,14 @@ "type": "string" } }, + { + "name": "published", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "shared", "required": false, @@ -7673,6 +7681,9 @@ "ownerId": { "type": "string" }, + "published": { + "type": "boolean" + }, "shared": { "type": "boolean" }, @@ -7698,6 +7709,7 @@ "isActivityEnabled", "owner", "ownerId", + "published", "shared", "updatedAt" ], @@ -11129,6 +11141,12 @@ "deviceType": { "type": "string" }, + "features": { + "items": { + "type": "string" + }, + "type": "array" + }, "id": { "type": "string" }, @@ -11141,6 +11159,7 @@ "current", "deviceOS", "deviceType", + "features", "id", "updatedAt" ], @@ -12646,6 +12665,9 @@ "isAdmin": { "type": "boolean" }, + "isEAdmin": { + "type": "boolean" + }, "license": { "allOf": [ { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 5694a95a61951..b173097c66497 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -51,6 +51,7 @@ export type UserAdminResponseDto = { email: string; id: string; isAdmin: boolean; + isEAdmin?: boolean; license: (UserLicense) | null; name: string; oauthId: string; @@ -291,6 +292,7 @@ export type AlbumResponseDto = { order?: AssetOrder; owner: UserResponseDto; ownerId: string; + published: boolean; shared: boolean; startDate?: string; updatedAt: string; @@ -1011,6 +1013,7 @@ export type SessionResponseDto = { current: boolean; deviceOS: string; deviceType: string; + features: string[]; id: string; updatedAt: string; }; @@ -1463,8 +1466,9 @@ export function restoreUserAdmin({ id }: { method: "POST" })); } -export function getAllAlbums({ assetId, shared }: { +export function getAllAlbums({ assetId, published, shared }: { assetId?: string; + published?: boolean; shared?: boolean; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ @@ -1472,6 +1476,7 @@ export function getAllAlbums({ assetId, shared }: { data: AlbumResponseDto[]; }>(`/albums${QS.query(QS.explode({ assetId, + published, shared }))}`, { ...opts diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 9774bd3bea96d..28ff6e1a39096 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -23,7 +23,7 @@ export class AlbumController { constructor(private service: AlbumService) {} @Get() - @Authenticated({ permission: Permission.ALBUM_READ }) + @Authenticated({ permission: Permission.ALBUM_READ, publishedRoute: true }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(auth, query); } @@ -40,14 +40,13 @@ export class AlbumController { return this.service.getStatistics(auth); } - @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true}) + @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true, publishedRoute: true}) @Get(':id') getAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Query() dto: AlbumInfoDto, ): Promise { - console.log("ALBUM INFO") return this.service.get(auth, id, dto); } diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index 3d12cca804d16..d5904a971e0cd 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -78,7 +78,7 @@ export class AssetMediaController { @Get(':id/original') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ sharedLink: true, publishedRoute: true }) async downloadAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -114,7 +114,7 @@ export class AssetMediaController { @Get(':id/thumbnail') @FileResponse() - @Authenticated({ sharedLink: true }) + @Authenticated({ sharedLink: true, publishedRoute: true }) async viewAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 8a5b5fb0b63a8..1949d0651de49 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -74,7 +74,7 @@ export class AssetController { } @Get(':id') - @Authenticated({ sharedLink: true }) + @Authenticated({ sharedLink: true, publishedRoute: true }) getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id) as Promise; } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 92fa59f6bf459..b52e3a5e95670 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -59,7 +59,7 @@ export class AuthController { @Post('logout') @HttpCode(HttpStatus.OK) - @Authenticated() + @Authenticated({publishedRoute: true}) async logout( @Req() request: Request, @Res({ passthrough: true }) res: Response, diff --git a/server/src/controllers/session.controller.ts b/server/src/controllers/session.controller.ts index d526c2e59951f..f33212cc4ae27 100644 --- a/server/src/controllers/session.controller.ts +++ b/server/src/controllers/session.controller.ts @@ -12,6 +12,8 @@ import { UUIDParamDto } from 'src/validation'; export class SessionController { constructor(private service: SessionService) {} + + @Get() @Authenticated({ permission: Permission.SESSION_READ }) getSessions(@Auth() auth: AuthDto): Promise { diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index 008813526f8c5..7ed04753e1bbd 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; +import { Exception } from 'handlebars'; import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -66,6 +67,7 @@ export class SharedLinkController { @Post() @Authenticated({ permission: Permission.SHARED_LINK_CREATE }) createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { + throw new Exception("shared links are disabled") return this.service.create(auth, dto); } diff --git a/server/src/controllers/timeline.controller.ts b/server/src/controllers/timeline.controller.ts index 92de84d346e9b..9837e3161dc85 100644 --- a/server/src/controllers/timeline.controller.ts +++ b/server/src/controllers/timeline.controller.ts @@ -13,13 +13,13 @@ export class TimelineController { constructor(private service: TimelineService) {} @Get('buckets') - @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) + @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true, publishedRoute: true }) getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise { return this.service.getTimeBuckets(auth, dto); } @Get('bucket') - @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) + @Authenticated({ permission: Permission.ASSET_READ, sharedLink: true, publishedRoute: true }) getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise { return this.service.getTimeBucket(auth, dto) as Promise; } diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index b4fe44b34634f..1d2517021fd04 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -90,7 +90,7 @@ export class UserController { @Get(':id') @Authenticated() - getUser(@Param() { id }: UUIDParamDto): Promise { + getUser(@Auth() {features}: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index b12847ee62537..e894d553415b4 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -82,10 +82,18 @@ export class GetAlbumsDto { /** * true: only shared albums * false: only non-shared own albums - * undefined: shared and owned albums + * undefined: shared, published and owned albums */ shared?: boolean; + @ValidateBoolean({ optional: true }) + /** + * true: only published albums + * false: only non-publised own albums + * undefined: shared, published and owned albums + */ + published?: boolean; + /** * Only returns albums that contain the asset * Ignores the shared parameter @@ -129,6 +137,7 @@ export class AlbumResponseDto { shared!: boolean; albumUsers!: AlbumUserResponseDto[]; hasSharedLink!: boolean; + published!: boolean; assets!: AssetResponseDto[]; owner!: UserResponseDto; @ApiProperty({ type: 'integer' }) @@ -189,6 +198,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, order: entity.order, + published: entity.published, }; }; diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index d96d7819adcab..dff8c9d6a5167 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -7,6 +7,7 @@ export class SessionResponseDto { current!: boolean; deviceType!: string; deviceOS!: string; + features!: string[]; } export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({ @@ -16,4 +17,5 @@ export const mapSession = (entity: SessionEntity, currentId?: string): SessionRe current: currentId === entity.id, deviceOS: entity.deviceOS, deviceType: entity.deviceType, + features: entity.features, }); diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 40a84aaa49f7d..3cf963a9f5bc6 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -129,6 +129,7 @@ export class UserAdminResponseDto extends UserResponseDto { storageLabel!: string | null; shouldChangePassword!: boolean; isAdmin!: boolean; + isEAdmin?: boolean; createdAt!: Date; deletedAt!: Date | null; updatedAt!: Date; @@ -142,7 +143,7 @@ export class UserAdminResponseDto extends UserResponseDto { license!: UserLicense | null; } -export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { +export function mapUserAdmin(entity: UserEntity, isEAdmin?: boolean): UserAdminResponseDto { const license = entity.metadata?.find( (item): item is UserMetadataEntity => item.key === UserMetadataKey.LICENSE, )?.value; @@ -159,5 +160,6 @@ export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { quotaUsageInBytes: entity.quotaUsageInBytes, status: entity.status, license: license ?? null, + isEAdmin: isEAdmin, }; } diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts index 5aec5a0f47dd0..12cf448a81305 100644 --- a/server/src/entities/album.entity.ts +++ b/server/src/entities/album.entity.ts @@ -63,4 +63,7 @@ export class AlbumEntity { @Column({ type: 'varchar', default: AssetOrder.DESC }) order!: AssetOrder; + + @Column({default: false}) + published!: boolean; } diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index d8d7b4e807ab9..ddd47b339087d 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -10,6 +10,7 @@ export interface IAccessRepository { }; asset: { + checkPublishedAlbumAccess(assetIds: Set): Promise>; checkOwnerAccess(userId: string, assetIds: Set): Promise>; checkAlbumAccess(userId: string, assetIds: Set): Promise>; checkPartnerAccess(userId: string, assetIds: Set): Promise>; @@ -21,6 +22,7 @@ export interface IAccessRepository { }; album: { + checkPublishedAccess(assetIds: Set): Promise>; checkOwnerAccess(userId: string, albumIds: Set): Promise>; checkSharedAlbumAccess(userId: string, albumIds: Set, access: AlbumUserRole): Promise>; checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise>; diff --git a/server/src/interfaces/album.interface.ts b/server/src/interfaces/album.interface.ts index 24c64bdc9d2c0..23f9b0cf07b49 100644 --- a/server/src/interfaces/album.interface.ts +++ b/server/src/interfaces/album.interface.ts @@ -19,6 +19,8 @@ export interface IAlbumRepository extends IBulkAsset { getByAssetId(ownerId: string, assetId: string): Promise; removeAsset(assetId: string): Promise; getMetadataForIds(ids: string[]): Promise; + getPublished(): Promise; + getNotPublished(): Promise; getOwned(ownerId: string): Promise; getShared(ownerId: string): Promise; getNotShared(ownerId: string): Promise; diff --git a/server/src/migrations/1739008254250-AddPublishedFlagAlbum.ts b/server/src/migrations/1739008254250-AddPublishedFlagAlbum.ts new file mode 100644 index 0000000000000..3cd20fa45d060 --- /dev/null +++ b/server/src/migrations/1739008254250-AddPublishedFlagAlbum.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddPublishedFlagAlbum1739008254250 implements MigrationInterface { + name = 'AddPublishedFlagAlbum1739008254250' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" ADD "published" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "published"`); + } + +} diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index f3cbf392db295..c9bae18175f18 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -103,6 +103,26 @@ class AlbumAccess implements IAlbumAccess { private sharedLinkRepository: Repository, ) {} + @GenerateSql({ params: [DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 0 }) + async checkPublishedAccess(albumIds: Set): Promise> { + if (albumIds.size === 0) { + return new Set(); + } + + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + published: true, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); + } + + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) async checkOwnerAccess(userId: string, albumIds: Set): Promise> { @@ -173,6 +193,42 @@ class AssetAccess implements IAssetAccess { private sharedLinkRepository: Repository, ) {} + + @GenerateSql({ params: [DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 0 }) + async checkPublishedAlbumAccess(assetIds: Set): Promise> { + if (assetIds.size === 0) { + return new Set(); + } + + return this.albumRepository + .createQueryBuilder('album') + .innerJoin('album.assets', 'asset') + .leftJoin('album.albumUsers', 'album_albumUsers_users') + .leftJoin('album_albumUsers_users.user', 'albumUsers') + .select('asset.id', 'assetId') + .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') + .where('array["asset"."id", "asset"."livePhotoVideoId"] && array[:...assetIds]::uuid[]', { + assetIds: [...assetIds], + }) + .andWhere( + 'album.published = true' + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { + allowedIds.add(row.livePhotoVideoId); + } + } + return allowedIds; + }); + } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) async checkAlbumAccess(userId: string, assetIds: Set): Promise> { diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 8b7565e318cf2..e2ca86dbe4792 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; @@ -98,12 +99,36 @@ export class AlbumRepository implements IAlbumRepository { })); } + @GenerateSql({ params: [DummyValue.UUID] }) + async getPublished(): Promise { + const albums = await this.repository.find({ + relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, + where: { published: true }, + order: { createdAt: 'DESC' }, + }); + + return albums.map((album) => withoutDeletedUsers(album)); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getNotPublished(): Promise { + const albums = await this.repository.find({ + relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, + where: { published: false }, + order: { createdAt: 'DESC' }, + }); + + return albums.map((album) => withoutDeletedUsers(album)); + } + + @GenerateSql({ params: [DummyValue.UUID] }) async getOwned(ownerId: string): Promise { + //Only people with proper features from E-guild auth can even get to this endpoint, thus okey to let everyone change album. const albums = await this.repository.find({ relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, - where: { ownerId }, order: { createdAt: 'DESC' }, + where: { ownerId } }); return albums.map((album) => withoutDeletedUsers(album)); @@ -141,6 +166,20 @@ export class AlbumRepository implements IAlbumRepository { return albums.map((album) => withoutDeletedUsers(album)); } + //This should only be called by an eguild admin + @GenerateSql({ params: [DummyValue.UUID] }) + async getAll(auth: AuthDto): Promise { + if (!(auth.features.includes('superadmin') || auth.features.includes('emmech_admin'))) { + return []; + } + const albums = await this.repository.find({ + relations: { albumUsers: { user: true }, sharedLinks: true, owner: true }, + order: { createdAt: 'DESC' }, + }); + + return albums.map((album) => withoutDeletedUsers(album)); + } + async restoreAll(userId: string): Promise { await this.repository.restore({ ownerId: userId }); } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 2cf83e9b99972..512297f3601d2 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -37,7 +37,7 @@ export class AlbumService extends BaseService { }; } - async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise { + async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared, published }: GetAlbumsDto): Promise { await this.albumRepository.updateThumbnails(); let albums: AlbumEntity[]; @@ -47,7 +47,12 @@ export class AlbumService extends BaseService { albums = await this.albumRepository.getShared(ownerId); } else if (shared === false) { albums = await this.albumRepository.getNotShared(ownerId); - } else { + } else if (published === true) { + albums = await this.albumRepository.getPublished() + } else if (published === false) { + albums = await this.albumRepository.getNotPublished() + } + else { albums = await this.albumRepository.getOwned(ownerId); } diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index dd15696e2aae2..af0565bfe9fc7 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -135,6 +135,10 @@ export class AuthService extends BaseService { throw new ForbiddenException('Forbidden'); } + if (authDto.sharedLink) { + throw new ForbiddenException("sharedLinks are disabled //Pontus") + } + if (authDto.sharedLink && !sharedLinkRoute) { this.logger.warn(`Denied access to non-shared route: ${uri}`); throw new ForbiddenException('Forbidden'); @@ -145,7 +149,7 @@ export class AuthService extends BaseService { } //console.log(`Username: ${authDto.user.oauthId}\nPublished: ${metadata.publishedRoute}\n features ${authDto.features}`) - if (authDto.user.oauthId != '' && !authDto.features.includes('superadmin') && !metadata.publishedRoute && !authDto.sharedLink) { + if (authDto.user.oauthId != '' && !(authDto.features.includes('superadmin') || authDto.features.includes('emmech_admin')) && !metadata.publishedRoute && !authDto.sharedLink) { throw new ForbiddenException(`E-guild features`) } diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 926482fb9c89a..ad0e4a13a9d79 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -25,7 +25,7 @@ export class UserService extends BaseService { } getMe(auth: AuthDto): UserAdminResponseDto { - return mapUserAdmin(auth.user); + return mapUserAdmin(auth.user, auth.user.isAdmin || auth.features.includes('emmech_admin') || auth.features.includes('superadmin')); } async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise { diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index d3219a1a6c4b6..71c933f2b61f6 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -50,9 +50,13 @@ export const checkAccess = async ( return new Set(); } - return auth.sharedLink - ? checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet }) - : checkOtherAccess(access, { auth, permission, ids: idSet }); + if(auth.sharedLink) { + return checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet }) + } else if (auth.user.isAdmin || auth.features.includes("superadmin") || auth.features.includes("emmech_admin")) { + return checkEAdminAccess(access, { auth, permission, ids: idSet }); + } else { + return checkOtherAccess(access, { auth, permission, ids: idSet }); + } }; const checkSharedLinkAccess = async ( @@ -81,7 +85,7 @@ const checkSharedLinkAccess = async ( case Permission.ASSET_SHARE: { // TODO: fix this to not use sharedLink.userId for access control - return await access.asset.checkOwnerAccess(sharedLink.userId, ids); + return await new Promise(() => new Set()) } case Permission.ALBUM_READ: { @@ -102,189 +106,163 @@ const checkSharedLinkAccess = async ( } }; +const checkEAdminAccess = (access: IAccessRepository, request: OtherAccessRequest): Set => { + const { auth, ids} = request; + if (auth.user.isAdmin || auth.features.includes('superadmin') || auth.features.includes('emmech_admin')) { + return ids; + } else { + return new Set(); + } +} + const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest): Promise> => { - const { auth, permission, ids } = request; + const { permission, ids } = request; switch (permission) { // uses album id - case Permission.ACTIVITY_CREATE: { - return await access.activity.checkCreateAccess(auth.user.id, ids); - } + // case Permission.ACTIVITY_CREATE: { + // return new Set(); + // } // uses activity id - case Permission.ACTIVITY_DELETE: { - const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids); - const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isAlbumOwner); - } + // case Permission.ACTIVITY_DELETE: { + // return new Set(); + // } case Permission.ASSET_READ: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); - return setUnion(isOwner, isAlbum, isPartner); + return await access.asset.checkPublishedAlbumAccess(ids); } - case Permission.ASSET_SHARE: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); - const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } + // case Permission.ASSET_SHARE: { + // return new Set(); + // } + // This tag is never used in the entire project. Unclear why it exists... case Permission.ASSET_VIEW: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); - return setUnion(isOwner, isAlbum, isPartner); + return await access.asset.checkPublishedAlbumAccess(ids); + } case Permission.ASSET_DOWNLOAD: { - const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); - return setUnion(isOwner, isAlbum, isPartner); + return access.asset.checkPublishedAlbumAccess(ids); } - case Permission.ASSET_UPDATE: { - return await access.asset.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.ASSET_UPDATE: { + // return new Set(); + // } - case Permission.ASSET_DELETE: { - return await access.asset.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.ASSET_DELETE: { + // return new Set(); + // } case Permission.ALBUM_READ: { - const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await access.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); + return access.album.checkPublishedAccess(ids); } - case Permission.ALBUM_ADD_ASSET: { - const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await access.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } + // case Permission.ALBUM_ADD_ASSET: { + // return new Set(); + // } - case Permission.ALBUM_UPDATE: { - return await access.album.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.ALBUM_UPDATE: { + // return new Set(); + // } - case Permission.ALBUM_DELETE: { - return await access.album.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.ALBUM_DELETE: { + // return new Set(); + // } - case Permission.ALBUM_SHARE: { - return await access.album.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.ALBUM_SHARE: { + // return new Set(); + // } case Permission.ALBUM_DOWNLOAD: { - const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await access.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); + return access.album.checkPublishedAccess(ids); } - case Permission.ALBUM_REMOVE_ASSET: { - const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await access.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } + // case Permission.ALBUM_REMOVE_ASSET: { + // return new Set(); + // } - case Permission.ASSET_UPLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } + // case Permission.ASSET_UPLOAD: { + // return new Set(); + // } - case Permission.ARCHIVE_READ: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } + // case Permission.ARCHIVE_READ: { + // return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + // } - case Permission.AUTH_DEVICE_DELETE: { - return await access.authDevice.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.AUTH_DEVICE_DELETE: { + // return await access.authDevice.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.TAG_ASSET: - case Permission.TAG_READ: - case Permission.TAG_UPDATE: - case Permission.TAG_DELETE: { - return await access.tag.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.TAG_ASSET: + // case Permission.TAG_READ: + // case Permission.TAG_UPDATE: + // case Permission.TAG_DELETE: { + // return await access.tag.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.TIMELINE_READ: { - const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } + // case Permission.TIMELINE_READ: { + // const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + // const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + // return setUnion(isOwner, isPartner); + // } - case Permission.TIMELINE_DOWNLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } + // case Permission.TIMELINE_DOWNLOAD: { + // return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + // } - case Permission.MEMORY_READ: { - return access.memory.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.MEMORY_READ: { + // return access.memory.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.MEMORY_UPDATE: { - return access.memory.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.MEMORY_UPDATE: { + // return access.memory.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.MEMORY_DELETE: { - return access.memory.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.MEMORY_DELETE: { + // return access.memory.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.MEMORY_DELETE: { - return access.memory.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.MEMORY_DELETE: { + // return access.memory.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.PERSON_READ: { - return await access.person.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.PERSON_READ: { + // return await access.person.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.PERSON_UPDATE: { - return await access.person.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.PERSON_UPDATE: { + // return await access.person.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.PERSON_MERGE: { - return await access.person.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.PERSON_MERGE: { + // return await access.person.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.PERSON_CREATE: { - return access.person.checkFaceOwnerAccess(auth.user.id, ids); - } + // case Permission.PERSON_CREATE: { + // return access.person.checkFaceOwnerAccess(auth.user.id, ids); + // } - case Permission.PERSON_REASSIGN: { - return access.person.checkFaceOwnerAccess(auth.user.id, ids); - } + // case Permission.PERSON_REASSIGN: { + // return access.person.checkFaceOwnerAccess(auth.user.id, ids); + // } - case Permission.PARTNER_UPDATE: { - return await access.partner.checkUpdateAccess(auth.user.id, ids); - } + // case Permission.PARTNER_UPDATE: { + // return await access.partner.checkUpdateAccess(auth.user.id, ids); + // } - case Permission.STACK_READ: { - return access.stack.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.STACK_READ: { + // return access.stack.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.STACK_UPDATE: { - return access.stack.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.STACK_UPDATE: { + // return access.stack.checkOwnerAccess(auth.user.id, ids); + // } - case Permission.STACK_DELETE: { - return access.stack.checkOwnerAccess(auth.user.id, ids); - } + // case Permission.STACK_DELETE: { + // return access.stack.checkOwnerAccess(auth.user.id, ids); + // } default: { return new Set(); diff --git a/web/src/lib/components/album-page/album-card-group-publish.svelte b/web/src/lib/components/album-page/album-card-group-publish.svelte index 598ad848f9134..f899cebd8c430 100644 --- a/web/src/lib/components/album-page/album-card-group-publish.svelte +++ b/web/src/lib/components/album-page/album-card-group-publish.svelte @@ -12,7 +12,6 @@ import { t } from 'svelte-i18n'; export let albums: AlbumResponseDto[]; - export let keys: Record; export let group: AlbumGroup | undefined = undefined; export let showOwner = false; export let showDateRange = false; @@ -55,12 +54,11 @@ {#each albums as album, index (album.id)} showContextMenu({ x: e.x, y: e.y }, album)} > {/if} - +

{#if thumbnailUrl} - + {:else} {/if} diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 3ec1842757201..dfebc7b63773b 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -32,6 +32,7 @@ export let onShowSelectSharedUser: () => void; export let onRemove: (userId: string) => void; export let onRefreshAlbum: () => void; + export let isEAdmin: boolean = false; let selectedRemoveUser: UserResponseDto | null = null; @@ -151,7 +152,7 @@ {:else} {$t('role_editor')} {/if} - {#if user.id !== album.ownerId} + {#if !isEAdmin} {#if role === AlbumUserRole.Viewer} => { return { [AlbumFilter.All]: $t('all'), - [AlbumFilter.Owned]: $t('owned'), - [AlbumFilter.Shared]: $t('shared'), + [AlbumFilter.Published]: $t('published'), + [AlbumFilter.NotPublished]: $t('not_published'), }; })(); diff --git a/web/src/lib/components/album-page/albums-list-publish.svelte b/web/src/lib/components/album-page/albums-list-publish.svelte index 877e607b97f5e..dc206af7e6232 100644 --- a/web/src/lib/components/album-page/albums-list-publish.svelte +++ b/web/src/lib/components/album-page/albums-list-publish.svelte @@ -19,12 +19,11 @@ import { t } from 'svelte-i18n'; import AlbumCardGroupPublish from '$lib/components/album-page/album-card-group-publish.svelte'; - export let sharedAlbums: AlbumResponseDto[] = []; + export let publishedAlbums: AlbumResponseDto[] = []; export let searchQuery: string = ''; export let userSettings: AlbumViewSettings; export let showOwner = false; export let albumGroupIds: string[] = []; - export let keys: Record; interface AlbumGroupOption { [option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumGroup[]; @@ -84,7 +83,7 @@ // Step 1: Filter between Owned and Shared albums, or both. $: { - albums = sharedAlbums; + albums = publishedAlbums; } // Step 2: Filter using the given search query. @@ -145,7 +144,6 @@ {#if albumGroupOption === AlbumGroupBy.None} id); } - $: showFullContextMenu = allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id; + $: showFullContextMenu = allowEdit && contextMenuTargetAlbum; onMount(async () => { if (allowEdit) { diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index d142c43f20396..ee41cb4e77592 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -84,7 +84,7 @@ class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white" data-testid="asset-viewer-navbar-actions" > - {#if !asset.isTrashed && $user} + {#if !asset.isTrashed && $user.isEAdmin} {/if} {#if asset.isOffline} diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index b2780cc1a06b0..7b42a9bf4217e 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -26,6 +26,7 @@ export let assetStore: AssetStore; export let bucket: AssetBucket; export let assetInteractionStore: AssetInteractionStore; + export let isEAdmin: boolean = false; export let onScrollTarget: ScrollTargetListener | undefined = undefined; export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; @@ -151,7 +152,7 @@ class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" style:width={dateGroup.geometry.containerWidth + 'px'} > - {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))} + {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle)) && isEAdmin}

{/each} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 6c534e5116698..9ffe2c2901cfc 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -37,6 +37,7 @@ import type { UpdatePayload } from 'vite'; import { generateId } from '$lib/utils/generate-id'; import { isTimelineScrolling } from '$lib/stores/timeline.store'; + import { user } from '$lib/stores/user.store'; export let isSelectionMode = false; export let singleSelect = false; @@ -45,7 +46,7 @@ `AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and additionally, update the page location/url with the asset as the asset-grid is scrolled */ export let enableRouting: boolean; - + export let isEAdmin: boolean = false; export let assetStore: AssetStore; export let assetInteractionStore: AssetInteractionStore; export let removeAction: @@ -854,6 +855,7 @@ onSelect={({ title, assets }) => handleGroupSelect(title, assets)} onSelectAssetCandidates={handleSelectAssetCandidates} onSelectAssets={handleSelectAssets} + {isEAdmin} /> {/if}
diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 87f4a7ba44708..7203c8f652d9f 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -108,8 +108,8 @@ export enum AlbumViewMode { export enum AlbumFilter { All = 'All', - Owned = 'Owned', - Shared = 'Shared', + Published = 'Published', + NotPublished = 'Not Published' } export enum AlbumGroupBy { diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index aa00801b6ba02..49ef92e652b03 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -13,6 +13,7 @@ import { AppRoute } from '../constants'; export interface AuthOptions { admin?: true; public?: true; + published?: true; } export const loadUser = async () => { @@ -31,6 +32,11 @@ export const loadUser = async () => { purchaseStore.setPurchaseStatus(true); } } + + if (user.isEAdmin === undefined) { + console.log("WARNING isEADMIN") + } + return user; } catch { return null; @@ -53,7 +59,7 @@ const hasAuthCookie = (): boolean => { }; export const authenticate = async (options?: AuthOptions) => { - const { public: publicRoute, admin: adminRoute} = options || {}; + const { public: publicRoute, admin: adminRoute, published: publishedRoute} = options || {}; const user = await loadUser(); if (publicRoute) { @@ -68,6 +74,10 @@ export const authenticate = async (options?: AuthOptions) => { redirect(302, AppRoute.PHOTOS); } + if (!publishedRoute && !user.isEAdmin) { + redirect(302, AppRoute.PUBLISH) + } + }; diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index 35402ce331d49..29d3197be7165 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -37,6 +37,7 @@ { await authenticate(); const sharedAlbums = await getAllAlbums({ shared: true }); + const publishedAlbums = await getAllAlbums({published: true}) const albums = await getAllAlbums({}); const $t = await getFormatter(); return { albums, sharedAlbums, + publishedAlbums, meta: { title: $t('albums'), }, diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 9ccb2b7182e7d..58fe44e370ba0 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -132,8 +132,8 @@ const timelineInteractionStore = createAssetInteractionStore(); const { selectedAssets: timelineSelected } = timelineInteractionStore; - $: isOwned = $user.id == album.ownerId; - $: isAllUserOwned = [...$selectedAssets].every((asset) => asset.ownerId === $user.id); + $: isOwned = $user.isEAdmin ?? false; + $: isAllUserOwned = $user.isEAdmin ?? false; $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); $: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived); $: { @@ -143,9 +143,9 @@ album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0); // svelte-ignore reactive_declaration_non_reactive_property - $: isEditor = - album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor || - album.ownerId === $user.id; + $: isEditor = $user.isEAdmin ?? false; + + $: isEAdmin = $user.isEAdmin ?? false; // svelte-ignore reactive_declaration_non_reactive_property $: albumHasViewers = album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer); @@ -436,7 +436,7 @@
- {#if $isMultiSelectState} + {#if $isMultiSelectState && isEAdmin} assetInteractionStore.clearMultiselect()}> @@ -503,7 +503,7 @@ {#if album.assetCount > 0} - + {#if isOwned} @@ -578,6 +578,7 @@ assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} + {isEAdmin} /> {:else} handleUpdateThumbnail(id)} onEscape={handleEscape} + {isEAdmin} > {#if viewMode !== ViewMode.SELECT_THUMBNAIL} @@ -753,6 +755,7 @@ onClose={() => (viewMode = ViewMode.VIEW)} onToggleEnabledActivity={handleToggleEnableActivity} onShowSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} + {isEAdmin} /> {/if} diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts index 0143390974f6e..51ae9658b640e 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -4,7 +4,7 @@ import { getAlbumInfo } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ params }) => { - await authenticate(); + await authenticate({published: true}); const [album, asset] = await Promise.all([ getAlbumInfo({ id: params.albumId, withoutAssets: true }), getAssetInfoFromParam(params), diff --git a/web/src/routes/(user)/publish/+page.svelte b/web/src/routes/(user)/publish/+page.svelte index 86bf9eec6a6ff..068860a55a628 100644 --- a/web/src/routes/(user)/publish/+page.svelte +++ b/web/src/routes/(user)/publish/+page.svelte @@ -5,6 +5,7 @@ import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { t } from 'svelte-i18n'; import AlbumsListPublish from '$lib/components/album-page/albums-list-publish.svelte'; + import { loadUser } from '$lib/utils/auth'; export let data: PageData; @@ -13,8 +14,7 @@ { - await authenticate(); - const sharedLinks = await getAllSharedLinksUnchecked(); - const sharedAlbums: AlbumResponseDto[] = sharedLinks.filter((link) => link.album).map((link) => link.album!); + await authenticate({published: true}); + const publishedAlbums: AlbumResponseDto[] = await getAllAlbums({published: true}) - const keys: Record = {}; - for (const link of sharedLinks) { - if (link.album) { - keys[link.album.id] = link.key; - } - } const $t = await getFormatter(); return { - sharedAlbums, - keys, + publishedAlbums, meta: { title: $t('albums'), },