chore: transition to nullable user.oauthId (#31560)
This commit is contained in:
@@ -4,10 +4,13 @@ enum ServerCapability {
|
||||
// Feature Support
|
||||
cloudIdMetadata(SemVer(major: 2, minor: 4, patch: 0)),
|
||||
bulkCloudIdMetadata(SemVer(major: 2, minor: 5, patch: 0)),
|
||||
assetEdits(SemVer(major: 2, minor: 6, patch: 0)),
|
||||
assetFacesV2(SemVer(major: 2, minor: 6, patch: 0)),
|
||||
|
||||
// Sync
|
||||
syncV2(SemVer(major: 3, minor: 0, patch: 0)),
|
||||
assetOcr(SemVer(major: 3, minor: 0, patch: 0)),
|
||||
syncAssetEditsV1(SemVer(major: 2, minor: 6, patch: 0)),
|
||||
syncAssetFacesV2(SemVer(major: 2, minor: 6, patch: 0)),
|
||||
syncAssetOcrV1(SemVer(major: 3, minor: 0, patch: 0)),
|
||||
syncAuthUsersV2(SemVer(major: 3, minor: 3, patch: 0)),
|
||||
|
||||
// Migrations
|
||||
assetPayloadChange20260128(SemVer(major: 2, minor: 5, patch: 0)),
|
||||
|
||||
@@ -194,6 +194,8 @@ class SyncStreamService {
|
||||
switch (type) {
|
||||
case SyncEntityType.authUserV1:
|
||||
return _syncStreamRepository.updateAuthUsersV1(data.cast());
|
||||
case SyncEntityType.authUserV2:
|
||||
return _syncStreamRepository.updateAuthUsersV2(data.cast());
|
||||
case SyncEntityType.userV1:
|
||||
return _syncStreamRepository.updateUsersV1(data.cast());
|
||||
case SyncEntityType.userDeleteV1:
|
||||
|
||||
@@ -43,11 +43,11 @@ class SyncApiRepository {
|
||||
request.body = jsonEncode(
|
||||
SyncStreamDto(
|
||||
types: [
|
||||
SyncRequestType.authUsersV1,
|
||||
serverVersion.supports(.syncAuthUsersV2) ? SyncRequestType.authUsersV2 : SyncRequestType.authUsersV1,
|
||||
SyncRequestType.usersV1,
|
||||
serverVersion.supports(.syncV2) ? SyncRequestType.assetsV2 : SyncRequestType.assetsV1,
|
||||
SyncRequestType.assetExifsV1,
|
||||
if (serverVersion.supports(.assetEdits)) SyncRequestType.assetEditsV1,
|
||||
if (serverVersion.supports(.syncAssetEditsV1)) SyncRequestType.assetEditsV1,
|
||||
SyncRequestType.assetMetadataV1,
|
||||
SyncRequestType.partnersV1,
|
||||
serverVersion.supports(.syncV2) ? SyncRequestType.partnerAssetsV2 : SyncRequestType.partnerAssetsV1,
|
||||
@@ -63,8 +63,8 @@ class SyncApiRepository {
|
||||
SyncRequestType.partnerStacksV1,
|
||||
SyncRequestType.userMetadataV1,
|
||||
SyncRequestType.peopleV1,
|
||||
serverVersion.supports(.assetFacesV2) ? SyncRequestType.assetFacesV2 : SyncRequestType.assetFacesV1,
|
||||
if (serverVersion.supports(.assetOcr)) SyncRequestType.assetOcrV1,
|
||||
serverVersion.supports(.syncAssetFacesV2) ? SyncRequestType.assetFacesV2 : SyncRequestType.assetFacesV1,
|
||||
if (serverVersion.supports(.syncAssetOcrV1)) SyncRequestType.assetOcrV1,
|
||||
],
|
||||
).toJson(),
|
||||
);
|
||||
@@ -140,6 +140,7 @@ class SyncApiRepository {
|
||||
|
||||
const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.authUserV1: SyncAuthUserV1.fromJson,
|
||||
SyncEntityType.authUserV2: SyncAuthUserV2.fromJson,
|
||||
SyncEntityType.userV1: SyncUserV1.fromJson,
|
||||
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
|
||||
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
||||
|
||||
@@ -116,6 +116,35 @@ class SyncStreamRepository extends DatabaseAccessor<Drift> with $SyncStreamRepos
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAuthUsersV2(Iterable<SyncAuthUserV2> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final user in data) {
|
||||
final companion = AuthUserEntityCompanion(
|
||||
name: Value(user.name),
|
||||
email: Value(user.email),
|
||||
hasProfileImage: Value(user.hasProfileImage),
|
||||
profileChangedAt: Value(user.profileChangedAt),
|
||||
avatarColor: Value(user.avatarColor.orElse(null)?.toAvatarColor() ?? AvatarColor.primary),
|
||||
isAdmin: Value(user.isAdmin),
|
||||
pinCode: Value(user.pinCode),
|
||||
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
|
||||
quotaUsageInBytes: Value(user.quotaUsageInBytes),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.authUserEntity,
|
||||
companion.copyWith(id: Value(user.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: SyncAuthUserV2', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/error_handler.dart';
|
||||
|
||||
final _stateProvider = Provider.family.autoDispose<RemoteAsset?, ActionSource>((ref, source) {
|
||||
final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion.supports(.assetEdits)));
|
||||
final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion.supports(.syncAssetEditsV1)));
|
||||
if (!isSupported) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -30710,6 +30710,98 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAuthUserV2": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"deletedAt": {
|
||||
"description": "User deleted at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"description": "User email",
|
||||
"type": "string"
|
||||
},
|
||||
"hasProfileImage": {
|
||||
"description": "User has profile image",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"description": "User ID",
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
},
|
||||
"isAdmin": {
|
||||
"description": "User is admin",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "User name",
|
||||
"type": "string"
|
||||
},
|
||||
"oauthId": {
|
||||
"description": "User OAuth ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"pinCode": {
|
||||
"description": "User pin code",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"profileChangedAt": {
|
||||
"description": "User profile changed at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"type": "string"
|
||||
},
|
||||
"quotaSizeInBytes": {
|
||||
"description": "Quota size in bytes",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"quotaUsageInBytes": {
|
||||
"description": "Quota usage in bytes",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer"
|
||||
},
|
||||
"storageLabel": {
|
||||
"description": "User storage label",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"deletedAt",
|
||||
"email",
|
||||
"hasProfileImage",
|
||||
"id",
|
||||
"isAdmin",
|
||||
"name",
|
||||
"oauthId",
|
||||
"pinCode",
|
||||
"profileChangedAt",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"storageLabel"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncCompleteV1": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
@@ -30718,6 +30810,7 @@
|
||||
"description": "Sync entity type",
|
||||
"enum": [
|
||||
"AuthUserV1",
|
||||
"AuthUserV2",
|
||||
"UserV1",
|
||||
"UserDeleteV1",
|
||||
"AssetV1",
|
||||
@@ -31082,6 +31175,7 @@
|
||||
"AssetMetadataV1",
|
||||
"AssetOcrV1",
|
||||
"AuthUsersV1",
|
||||
"AuthUsersV2",
|
||||
"MemoriesV1",
|
||||
"MemoryToAssetsV1",
|
||||
"PartnersV1",
|
||||
|
||||
@@ -3594,6 +3594,33 @@ export type SyncAuthUserV1 = {
|
||||
/** User storage label */
|
||||
storageLabel: string | null;
|
||||
};
|
||||
export type SyncAuthUserV2 = {
|
||||
avatarColor?: (UserAvatarColor) | null;
|
||||
/** User deleted at */
|
||||
deletedAt: string | null;
|
||||
/** User email */
|
||||
email: string;
|
||||
/** User has profile image */
|
||||
hasProfileImage: boolean;
|
||||
/** User ID */
|
||||
id: string;
|
||||
/** User is admin */
|
||||
isAdmin: boolean;
|
||||
/** User name */
|
||||
name: string;
|
||||
/** User OAuth ID */
|
||||
oauthId: string | null;
|
||||
/** User pin code */
|
||||
pinCode: string | null;
|
||||
/** User profile changed at */
|
||||
profileChangedAt: string;
|
||||
/** Quota size in bytes */
|
||||
quotaSizeInBytes: number | null;
|
||||
/** Quota usage in bytes */
|
||||
quotaUsageInBytes: number;
|
||||
/** User storage label */
|
||||
storageLabel: string | null;
|
||||
};
|
||||
export type SyncCompleteV1 = {};
|
||||
export type SyncMemoryAssetDeleteV1 = {
|
||||
/** Asset ID */
|
||||
@@ -8306,6 +8333,7 @@ export enum AssetIdErrorReason {
|
||||
}
|
||||
export enum SyncEntityType {
|
||||
AuthUserV1 = "AuthUserV1",
|
||||
AuthUserV2 = "AuthUserV2",
|
||||
UserV1 = "UserV1",
|
||||
UserDeleteV1 = "UserDeleteV1",
|
||||
AssetV1 = "AssetV1",
|
||||
@@ -8380,6 +8408,7 @@ export enum SyncRequestType {
|
||||
AssetMetadataV1 = "AssetMetadataV1",
|
||||
AssetOcrV1 = "AssetOcrV1",
|
||||
AuthUsersV1 = "AuthUsersV1",
|
||||
AuthUsersV2 = "AuthUsersV2",
|
||||
MemoriesV1 = "MemoriesV1",
|
||||
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
||||
PartnersV1 = "PartnersV1",
|
||||
|
||||
@@ -140,7 +140,7 @@ export type UserAdmin = User & {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date | null;
|
||||
oauthId: string;
|
||||
oauthId: string | null;
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number;
|
||||
status: UserStatus;
|
||||
|
||||
@@ -40,6 +40,10 @@ const SyncAuthUserV1Schema = SyncUserV1Schema.merge(
|
||||
}),
|
||||
).meta({ id: 'SyncAuthUserV1' });
|
||||
|
||||
const SyncAuthUserV2Schema = SyncAuthUserV1Schema.extend({
|
||||
oauthId: z.string().nullable().describe('User OAuth ID'),
|
||||
}).meta({ id: 'SyncAuthUserV2' });
|
||||
|
||||
const SyncUserDeleteV1Schema = z.object({ userId: z.uuidv4().describe('User ID') }).meta({ id: 'SyncUserDeleteV1' });
|
||||
|
||||
const SyncPartnerV1Schema = z
|
||||
@@ -112,6 +116,8 @@ class SyncUserV1 extends createZodDto(SyncUserV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAuthUserV1 extends createZodDto(SyncAuthUserV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAuthUserV2 extends createZodDto(SyncAuthUserV2Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncUserDeleteV1 extends createZodDto(SyncUserDeleteV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {}
|
||||
@@ -463,6 +469,7 @@ class SyncCompleteV1 extends createZodDto(SyncCompleteV1Schema) {}
|
||||
|
||||
export type SyncItem = {
|
||||
[SyncEntityType.AuthUserV1]: SyncAuthUserV1;
|
||||
[SyncEntityType.AuthUserV2]: SyncAuthUserV2;
|
||||
[SyncEntityType.UserV1]: SyncUserV1;
|
||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
||||
|
||||
@@ -151,7 +151,8 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
|
||||
createdAt: entity.createdAt,
|
||||
deletedAt: entity.deletedAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
oauthId: entity.oauthId,
|
||||
// TODO(v4): remove the mapping and make `oauthId` nullable
|
||||
oauthId: entity.oauthId ?? '',
|
||||
quotaSizeInBytes: entity.quotaSizeInBytes,
|
||||
quotaUsageInBytes: entity.quotaUsageInBytes,
|
||||
status: entity.status,
|
||||
|
||||
@@ -1024,6 +1024,7 @@ export enum SyncRequestType {
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
AssetOcrV1 = 'AssetOcrV1',
|
||||
AuthUsersV1 = 'AuthUsersV1',
|
||||
AuthUsersV2 = 'AuthUsersV2',
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
PartnersV1 = 'PartnersV1',
|
||||
@@ -1048,6 +1049,7 @@ export const SyncRequestTypeSchema = z
|
||||
|
||||
export enum SyncEntityType {
|
||||
AuthUserV1 = 'AuthUserV1',
|
||||
AuthUserV2 = 'AuthUserV2',
|
||||
|
||||
UserV1 = 'UserV1',
|
||||
UserDeleteV1 = 'UserDeleteV1',
|
||||
|
||||
@@ -130,7 +130,7 @@ type UserEvent = {
|
||||
isAdmin: boolean;
|
||||
shouldChangePassword: boolean;
|
||||
avatarColor: UserAvatarColor | null;
|
||||
oauthId: string;
|
||||
oauthId: string | null;
|
||||
storageLabel: string | null;
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" DROP NOT NULL;`.execute(db);
|
||||
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" SET DEFAULT NULL;`.execute(db);
|
||||
await sql`UPDATE "user" SET "oauthId" = NULL WHERE "oauthId" = '';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`UPDATE "user" SET "oauthId" = '' WHERE "oauthId" IS NULL;`.execute(db);
|
||||
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" SET DEFAULT '';`.execute(db);
|
||||
await sql`ALTER TABLE "user" ALTER COLUMN "oauthId" SET NOT NULL;`.execute(db);
|
||||
}
|
||||
@@ -94,3 +94,4 @@
|
||||
1786972746372-AssetOcrSyncReset
|
||||
1787148183729-ClusterGroups
|
||||
1787148183730-DeleteMismatchedMemoryAssets
|
||||
1789419229196-ConvertUserOAuthIdEmptyStringToNull
|
||||
|
||||
@@ -57,8 +57,8 @@ export class UserTable {
|
||||
@DeleteDateColumn()
|
||||
deletedAt!: Timestamp | null;
|
||||
|
||||
@Column({ default: '' })
|
||||
oauthId!: Generated<string>;
|
||||
@Column({ nullable: true })
|
||||
oauthId!: string | null;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Generated<Timestamp>;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { BaseService } from 'src/services/base.service.js';
|
||||
@Injectable()
|
||||
export class AuthAdminService extends BaseService {
|
||||
async unlinkAll(_auth: AuthDto) {
|
||||
// TODO replace '' with null
|
||||
await this.userRepository.updateAll({ oauthId: '' });
|
||||
await this.userRepository.updateAll({ oauthId: null });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,12 +1057,13 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
|
||||
it('should not sync the profile picture if the user already has one', async () => {
|
||||
const user = UserFactory.create({ oauthId: 'oauth-id', profileImagePath: 'not-empty' });
|
||||
const oauthId = 'oauth-id';
|
||||
const user = UserFactory.create({ oauthId, profileImagePath: 'not-empty' });
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
|
||||
profile: OAuthProfileFactory.create({
|
||||
sub: user.oauthId,
|
||||
sub: oauthId,
|
||||
email: user.email,
|
||||
picture: 'https://auth.immich.cloud/profiles/1.jpg',
|
||||
}),
|
||||
@@ -1181,11 +1182,12 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
|
||||
it('should promote an existing user to admin if the role claim contains admin on login', async () => {
|
||||
const user = UserFactory.create({ isAdmin: false, oauthId: 'oauth-id' });
|
||||
const oauthId = 'oauth-id';
|
||||
const user = UserFactory.create({ isAdmin: false, oauthId });
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
|
||||
profile: OAuthProfileFactory.create({ sub: user.oauthId, immich_role: 'admin' }),
|
||||
profile: OAuthProfileFactory.create({ sub: oauthId, immich_role: 'admin' }),
|
||||
});
|
||||
mocks.user.getByOAuthId.mockResolvedValue(user);
|
||||
mocks.user.update.mockResolvedValue({ ...user, isAdmin: true });
|
||||
@@ -1201,11 +1203,12 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
|
||||
it('should demote an existing admin if the role claim only contains user on login', async () => {
|
||||
const user = UserFactory.create({ isAdmin: true, oauthId: 'oauth-id' });
|
||||
const oauthId = 'oauth-id';
|
||||
const user = UserFactory.create({ isAdmin: true, oauthId });
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
|
||||
profile: OAuthProfileFactory.create({ sub: user.oauthId, immich_role: ['user'] }),
|
||||
profile: OAuthProfileFactory.create({ sub: oauthId, immich_role: ['user'] }),
|
||||
});
|
||||
mocks.user.getByOAuthId.mockResolvedValue(user);
|
||||
mocks.user.update.mockResolvedValue({ ...user, isAdmin: false });
|
||||
@@ -1221,11 +1224,12 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
|
||||
it('should not change isAdmin for an existing user if the role claim is blank', async () => {
|
||||
const user = UserFactory.create({ isAdmin: true, oauthId: 'oauth-id' });
|
||||
const oauthId = 'oauth-id';
|
||||
const user = UserFactory.create({ isAdmin: true, oauthId });
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.oauth.getProfileAndOAuthSid.mockResolvedValue({
|
||||
profile: OAuthProfileFactory.create({ sub: user.oauthId }),
|
||||
profile: OAuthProfileFactory.create({ sub: oauthId }),
|
||||
});
|
||||
mocks.user.getByOAuthId.mockResolvedValue(user);
|
||||
mocks.session.create.mockResolvedValue(SessionFactory.create());
|
||||
@@ -1334,7 +1338,7 @@ describe(AuthService.name, () => {
|
||||
|
||||
await sut.unlink(auth);
|
||||
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: '' });
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: null });
|
||||
});
|
||||
|
||||
it('should unlink an account and remove the OAuth data from the session', async () => {
|
||||
@@ -1349,7 +1353,7 @@ describe(AuthService.name, () => {
|
||||
await sut.unlink(auth);
|
||||
|
||||
expect(mocks.session.update).toHaveBeenCalledWith(session.id, { oauthSid: null, oauthBearerToken: null });
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: '' });
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(auth.user.id, { oauthId: null });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -441,7 +441,7 @@ export class AuthService extends BaseService {
|
||||
await this.sessionRepository.update(auth.session.id, { oauthSid: null, oauthBearerToken: null });
|
||||
}
|
||||
|
||||
const user = await this.userRepository.update(auth.user.id, { oauthId: '' });
|
||||
const user = await this.userRepository.update(auth.user.id, { oauthId: null });
|
||||
return mapUserAdmin(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ const sendEntityBackfillCompleteAck = async (response: Writable, ackType: SyncEn
|
||||
|
||||
export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.AuthUsersV1,
|
||||
SyncRequestType.AuthUsersV2,
|
||||
SyncRequestType.UsersV1,
|
||||
SyncRequestType.PartnersV1,
|
||||
SyncRequestType.AssetsV1,
|
||||
@@ -190,6 +191,7 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(),
|
||||
|
||||
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AuthUsersV2]: () => this.syncAuthUsersV2(options, response, checkpointMap),
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV2]: () => this.syncAssetsV2(options, response, checkpointMap),
|
||||
@@ -266,6 +268,18 @@ export class SyncService extends BaseService {
|
||||
private async syncAuthUsersV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const upsertType = SyncEntityType.AuthUserV1;
|
||||
const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
||||
await send(response, {
|
||||
type: upsertType,
|
||||
ids: [updateId],
|
||||
data: { ...data, oauthId: data.oauthId ?? '', hasProfileImage: !!profileImagePath },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAuthUsersV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const upsertType = SyncEntityType.AuthUserV2;
|
||||
const upserts = this.syncRepository.authUser.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, profileImagePath, ...data } of upserts) {
|
||||
await send(response, {
|
||||
type: upsertType,
|
||||
|
||||
@@ -27,7 +27,7 @@ export class UserFactory {
|
||||
shouldChangePassword: false,
|
||||
avatarColor: null,
|
||||
deletedAt: null,
|
||||
oauthId: '',
|
||||
oauthId: null,
|
||||
updatedAt: newDate(),
|
||||
storageLabel: null,
|
||||
name: 'Test User',
|
||||
|
||||
Vendored
+2
-2
@@ -11,7 +11,7 @@ export const userStub = {
|
||||
name: 'admin_name',
|
||||
id: 'admin_id',
|
||||
storageLabel: 'admin',
|
||||
oauthId: '',
|
||||
oauthId: null,
|
||||
shouldChangePassword: false,
|
||||
avatarColor: null,
|
||||
profileImagePath: '',
|
||||
@@ -29,7 +29,7 @@ export const userStub = {
|
||||
profileChangedAt: new Date('2021-01-01'),
|
||||
name: 'immich_name',
|
||||
storageLabel: null,
|
||||
oauthId: '',
|
||||
oauthId: null,
|
||||
shouldChangePassword: false,
|
||||
avatarColor: null,
|
||||
profileImagePath: '',
|
||||
|
||||
@@ -761,7 +761,7 @@ const userInsert = (user: Partial<Insertable<UserTable>> & { clusterGroupId: str
|
||||
shouldChangePassword: true,
|
||||
storageLabel: null,
|
||||
pinCode: null,
|
||||
oauthId: '',
|
||||
oauthId: null,
|
||||
avatarColor: null,
|
||||
quotaSizeInBytes: null,
|
||||
quotaUsageInBytes: 0,
|
||||
|
||||
@@ -31,7 +31,7 @@ describe(AuthAdminService.name, () => {
|
||||
|
||||
await expect(sut.unlinkAll(auth)).resolves.toBeUndefined();
|
||||
await expect(userRepo.get(user.id, { withDeleted: true })).resolves.toEqual(
|
||||
expect.objectContaining({ oauthId: '' }),
|
||||
expect.objectContaining({ oauthId: null }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ describe(AuthAdminService.name, () => {
|
||||
|
||||
await expect(sut.unlinkAll(auth)).resolves.toBeUndefined();
|
||||
await expect(userRepo.get(user.id, { withDeleted: true })).resolves.toEqual(
|
||||
expect.objectContaining({ oauthId: '' }),
|
||||
expect.objectContaining({ oauthId: null }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -56,10 +56,10 @@ describe(AuthAdminService.name, () => {
|
||||
|
||||
await expect(sut.unlinkAll(auth)).resolves.toBeUndefined();
|
||||
await expect(userRepo.get(user1.id, { withDeleted: true })).resolves.toEqual(
|
||||
expect.objectContaining({ oauthId: '' }),
|
||||
expect.objectContaining({ oauthId: null }),
|
||||
);
|
||||
await expect(userRepo.get(user2.id, { withDeleted: true })).resolves.toEqual(
|
||||
expect.objectContaining({ oauthId: '' }),
|
||||
expect.objectContaining({ oauthId: null }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,11 @@ beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.AuthUserV1, () => {
|
||||
describe(SyncEntityType.AuthUserV2, () => {
|
||||
it('should detect and sync the first user', async () => {
|
||||
const { auth, user, ctx } = await setup(await getKyselyDB());
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -40,13 +40,13 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
quotaUsageInBytes: user.quotaUsageInBytes,
|
||||
storageLabel: user.storageLabel,
|
||||
},
|
||||
type: 'AuthUserV1',
|
||||
type: 'AuthUserV2',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AuthUsersV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AuthUsersV2]);
|
||||
});
|
||||
|
||||
it('should sync a change and then another change to that same user', async () => {
|
||||
@@ -54,7 +54,7 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
|
||||
const userRepo = ctx.get(UserRepository);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -62,7 +62,7 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
id: user.id,
|
||||
isAdmin: false,
|
||||
}),
|
||||
type: 'AuthUserV1',
|
||||
type: 'AuthUserV2',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
@@ -71,7 +71,7 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
|
||||
await userRepo.update(user.id, { isAdmin: true });
|
||||
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -79,7 +79,7 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
id: user.id,
|
||||
isAdmin: true,
|
||||
}),
|
||||
type: 'AuthUserV1',
|
||||
type: 'AuthUserV2',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
@@ -90,7 +90,7 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
|
||||
await ctx.newUser();
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AuthUsersV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -98,7 +98,7 @@ describe(SyncEntityType.AuthUserV1, () => {
|
||||
id: user.id,
|
||||
isAdmin: false,
|
||||
}),
|
||||
type: 'AuthUserV1',
|
||||
type: 'AuthUserV2',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
@@ -120,7 +120,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
|
||||
createdAt = newDate(),
|
||||
updatedAt = newDate(),
|
||||
deletedAt = null,
|
||||
oauthId = '',
|
||||
oauthId = null,
|
||||
quotaSizeInBytes = null,
|
||||
quotaUsageInBytes = 0,
|
||||
status = UserStatus.Active,
|
||||
|
||||
Reference in New Issue
Block a user