diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 6ca222518..0148f2e1e 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -94,6 +94,7 @@ export const signupResponseDto = { quotaSizeInBytes: null, status: 'active', license: null, + profileChangedAt: expect.any(String), }, }; diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index c9ae3ea65..86624ed06 100644 Binary files a/mobile/openapi/lib/model/create_profile_image_response_dto.dart and b/mobile/openapi/lib/model/create_profile_image_response_dto.dart differ diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 7c3cf03bd..375303c94 100644 Binary files a/mobile/openapi/lib/model/partner_response_dto.dart and b/mobile/openapi/lib/model/partner_response_dto.dart differ diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index af1ad3ad1..461596b7b 100644 Binary files a/mobile/openapi/lib/model/user_admin_response_dto.dart and b/mobile/openapi/lib/model/user_admin_response_dto.dart differ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 41c189984..282a5a40d 100644 Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index af7981556..fc813ae24 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8779,6 +8779,10 @@ }, "CreateProfileImageResponseDto": { "properties": { + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" }, @@ -8787,6 +8791,7 @@ } }, "required": [ + "profileChangedAt", "profileImagePath", "userId" ], @@ -10015,6 +10020,10 @@ "name": { "type": "string" }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" } @@ -10024,6 +10033,7 @@ "email", "id", "name", + "profileChangedAt", "profileImagePath" ], "type": "object" @@ -12454,6 +12464,10 @@ "oauthId": { "type": "string" }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" }, @@ -12492,6 +12506,7 @@ "license", "name", "oauthId", + "profileChangedAt", "profileImagePath", "quotaSizeInBytes", "quotaUsageInBytes", @@ -12653,6 +12668,10 @@ "name": { "type": "string" }, + "profileChangedAt": { + "format": "date-time", + "type": "string" + }, "profileImagePath": { "type": "string" } @@ -12662,6 +12681,7 @@ "email", "id", "name", + "profileChangedAt", "profileImagePath" ], "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index da5731369..dec8ad1e3 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -19,6 +19,7 @@ export type UserResponseDto = { email: string; id: string; name: string; + profileChangedAt: string; profileImagePath: string; }; export type ActivityResponseDto = { @@ -53,6 +54,7 @@ export type UserAdminResponseDto = { license: (UserLicense) | null; name: string; oauthId: string; + profileChangedAt: string; profileImagePath: string; quotaSizeInBytes: number | null; quotaUsageInBytes: number | null; @@ -669,6 +671,7 @@ export type PartnerResponseDto = { id: string; inTimeline?: boolean; name: string; + profileChangedAt: string; profileImagePath: string; }; export type UpdatePartnerDto = { @@ -1252,6 +1255,7 @@ export type CreateProfileImageDto = { file: Blob; }; export type CreateProfileImageResponseDto = { + profileChangedAt: string; profileImagePath: string; userId: string; }; diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 9659fa396..16eea373e 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -8,12 +8,6 @@ export class CreateProfileImageDto { export class CreateProfileImageResponseDto { userId!: string; + profileChangedAt!: Date; profileImagePath!: string; } - -export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { - return { - userId, - profileImagePath, - }; -} diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index f7cd70ee7..36f0b6386 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -32,6 +32,7 @@ export class UserResponseDto { profileImagePath!: string; @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) avatarColor!: UserAvatarColor; + profileChangedAt!: Date; } export class UserLicense { @@ -47,6 +48,7 @@ export const mapUser = (entity: UserEntity): UserResponseDto => { name: entity.name, profileImagePath: entity.profileImagePath, avatarColor: getPreferences(entity).avatar.color, + profileChangedAt: entity.profileChangedAt, }; }; diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 9cacad315..ea446be39 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -67,4 +67,7 @@ export class UserEntity { @OneToMany(() => UserMetadataEntity, (metadata) => metadata.user) metadata!: UserMetadataEntity[]; + + @Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + profileChangedAt!: Date; } diff --git a/server/src/migrations/1726491047923-AddprofileChangedAt.ts b/server/src/migrations/1726491047923-AddprofileChangedAt.ts new file mode 100644 index 000000000..bcf568426 --- /dev/null +++ b/server/src/migrations/1726491047923-AddprofileChangedAt.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddprofileChangedAt1726491047923 implements MigrationInterface { + name = 'AddprofileChangedAt1726491047923' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "profileChangedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profileChangedAt"`); + } + +} diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3f3e04140..44042c0e6 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -23,7 +23,8 @@ SELECT "ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status", "ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt", "ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes", - "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes" + "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes", + "ActivityEntity__ActivityEntity_user"."profileChangedAt" AS "ActivityEntity__ActivityEntity_user_profileChangedAt" FROM "activity" "ActivityEntity" LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId" diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 729f7c7f2..b9531a43d 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -30,6 +30,7 @@ FROM "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -47,6 +48,7 @@ FROM "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "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", @@ -106,6 +108,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -122,7 +125,8 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -164,6 +168,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -180,7 +185,8 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -299,6 +305,7 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "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", @@ -324,7 +331,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -372,6 +380,7 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "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", @@ -397,7 +406,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -495,7 +505,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -553,7 +564,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index e5f389ac4..f4989d355 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -24,6 +24,7 @@ FROM "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes", + "APIKeyEntity__APIKeyEntity_user"."profileChangedAt" AS "APIKeyEntity__APIKeyEntity_user_profileChangedAt", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value" diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 5dd32ce36..a5d6ba05d 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -28,7 +28,8 @@ FROM "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -68,7 +69,8 @@ SELECT "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -104,7 +106,8 @@ SELECT "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 17fff94f4..2f0613b4d 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -39,6 +39,7 @@ FROM "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt", "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes", "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes", + "SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value" diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 10af8d17d..f3b3c3140 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -156,7 +156,8 @@ FROM "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -257,7 +258,8 @@ SELECT "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -309,7 +311,8 @@ FROM "SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status", "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt", "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes", - "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes" + "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes", + "SharedLinkEntity__SharedLinkEntity_user"."profileChangedAt" AS "SharedLinkEntity__SharedLinkEntity_user_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId" diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 2c75786f9..ab0a6cc53 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -15,7 +15,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE @@ -60,7 +61,8 @@ SELECT "user"."status" AS "user_status", "user"."updatedAt" AS "user_updatedAt", "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes", - "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes" + "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes", + "user"."profileChangedAt" AS "user_profileChangedAt" FROM "users" "user" WHERE @@ -82,7 +84,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE @@ -106,7 +109,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 92404a695..cf918198a 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -7,7 +7,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; -import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; +import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; @@ -93,13 +93,23 @@ export class UserService { return mapUser(user); } - async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise { + async createProfileImage(auth: AuthDto, file: Express.Multer.File): Promise { const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false }); - const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path }); + + const user = await this.userRepository.update(auth.user.id, { + profileImagePath: file.path, + profileChangedAt: new Date(), + }); + if (oldpath !== '') { await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); } - return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); + + return { + userId: user.id, + profileImagePath: user.profileImagePath, + profileChangedAt: user.profileChangedAt, + }; } async deleteProfileImage(auth: AuthDto): Promise { @@ -107,7 +117,7 @@ export class UserService { if (user.profileImagePath === '') { throw new BadRequestException("Can't delete a missing profile Image"); } - await this.userRepository.update(auth.user.id, { profileImagePath: '' }); + await this.userRepository.update(auth.user.id, { profileImagePath: '', profileChangedAt: new Date() }); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); } diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte index 0c3f79895..3dabd86d4 100644 --- a/web/src/lib/components/shared-components/profile-image-cropper.svelte +++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte @@ -56,13 +56,14 @@ return; } const file = new File([blob], 'profile-picture.png', { type: 'image/png' }); - const { profileImagePath } = await createProfileImage({ createProfileImageDto: { file } }); + const { profileImagePath, profileChangedAt } = await createProfileImage({ createProfileImageDto: { file } }); notificationController.show({ type: NotificationType.Info, message: $t('profile_picture_set'), timeout: 3000, }); $user.profileImagePath = profileImagePath; + $user.profileChangedAt = profileChangedAt; } catch (error) { handleError(error, $t('errors.unable_to_set_profile_picture')); } diff --git a/web/src/lib/components/shared-components/user-avatar.svelte b/web/src/lib/components/shared-components/user-avatar.svelte index bb59af9aa..74750953b 100644 --- a/web/src/lib/components/shared-components/user-avatar.svelte +++ b/web/src/lib/components/shared-components/user-avatar.svelte @@ -13,6 +13,7 @@ email: string; profileImagePath: string; avatarColor: UserAvatarColor; + profileChangedAt: string; } export let user: User; @@ -79,7 +80,7 @@ {#if showProfileImage && user.profileImagePath} {$t('profile_image_of_user', createUrl(getUserProfileImagePath(userId)); +export const getProfileImageUrl = (user: UserResponseDto) => + createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt }); export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) => createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt }); diff --git a/web/src/test-data/factories/user-factory.ts b/web/src/test-data/factories/user-factory.ts index c89dccb2b..92d1510d4 100644 --- a/web/src/test-data/factories/user-factory.ts +++ b/web/src/test-data/factories/user-factory.ts @@ -8,6 +8,7 @@ export const userFactory = Sync.makeFactory({ name: Sync.each(() => faker.person.fullName()), profileImagePath: '', avatarColor: UserAvatarColor.Primary, + profileChangedAt: Sync.each(() => faker.date.recent().toISOString()), }); export const userAdminFactory = Sync.makeFactory({ @@ -31,4 +32,5 @@ export const userAdminFactory = Sync.makeFactory({ activationKey: 'activation-key', activatedAt: new Date().toISOString(), }, + profileChangedAt: Sync.each(() => faker.date.recent().toISOString()), });