pull/21601/merge
aviv926 2025-12-14 00:06:41 +01:00 committed by GitHub
commit 78df3530f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 146 additions and 19 deletions

View File

@ -132,6 +132,8 @@ describe('/server', () => {
oauthButtonText: 'Login with OAuth',
trashDays: 30,
userDeleteDelay: 7,
sessionDeleteDelayBrowser: 90,
sessionDeleteDelayMobile: 90,
isInitialized: true,
externalDomain: '',
publicUsers: true,

View File

@ -299,7 +299,10 @@
"server_stats_page_description": "Admin server statistics page",
"server_welcome_message": "Welcome message",
"server_welcome_message_description": "A message that is displayed on the login page.",
"settings_page_description": "Admin settings page",
"session_delete_delay_browser_settings": "Session delete delay (browser)",
"session_delete_delay_browser_settings_description": "Number of days after last update to delete browser sessions. Sessions will be deleted if not updated within this period.",
"session_delete_delay_mobile_settings": "Session delete delay (mobile)",
"session_delete_delay_mobile_settings_description": "Number of days after last update to delete mobile sessions. Sessions will be deleted if not updated within this period.",
"sidecar_job": "Sidecar metadata",
"sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem",
"slideshow_duration_description": "Number of seconds to display each image",

View File

@ -22,6 +22,8 @@ class ServerConfigDto {
required this.mapLightStyleUrl,
required this.oauthButtonText,
required this.publicUsers,
required this.sessionDeleteDelayBrowser,
required this.sessionDeleteDelayMobile,
required this.trashDays,
required this.userDeleteDelay,
});
@ -44,6 +46,10 @@ class ServerConfigDto {
bool publicUsers;
int sessionDeleteDelayBrowser;
int sessionDeleteDelayMobile;
int trashDays;
int userDeleteDelay;
@ -59,6 +65,8 @@ class ServerConfigDto {
other.mapLightStyleUrl == mapLightStyleUrl &&
other.oauthButtonText == oauthButtonText &&
other.publicUsers == publicUsers &&
other.sessionDeleteDelayBrowser == sessionDeleteDelayBrowser &&
other.sessionDeleteDelayMobile == sessionDeleteDelayMobile &&
other.trashDays == trashDays &&
other.userDeleteDelay == userDeleteDelay;
@ -74,11 +82,13 @@ class ServerConfigDto {
(mapLightStyleUrl.hashCode) +
(oauthButtonText.hashCode) +
(publicUsers.hashCode) +
(sessionDeleteDelayBrowser.hashCode) +
(sessionDeleteDelayMobile.hashCode) +
(trashDays.hashCode) +
(userDeleteDelay.hashCode);
@override
String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, maintenanceMode=$maintenanceMode, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, publicUsers=$publicUsers, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]';
String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, maintenanceMode=$maintenanceMode, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, publicUsers=$publicUsers, sessionDeleteDelayBrowser=$sessionDeleteDelayBrowser, sessionDeleteDelayMobile=$sessionDeleteDelayMobile, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -91,6 +101,8 @@ class ServerConfigDto {
json[r'mapLightStyleUrl'] = this.mapLightStyleUrl;
json[r'oauthButtonText'] = this.oauthButtonText;
json[r'publicUsers'] = this.publicUsers;
json[r'sessionDeleteDelayBrowser'] = this.sessionDeleteDelayBrowser;
json[r'sessionDeleteDelayMobile'] = this.sessionDeleteDelayMobile;
json[r'trashDays'] = this.trashDays;
json[r'userDeleteDelay'] = this.userDeleteDelay;
return json;
@ -114,6 +126,8 @@ class ServerConfigDto {
mapLightStyleUrl: mapValueOfType<String>(json, r'mapLightStyleUrl')!,
oauthButtonText: mapValueOfType<String>(json, r'oauthButtonText')!,
publicUsers: mapValueOfType<bool>(json, r'publicUsers')!,
sessionDeleteDelayBrowser: mapValueOfType<int>(json, r'sessionDeleteDelayBrowser')!,
sessionDeleteDelayMobile: mapValueOfType<int>(json, r'sessionDeleteDelayMobile')!,
trashDays: mapValueOfType<int>(json, r'trashDays')!,
userDeleteDelay: mapValueOfType<int>(json, r'userDeleteDelay')!,
);
@ -172,6 +186,8 @@ class ServerConfigDto {
'mapLightStyleUrl',
'oauthButtonText',
'publicUsers',
'sessionDeleteDelayBrowser',
'sessionDeleteDelayMobile',
'trashDays',
'userDeleteDelay',
};

View File

@ -14,26 +14,40 @@ class SystemConfigUserDto {
/// Returns a new [SystemConfigUserDto] instance.
SystemConfigUserDto({
required this.deleteDelay,
required this.sessionDeleteDelayBrowser,
required this.sessionDeleteDelayMobile,
});
/// Minimum value: 1
int deleteDelay;
/// Minimum value: 1
int sessionDeleteDelayBrowser;
/// Minimum value: 1
int sessionDeleteDelayMobile;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigUserDto &&
other.deleteDelay == deleteDelay;
other.deleteDelay == deleteDelay &&
other.sessionDeleteDelayBrowser == sessionDeleteDelayBrowser &&
other.sessionDeleteDelayMobile == sessionDeleteDelayMobile;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(deleteDelay.hashCode);
(deleteDelay.hashCode) +
(sessionDeleteDelayBrowser.hashCode) +
(sessionDeleteDelayMobile.hashCode);
@override
String toString() => 'SystemConfigUserDto[deleteDelay=$deleteDelay]';
String toString() => 'SystemConfigUserDto[deleteDelay=$deleteDelay, sessionDeleteDelayBrowser=$sessionDeleteDelayBrowser, sessionDeleteDelayMobile=$sessionDeleteDelayMobile]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'deleteDelay'] = this.deleteDelay;
json[r'sessionDeleteDelayBrowser'] = this.sessionDeleteDelayBrowser;
json[r'sessionDeleteDelayMobile'] = this.sessionDeleteDelayMobile;
return json;
}
@ -47,6 +61,8 @@ class SystemConfigUserDto {
return SystemConfigUserDto(
deleteDelay: mapValueOfType<int>(json, r'deleteDelay')!,
sessionDeleteDelayBrowser: mapValueOfType<int>(json, r'sessionDeleteDelayBrowser')!,
sessionDeleteDelayMobile: mapValueOfType<int>(json, r'sessionDeleteDelayMobile')!,
);
}
return null;
@ -95,6 +111,8 @@ class SystemConfigUserDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'deleteDelay',
'sessionDeleteDelayBrowser',
'sessionDeleteDelayMobile',
};
}

View File

@ -19194,6 +19194,12 @@
"publicUsers": {
"type": "boolean"
},
"sessionDeleteDelayBrowser": {
"type": "integer"
},
"sessionDeleteDelayMobile": {
"type": "integer"
},
"trashDays": {
"type": "integer"
},
@ -19211,6 +19217,8 @@
"mapLightStyleUrl",
"oauthButtonText",
"publicUsers",
"sessionDeleteDelayBrowser",
"sessionDeleteDelayMobile",
"trashDays",
"userDeleteDelay"
],
@ -22111,10 +22119,20 @@
"deleteDelay": {
"minimum": 1,
"type": "integer"
},
"sessionDeleteDelayBrowser": {
"minimum": 1,
"type": "integer"
},
"sessionDeleteDelayMobile": {
"minimum": 1,
"type": "integer"
}
},
"required": [
"deleteDelay"
"deleteDelay",
"sessionDeleteDelayBrowser",
"sessionDeleteDelayMobile"
],
"type": "object"
},

View File

@ -1215,6 +1215,8 @@ export type ServerConfigDto = {
mapLightStyleUrl: string;
oauthButtonText: string;
publicUsers: boolean;
sessionDeleteDelayBrowser: number;
sessionDeleteDelayMobile: number;
trashDays: number;
userDeleteDelay: number;
};
@ -1601,6 +1603,8 @@ export type SystemConfigTrashDto = {
};
export type SystemConfigUserDto = {
deleteDelay: number;
sessionDeleteDelayBrowser: number;
sessionDeleteDelayMobile: number;
};
export type SystemConfigDto = {
backup: SystemConfigBackupsDto;

View File

@ -186,6 +186,8 @@ export interface SystemConfig {
};
user: {
deleteDelay: number;
sessionDeleteDelayBrowser: number;
sessionDeleteDelayMobile: number;
};
}
@ -388,5 +390,7 @@ export const defaults = Object.freeze<SystemConfig>({
},
user: {
deleteDelay: 7,
sessionDeleteDelayBrowser: 90,
sessionDeleteDelayMobile: 90,
},
});

View File

@ -148,6 +148,10 @@ export class ServerConfigDto {
trashDays!: number;
@ApiProperty({ type: 'integer' })
userDeleteDelay!: number;
@ApiProperty({ type: 'integer' })
sessionDeleteDelayBrowser!: number;
@ApiProperty({ type: 'integer' })
sessionDeleteDelayMobile!: number;
isInitialized!: boolean;
isOnboarded!: boolean;
externalDomain!: string;

View File

@ -636,6 +636,18 @@ class SystemConfigUserDto {
@Type(() => Number)
@ApiProperty({ type: 'integer' })
deleteDelay!: number;
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
sessionDeleteDelayBrowser!: number;
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
sessionDeleteDelayMobile!: number;
}
export class SystemConfigDto implements SystemConfig {

View File

@ -62,6 +62,8 @@ export class MaintenanceWorkerService {
loginPageMessage: config.server.loginPageMessage,
trashDays: config.trash.days,
userDeleteDelay: config.user.deleteDelay,
sessionDeleteDelayBrowser: config.user.sessionDeleteDelayBrowser,
sessionDeleteDelayMobile: config.user.sessionDeleteDelayMobile,
oauthButtonText: config.oauth.buttonText,
isInitialized: true,
isOnboarded: true,

View File

@ -15,12 +15,22 @@ export type SessionSearchOptions = { updatedBefore: Date };
export class SessionRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
cleanup() {
cleanup(sessionDeleteDelayBrowser: number, sessionDeleteDelayMobile: number) {
const mobileOsList = ['Android', 'iOS'];
return this.db
.deleteFrom('session')
.where((eb) =>
eb.or([
eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()),
eb.and([
eb('deviceOS', 'in', mobileOsList),
eb('updatedAt', '<=', DateTime.now().minus({ days: sessionDeleteDelayMobile }).toJSDate()),
]),
// Browser sessions
eb.and([
eb('deviceOS', 'not in', mobileOsList),
eb('updatedAt', '<=', DateTime.now().minus({ days: sessionDeleteDelayBrowser }).toJSDate()),
]),
// Expired sessions
eb.and([eb('expiresAt', 'is not', null), eb('expiresAt', '<=', DateTime.now().toJSDate())]),
]),
)

View File

@ -8,30 +8,30 @@ import { BaseService } from 'src/services/base.service';
import { JobItem } from 'src/types';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
const asJobItem = (dto: JobCreateDto): JobItem => {
const asJobItem = (dto: JobCreateDto): JobItem[] => {
switch (dto.name) {
case ManualJobName.TagCleanup: {
return { name: JobName.TagCleanup };
return [{ name: JobName.TagCleanup }];
}
case ManualJobName.PersonCleanup: {
return { name: JobName.PersonCleanup };
return [{ name: JobName.PersonCleanup }];
}
case ManualJobName.UserCleanup: {
return { name: JobName.UserDeleteCheck };
return [{ name: JobName.UserDeleteCheck }, { name: JobName.SessionCleanup }];
}
case ManualJobName.MemoryCleanup: {
return { name: JobName.MemoryCleanup };
return [{ name: JobName.MemoryCleanup }];
}
case ManualJobName.MemoryCreate: {
return { name: JobName.MemoryGenerate };
return [{ name: JobName.MemoryGenerate }];
}
case ManualJobName.BackupDatabase: {
return { name: JobName.DatabaseBackup };
return [{ name: JobName.DatabaseBackup }];
}
default: {
@ -43,7 +43,7 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
@Injectable()
export class JobService extends BaseService {
async create(dto: JobCreateDto): Promise<void> {
await this.jobRepository.queue(asJobItem(dto));
await this.jobRepository.queueAll(asJobItem(dto));
}
@OnEvent({ name: 'JobRun' })

View File

@ -160,6 +160,8 @@ describe(ServerService.name, () => {
oauthButtonText: 'Login with OAuth',
trashDays: 30,
userDeleteDelay: 7,
sessionDeleteDelayBrowser: 90,
sessionDeleteDelayMobile: 90,
isInitialized: undefined,
isOnboarded: false,
externalDomain: '',

View File

@ -123,6 +123,8 @@ export class ServerService extends BaseService {
loginPageMessage: config.server.loginPageMessage,
trashDays: config.trash.days,
userDeleteDelay: config.user.deleteDelay,
sessionDeleteDelayBrowser: config.user.sessionDeleteDelayBrowser,
sessionDeleteDelayMobile: config.user.sessionDeleteDelayMobile,
oauthButtonText: config.oauth.buttonText,
isInitialized,
isOnboarded: onboarding?.isOnboarded || false,

View File

@ -17,7 +17,11 @@ import { BaseService } from 'src/services/base.service';
export class SessionService extends BaseService {
@OnJob({ name: JobName.SessionCleanup, queue: QueueName.BackgroundTask })
async handleCleanup(): Promise<JobStatus> {
const sessions = await this.sessionRepository.cleanup();
const config = await this.getConfig({ withCache: false });
const sessions = await this.sessionRepository.cleanup(
config.user.sessionDeleteDelayBrowser,
config.user.sessionDeleteDelayMobile,
);
for (const session of sessions) {
this.logger.verbose(`Deleted expired session token: ${session.deviceOS}/${session.deviceType}`);
}

View File

@ -23,7 +23,11 @@ const partialConfig = {
ffmpeg: { crf: 30 },
oauth: { autoLaunch: true },
trash: { days: 10 },
user: { deleteDelay: 15 },
user: {
deleteDelay: 15,
sessionDeleteDelayBrowser: 90,
sessionDeleteDelayMobile: 90,
},
} satisfies DeepPartial<SystemConfig>;
const updatedConfig = Object.freeze<SystemConfig>({
@ -197,6 +201,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
},
user: {
deleteDelay: 15,
sessionDeleteDelayBrowser: 90,
sessionDeleteDelayMobile: 90,
},
notifications: {
smtp: {
@ -255,7 +261,11 @@ describe(SystemConfigService.name, () => {
ffmpeg: { crf: 30 },
oauth: { autoLaunch: true },
trash: { days: 10 },
user: { deleteDelay: 15 },
user: {
deleteDelay: 15,
sessionDeleteDelayBrowser: 90,
sessionDeleteDelayMobile: 90,
},
});
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);

View File

@ -25,6 +25,22 @@
bind:value={configToEdit.user.deleteDelay}
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
/>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
min={1}
label={$t('admin.session_delete_delay_browser_settings')}
description={$t('admin.session_delete_delay_browser_settings_description')}
bind:value={configToEdit.user.sessionDeleteDelayBrowser}
isEdited={configToEdit.user.sessionDeleteDelayBrowser !== config.user.sessionDeleteDelayBrowser}
/>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
min={1}
label={$t('admin.session_delete_delay_mobile_settings')}
description={$t('admin.session_delete_delay_mobile_settings_description')}
bind:value={configToEdit.user.sessionDeleteDelayMobile}
isEdited={configToEdit.user.sessionDeleteDelayMobile !== config.user.sessionDeleteDelayMobile}
/>
</div>
<div class="ms-4">