mirror of
https://github.com/immich-app/immich
synced 2026-08-29 13:15:45 +00:00
fix(server): force full sync for non-admins when public users is disabled
Without this, devices that had already synced the full user list keep those cached records until a manual logout, since the sync protocol only upserts users and never prunes ones that quietly become invisible.
This commit is contained in:
parent
5cba070a2b
commit
cbf8e6da91
4 changed files with 89 additions and 3 deletions
|
|
@ -88,6 +88,20 @@ set
|
|||
where
|
||||
"userId" = $2
|
||||
|
||||
-- SessionRepository.requireFullSyncForNonAdmins
|
||||
update "session"
|
||||
set
|
||||
"isPendingSyncReset" = $1
|
||||
where
|
||||
"userId" in (
|
||||
select
|
||||
"user"."id"
|
||||
from
|
||||
"user"
|
||||
where
|
||||
"user"."isAdmin" = $2
|
||||
)
|
||||
|
||||
-- SessionRepository.resetSyncProgress
|
||||
begin
|
||||
update "session"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,15 @@ export class SessionRepository {
|
|||
await this.db.updateTable('session').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
async requireFullSyncForNonAdmins() {
|
||||
await this.db
|
||||
.updateTable('session')
|
||||
.set({ isPendingSyncReset: true })
|
||||
.where((eb) => eb('userId', 'in', eb.selectFrom('user').select('user.id').where('user.isAdmin', '=', false)))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async resetSyncProgress(sessionId: string) {
|
||||
await this.db.transaction().execute((tx) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com
|
|||
import { Insertable } from 'kysely';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { Writable } from 'node:stream';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
SyncAckDeleteDto,
|
||||
|
|
@ -12,7 +12,8 @@ import {
|
|||
SyncItem,
|
||||
SyncStreamDto,
|
||||
} from 'src/dtos/sync.dto';
|
||||
import { JobName, QueueName, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { ImmichWorker, JobName, QueueName, SyncEntityType, SyncRequestType } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { SyncQueryOptions } from 'src/repositories/sync.repository';
|
||||
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
|
@ -86,6 +87,13 @@ const throwSessionRequired = () => {
|
|||
|
||||
@Injectable()
|
||||
export class SyncService extends BaseService {
|
||||
@OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.Microservices] })
|
||||
async onConfigUpdate({ newConfig, oldConfig }: ArgOf<'ConfigUpdate'>) {
|
||||
if (oldConfig.server.publicUsers && !newConfig.server.publicUsers) {
|
||||
await this.sessionRepository.requireFullSyncForNonAdmins();
|
||||
}
|
||||
}
|
||||
|
||||
getAcks(auth: AuthDto) {
|
||||
const sessionId = auth.session?.id;
|
||||
if (!sessionId) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { schemaFromCode } from '@immich/sql-tools';
|
||||
import { Kysely } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { AssetMetadataKey, UserMetadataKey } from 'src/enum';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { BaseSync, SyncRepository } from 'src/repositories/sync.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { SyncService } from 'src/services/sync.service';
|
||||
|
|
@ -16,7 +18,7 @@ let defaultDatabase: Kysely<DB>;
|
|||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(SyncService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [DatabaseRepository, SyncRepository],
|
||||
real: [DatabaseRepository, SyncRepository, SessionRepository],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
};
|
||||
|
|
@ -33,6 +35,17 @@ const assertTableCount = async <T extends keyof DB>(db: Kysely<DB>, t: T, count:
|
|||
expect(results).toHaveLength(count);
|
||||
};
|
||||
|
||||
const withPublicUsers = (publicUsers: boolean) => ({ server: { publicUsers } }) as SystemConfig;
|
||||
|
||||
const isPendingSyncReset = async (db: Kysely<DB>, sessionId: string) => {
|
||||
const session = await db
|
||||
.selectFrom('session')
|
||||
.select('isPendingSyncReset')
|
||||
.where('id', '=', sessionId)
|
||||
.executeTakeFirstOrThrow();
|
||||
return session.isPendingSyncReset;
|
||||
};
|
||||
|
||||
describe(SyncService.name, () => {
|
||||
describe('onAuditTableCleanup', () => {
|
||||
it('should work', async () => {
|
||||
|
|
@ -240,4 +253,46 @@ describe(SyncService.name, () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('onConfigUpdate', () => {
|
||||
it('should require a full sync for non-admins when public users is disabled', async () => {
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const { user } = await ctx.newUser();
|
||||
const { session } = await ctx.newSession({ userId: user.id });
|
||||
|
||||
await sut.onConfigUpdate({ oldConfig: withPublicUsers(true), newConfig: withPublicUsers(false) });
|
||||
|
||||
await expect(isPendingSyncReset(ctx.database, session.id)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('should not require a full sync for admins when public users is disabled', async () => {
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const { user } = await ctx.newUser({ isAdmin: true });
|
||||
const { session } = await ctx.newSession({ userId: user.id });
|
||||
|
||||
await sut.onConfigUpdate({ oldConfig: withPublicUsers(true), newConfig: withPublicUsers(false) });
|
||||
|
||||
await expect(isPendingSyncReset(ctx.database, session.id)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should not require a full sync when public users is enabled', async () => {
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const { user } = await ctx.newUser();
|
||||
const { session } = await ctx.newSession({ userId: user.id });
|
||||
|
||||
await sut.onConfigUpdate({ oldConfig: withPublicUsers(false), newConfig: withPublicUsers(true) });
|
||||
|
||||
await expect(isPendingSyncReset(ctx.database, session.id)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should not require a full sync when public users was already disabled', async () => {
|
||||
const { sut, ctx } = setup(await getKyselyDB());
|
||||
const { user } = await ctx.newUser();
|
||||
const { session } = await ctx.newSession({ userId: user.id });
|
||||
|
||||
await sut.onConfigUpdate({ oldConfig: withPublicUsers(false), newConfig: withPublicUsers(false) });
|
||||
|
||||
await expect(isPendingSyncReset(ctx.database, session.id)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue