mirror of
https://github.com/sct/overseerr.git
synced 2025-12-27 16:46:29 +01:00
When writing the settings.json file ensure that the file is fully written by writing it to temporary file before renaming it to the final settings path. This should prevent issues where the config gets lost due to the file being corrupted.
708 lines
18 KiB
TypeScript
708 lines
18 KiB
TypeScript
import { MediaServerType } from '@server/constants/server';
|
|
import { Permission } from '@server/lib/permissions';
|
|
import { runMigrations } from '@server/lib/settings/migrator';
|
|
import { randomUUID } from 'crypto';
|
|
import fs from 'fs/promises';
|
|
import { merge } from 'lodash';
|
|
import path from 'path';
|
|
import webpush from 'web-push';
|
|
|
|
export interface Library {
|
|
id: string;
|
|
name: string;
|
|
enabled: boolean;
|
|
type: 'show' | 'movie';
|
|
lastScan?: number;
|
|
}
|
|
|
|
export interface Region {
|
|
iso_3166_1: string;
|
|
english_name: string;
|
|
name?: string;
|
|
}
|
|
|
|
export interface Language {
|
|
iso_639_1: string;
|
|
english_name: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface PlexSettings {
|
|
name: string;
|
|
machineId?: string;
|
|
ip: string;
|
|
port: number;
|
|
useSsl?: boolean;
|
|
libraries: Library[];
|
|
webAppUrl?: string;
|
|
}
|
|
|
|
export interface JellyfinSettings {
|
|
name: string;
|
|
ip: string;
|
|
port: number;
|
|
useSsl?: boolean;
|
|
urlBase?: string;
|
|
externalHostname?: string;
|
|
jellyfinForgotPasswordUrl?: string;
|
|
libraries: Library[];
|
|
serverId: string;
|
|
apiKey: string;
|
|
}
|
|
export interface TautulliSettings {
|
|
hostname?: string;
|
|
port?: number;
|
|
useSsl?: boolean;
|
|
urlBase?: string;
|
|
apiKey?: string;
|
|
externalUrl?: string;
|
|
}
|
|
|
|
export interface DVRSettings {
|
|
id: number;
|
|
name: string;
|
|
hostname: string;
|
|
port: number;
|
|
apiKey: string;
|
|
useSsl: boolean;
|
|
baseUrl?: string;
|
|
activeProfileId: number;
|
|
activeProfileName: string;
|
|
activeDirectory: string;
|
|
tags: number[];
|
|
is4k: boolean;
|
|
isDefault: boolean;
|
|
externalUrl?: string;
|
|
syncEnabled: boolean;
|
|
preventSearch: boolean;
|
|
tagRequests: boolean;
|
|
}
|
|
|
|
export interface RadarrSettings extends DVRSettings {
|
|
minimumAvailability: string;
|
|
}
|
|
|
|
export interface SonarrSettings extends DVRSettings {
|
|
seriesType: 'standard' | 'daily' | 'anime';
|
|
animeSeriesType: 'standard' | 'daily' | 'anime';
|
|
activeAnimeProfileId?: number;
|
|
activeAnimeProfileName?: string;
|
|
activeAnimeDirectory?: string;
|
|
activeAnimeLanguageProfileId?: number;
|
|
activeLanguageProfileId?: number;
|
|
animeTags?: number[];
|
|
enableSeasonFolders: boolean;
|
|
}
|
|
|
|
interface Quota {
|
|
quotaLimit?: number;
|
|
quotaDays?: number;
|
|
}
|
|
|
|
export interface ProxySettings {
|
|
enabled: boolean;
|
|
hostname: string;
|
|
port: number;
|
|
useSsl: boolean;
|
|
user: string;
|
|
password: string;
|
|
bypassFilter: string;
|
|
bypassLocalAddresses: boolean;
|
|
}
|
|
|
|
export interface MainSettings {
|
|
apiKey: string;
|
|
applicationTitle: string;
|
|
applicationUrl: string;
|
|
csrfProtection: boolean;
|
|
cacheImages: boolean;
|
|
defaultPermissions: number;
|
|
defaultQuotas: {
|
|
movie: Quota;
|
|
tv: Quota;
|
|
};
|
|
hideAvailable: boolean;
|
|
localLogin: boolean;
|
|
newPlexLogin: boolean;
|
|
region: string;
|
|
originalLanguage: string;
|
|
trustProxy: boolean;
|
|
mediaServerType: number;
|
|
partialRequestsEnabled: boolean;
|
|
locale: string;
|
|
proxy: ProxySettings;
|
|
}
|
|
|
|
interface PublicSettings {
|
|
initialized: boolean;
|
|
}
|
|
|
|
interface FullPublicSettings extends PublicSettings {
|
|
applicationTitle: string;
|
|
applicationUrl: string;
|
|
hideAvailable: boolean;
|
|
localLogin: boolean;
|
|
movie4kEnabled: boolean;
|
|
series4kEnabled: boolean;
|
|
region: string;
|
|
originalLanguage: string;
|
|
mediaServerType: number;
|
|
jellyfinExternalHost?: string;
|
|
jellyfinForgotPasswordUrl?: string;
|
|
jellyfinServerName?: string;
|
|
partialRequestsEnabled: boolean;
|
|
cacheImages: boolean;
|
|
vapidPublic: string;
|
|
enablePushRegistration: boolean;
|
|
locale: string;
|
|
emailEnabled: boolean;
|
|
userEmailRequired: boolean;
|
|
newPlexLogin: boolean;
|
|
}
|
|
|
|
export interface NotificationAgentConfig {
|
|
enabled: boolean;
|
|
types?: number;
|
|
options: Record<string, unknown>;
|
|
}
|
|
export interface NotificationAgentDiscord extends NotificationAgentConfig {
|
|
options: {
|
|
botUsername?: string;
|
|
botAvatarUrl?: string;
|
|
webhookUrl: string;
|
|
webhookRoleId?: string;
|
|
enableMentions: boolean;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentSlack extends NotificationAgentConfig {
|
|
options: {
|
|
webhookUrl: string;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentEmail extends NotificationAgentConfig {
|
|
options: {
|
|
userEmailRequired: boolean;
|
|
emailFrom: string;
|
|
smtpHost: string;
|
|
smtpPort: number;
|
|
secure: boolean;
|
|
ignoreTls: boolean;
|
|
requireTls: boolean;
|
|
authUser?: string;
|
|
authPass?: string;
|
|
allowSelfSigned: boolean;
|
|
senderName: string;
|
|
pgpPrivateKey?: string;
|
|
pgpPassword?: string;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentLunaSea extends NotificationAgentConfig {
|
|
options: {
|
|
webhookUrl: string;
|
|
profileName?: string;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentTelegram extends NotificationAgentConfig {
|
|
options: {
|
|
botUsername?: string;
|
|
botAPI: string;
|
|
chatId: string;
|
|
sendSilently: boolean;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentPushbullet extends NotificationAgentConfig {
|
|
options: {
|
|
accessToken: string;
|
|
channelTag?: string;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentPushover extends NotificationAgentConfig {
|
|
options: {
|
|
accessToken: string;
|
|
userToken: string;
|
|
sound: string;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentWebhook extends NotificationAgentConfig {
|
|
options: {
|
|
webhookUrl: string;
|
|
jsonPayload: string;
|
|
authHeader?: string;
|
|
};
|
|
}
|
|
|
|
export interface NotificationAgentGotify extends NotificationAgentConfig {
|
|
options: {
|
|
url: string;
|
|
token: string;
|
|
};
|
|
}
|
|
|
|
export enum NotificationAgentKey {
|
|
DISCORD = 'discord',
|
|
EMAIL = 'email',
|
|
GOTIFY = 'gotify',
|
|
PUSHBULLET = 'pushbullet',
|
|
PUSHOVER = 'pushover',
|
|
SLACK = 'slack',
|
|
TELEGRAM = 'telegram',
|
|
WEBHOOK = 'webhook',
|
|
WEBPUSH = 'webpush',
|
|
}
|
|
|
|
interface NotificationAgents {
|
|
discord: NotificationAgentDiscord;
|
|
email: NotificationAgentEmail;
|
|
gotify: NotificationAgentGotify;
|
|
lunasea: NotificationAgentLunaSea;
|
|
pushbullet: NotificationAgentPushbullet;
|
|
pushover: NotificationAgentPushover;
|
|
slack: NotificationAgentSlack;
|
|
telegram: NotificationAgentTelegram;
|
|
webhook: NotificationAgentWebhook;
|
|
webpush: NotificationAgentConfig;
|
|
}
|
|
|
|
interface NotificationSettings {
|
|
agents: NotificationAgents;
|
|
}
|
|
|
|
interface JobSettings {
|
|
schedule: string;
|
|
}
|
|
|
|
export type JobId =
|
|
| 'plex-recently-added-scan'
|
|
| 'plex-full-scan'
|
|
| 'plex-watchlist-sync'
|
|
| 'plex-refresh-token'
|
|
| 'radarr-scan'
|
|
| 'sonarr-scan'
|
|
| 'download-sync'
|
|
| 'download-sync-reset'
|
|
| 'jellyfin-recently-added-scan'
|
|
| 'jellyfin-full-scan'
|
|
| 'image-cache-cleanup'
|
|
| 'availability-sync';
|
|
|
|
export interface AllSettings {
|
|
clientId: string;
|
|
vapidPublic: string;
|
|
vapidPrivate: string;
|
|
main: MainSettings;
|
|
plex: PlexSettings;
|
|
jellyfin: JellyfinSettings;
|
|
tautulli: TautulliSettings;
|
|
radarr: RadarrSettings[];
|
|
sonarr: SonarrSettings[];
|
|
public: PublicSettings;
|
|
notifications: NotificationSettings;
|
|
jobs: Record<JobId, JobSettings>;
|
|
}
|
|
|
|
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
|
? `${process.env.CONFIG_DIRECTORY}/settings.json`
|
|
: path.join(__dirname, '../../../config/settings.json');
|
|
|
|
class Settings {
|
|
private data: AllSettings;
|
|
|
|
constructor(initialSettings?: AllSettings) {
|
|
this.data = {
|
|
clientId: randomUUID(),
|
|
vapidPrivate: '',
|
|
vapidPublic: '',
|
|
main: {
|
|
apiKey: '',
|
|
applicationTitle: 'Jellyseerr',
|
|
applicationUrl: '',
|
|
csrfProtection: false,
|
|
cacheImages: false,
|
|
defaultPermissions: Permission.REQUEST,
|
|
defaultQuotas: {
|
|
movie: {},
|
|
tv: {},
|
|
},
|
|
hideAvailable: false,
|
|
localLogin: true,
|
|
newPlexLogin: true,
|
|
region: '',
|
|
originalLanguage: '',
|
|
trustProxy: false,
|
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
|
partialRequestsEnabled: true,
|
|
locale: 'en',
|
|
proxy: {
|
|
enabled: false,
|
|
hostname: '',
|
|
port: 8080,
|
|
useSsl: false,
|
|
user: '',
|
|
password: '',
|
|
bypassFilter: '',
|
|
bypassLocalAddresses: true,
|
|
},
|
|
},
|
|
plex: {
|
|
name: '',
|
|
ip: '',
|
|
port: 32400,
|
|
useSsl: false,
|
|
libraries: [],
|
|
},
|
|
jellyfin: {
|
|
name: '',
|
|
ip: '',
|
|
port: 8096,
|
|
useSsl: false,
|
|
urlBase: '',
|
|
externalHostname: '',
|
|
jellyfinForgotPasswordUrl: '',
|
|
libraries: [],
|
|
serverId: '',
|
|
apiKey: '',
|
|
},
|
|
tautulli: {},
|
|
radarr: [],
|
|
sonarr: [],
|
|
public: {
|
|
initialized: false,
|
|
},
|
|
notifications: {
|
|
agents: {
|
|
email: {
|
|
enabled: false,
|
|
options: {
|
|
userEmailRequired: false,
|
|
emailFrom: '',
|
|
smtpHost: '',
|
|
smtpPort: 587,
|
|
secure: false,
|
|
ignoreTls: false,
|
|
requireTls: false,
|
|
allowSelfSigned: false,
|
|
senderName: 'Jellyseerr',
|
|
},
|
|
},
|
|
discord: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
webhookUrl: '',
|
|
webhookRoleId: '',
|
|
enableMentions: true,
|
|
},
|
|
},
|
|
lunasea: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
webhookUrl: '',
|
|
},
|
|
},
|
|
slack: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
webhookUrl: '',
|
|
},
|
|
},
|
|
telegram: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
botAPI: '',
|
|
chatId: '',
|
|
sendSilently: false,
|
|
},
|
|
},
|
|
pushbullet: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
accessToken: '',
|
|
},
|
|
},
|
|
pushover: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
accessToken: '',
|
|
userToken: '',
|
|
sound: '',
|
|
},
|
|
},
|
|
webhook: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
webhookUrl: '',
|
|
jsonPayload:
|
|
'IntcbiAgXCJub3RpZmljYXRpb25fdHlwZVwiOiBcInt7bm90aWZpY2F0aW9uX3R5cGV9fVwiLFxuICBcImV2ZW50XCI6IFwie3tldmVudH19XCIsXG4gIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gIFwibWVzc2FnZVwiOiBcInt7bWVzc2FnZX19XCIsXG4gIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgXCJ7e21lZGlhfX1cIjoge1xuICAgIFwibWVkaWFfdHlwZVwiOiBcInt7bWVkaWFfdHlwZX19XCIsXG4gICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgXCJzdGF0dXM0a1wiOiBcInt7bWVkaWFfc3RhdHVzNGt9fVwiXG4gIH0sXG4gIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgXCJyZXF1ZXN0ZWRCeV9lbWFpbFwiOiBcInt7cmVxdWVzdGVkQnlfZW1haWx9fVwiLFxuICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVxdWVzdGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tyZXF1ZXN0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2lzc3VlfX1cIjoge1xuICAgIFwiaXNzdWVfaWRcIjogXCJ7e2lzc3VlX2lkfX1cIixcbiAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgIFwiaXNzdWVfc3RhdHVzXCI6IFwie3tpc3N1ZV9zdGF0dXN9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9lbWFpbFwiOiBcInt7cmVwb3J0ZWRCeV9lbWFpbH19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcG9ydGVkQnlfYXZhdGFyXCI6IFwie3tyZXBvcnRlZEJ5X2F2YXRhcn19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc19kaXNjb3JkSWR9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2NvbW1lbnR9fVwiOiB7XG4gICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgXCJjb21tZW50ZWRCeV9lbWFpbFwiOiBcInt7Y29tbWVudGVkQnlfZW1haWx9fVwiLFxuICAgIFwiY29tbWVudGVkQnlfdXNlcm5hbWVcIjogXCJ7e2NvbW1lbnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7Y29tbWVudGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tjb21tZW50ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
|
|
},
|
|
},
|
|
webpush: {
|
|
enabled: false,
|
|
options: {},
|
|
},
|
|
gotify: {
|
|
enabled: false,
|
|
types: 0,
|
|
options: {
|
|
url: '',
|
|
token: '',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
jobs: {
|
|
'plex-recently-added-scan': {
|
|
schedule: '0 */5 * * * *',
|
|
},
|
|
'plex-full-scan': {
|
|
schedule: '0 0 3 * * *',
|
|
},
|
|
'plex-watchlist-sync': {
|
|
schedule: '0 */3 * * * *',
|
|
},
|
|
'plex-refresh-token': {
|
|
schedule: '0 0 5 * * *',
|
|
},
|
|
'radarr-scan': {
|
|
schedule: '0 0 4 * * *',
|
|
},
|
|
'sonarr-scan': {
|
|
schedule: '0 30 4 * * *',
|
|
},
|
|
'availability-sync': {
|
|
schedule: '0 0 5 * * *',
|
|
},
|
|
'download-sync': {
|
|
schedule: '0 * * * * *',
|
|
},
|
|
'download-sync-reset': {
|
|
schedule: '0 0 1 * * *',
|
|
},
|
|
'jellyfin-recently-added-scan': {
|
|
schedule: '0 */5 * * * *',
|
|
},
|
|
'jellyfin-full-scan': {
|
|
schedule: '0 0 3 * * *',
|
|
},
|
|
'image-cache-cleanup': {
|
|
schedule: '0 0 5 * * *',
|
|
},
|
|
},
|
|
};
|
|
if (initialSettings) {
|
|
this.data = merge(this.data, initialSettings);
|
|
}
|
|
}
|
|
|
|
get main(): MainSettings {
|
|
return this.data.main;
|
|
}
|
|
|
|
set main(data: MainSettings) {
|
|
this.data.main = data;
|
|
}
|
|
|
|
get plex(): PlexSettings {
|
|
return this.data.plex;
|
|
}
|
|
|
|
set plex(data: PlexSettings) {
|
|
this.data.plex = data;
|
|
}
|
|
|
|
get jellyfin(): JellyfinSettings {
|
|
return this.data.jellyfin;
|
|
}
|
|
|
|
set jellyfin(data: JellyfinSettings) {
|
|
this.data.jellyfin = data;
|
|
}
|
|
|
|
get tautulli(): TautulliSettings {
|
|
return this.data.tautulli;
|
|
}
|
|
|
|
set tautulli(data: TautulliSettings) {
|
|
this.data.tautulli = data;
|
|
}
|
|
|
|
get radarr(): RadarrSettings[] {
|
|
return this.data.radarr;
|
|
}
|
|
|
|
set radarr(data: RadarrSettings[]) {
|
|
this.data.radarr = data;
|
|
}
|
|
|
|
get sonarr(): SonarrSettings[] {
|
|
return this.data.sonarr;
|
|
}
|
|
|
|
set sonarr(data: SonarrSettings[]) {
|
|
this.data.sonarr = data;
|
|
}
|
|
|
|
get public(): PublicSettings {
|
|
return this.data.public;
|
|
}
|
|
|
|
set public(data: PublicSettings) {
|
|
this.data.public = data;
|
|
}
|
|
|
|
get fullPublicSettings(): FullPublicSettings {
|
|
return {
|
|
...this.data.public,
|
|
applicationTitle: this.data.main.applicationTitle,
|
|
applicationUrl: this.data.main.applicationUrl,
|
|
hideAvailable: this.data.main.hideAvailable,
|
|
localLogin: this.data.main.localLogin,
|
|
jellyfinForgotPasswordUrl: this.data.jellyfin.jellyfinForgotPasswordUrl,
|
|
movie4kEnabled: this.data.radarr.some(
|
|
(radarr) => radarr.is4k && radarr.isDefault
|
|
),
|
|
series4kEnabled: this.data.sonarr.some(
|
|
(sonarr) => sonarr.is4k && sonarr.isDefault
|
|
),
|
|
region: this.data.main.region,
|
|
originalLanguage: this.data.main.originalLanguage,
|
|
mediaServerType: this.main.mediaServerType,
|
|
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
|
cacheImages: this.data.main.cacheImages,
|
|
vapidPublic: this.vapidPublic,
|
|
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
|
locale: this.data.main.locale,
|
|
emailEnabled: this.data.notifications.agents.email.enabled,
|
|
userEmailRequired:
|
|
this.data.notifications.agents.email.options.userEmailRequired,
|
|
newPlexLogin: this.data.main.newPlexLogin,
|
|
};
|
|
}
|
|
|
|
get notifications(): NotificationSettings {
|
|
return this.data.notifications;
|
|
}
|
|
|
|
set notifications(data: NotificationSettings) {
|
|
this.data.notifications = data;
|
|
}
|
|
|
|
get jobs(): Record<JobId, JobSettings> {
|
|
return this.data.jobs;
|
|
}
|
|
|
|
set jobs(data: Record<JobId, JobSettings>) {
|
|
this.data.jobs = data;
|
|
}
|
|
|
|
get clientId(): string {
|
|
return this.data.clientId;
|
|
}
|
|
|
|
get vapidPublic(): string {
|
|
return this.data.vapidPublic;
|
|
}
|
|
|
|
get vapidPrivate(): string {
|
|
return this.data.vapidPrivate;
|
|
}
|
|
|
|
public async regenerateApiKey(): Promise<MainSettings> {
|
|
this.main.apiKey = this.generateApiKey();
|
|
await this.save();
|
|
return this.main;
|
|
}
|
|
|
|
private generateApiKey(): string {
|
|
if (process.env.API_KEY) {
|
|
return process.env.API_KEY;
|
|
} else {
|
|
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Settings Load
|
|
*
|
|
* This will load settings from file unless an optional argument of the object structure
|
|
* is passed in.
|
|
* @param overrideSettings If passed in, will override all existing settings with these
|
|
* values
|
|
*/
|
|
public async load(overrideSettings?: AllSettings): Promise<Settings> {
|
|
if (overrideSettings) {
|
|
this.data = overrideSettings;
|
|
return this;
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
|
} catch {
|
|
await this.save();
|
|
}
|
|
|
|
if (data) {
|
|
const parsedJson = JSON.parse(data);
|
|
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
|
|
this.data = merge(this.data, migratedData);
|
|
}
|
|
|
|
// generate keys and ids if it's missing
|
|
let change = false;
|
|
if (!this.data.main.apiKey) {
|
|
this.data.main.apiKey = this.generateApiKey();
|
|
change = true;
|
|
} else if (process.env.API_KEY) {
|
|
if (this.main.apiKey != process.env.API_KEY) {
|
|
this.main.apiKey = process.env.API_KEY;
|
|
}
|
|
}
|
|
if (!this.data.clientId) {
|
|
this.data.clientId = randomUUID();
|
|
change = true;
|
|
}
|
|
if (!this.data.vapidPublic || !this.data.vapidPrivate) {
|
|
const vapidKeys = webpush.generateVAPIDKeys();
|
|
this.data.vapidPrivate = vapidKeys.privateKey;
|
|
this.data.vapidPublic = vapidKeys.publicKey;
|
|
change = true;
|
|
}
|
|
if (change) {
|
|
await this.save();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public async save(): Promise<void> {
|
|
const tmp = SETTINGS_PATH + '.tmp';
|
|
await fs.writeFile(tmp, JSON.stringify(this.data, undefined, ' '));
|
|
await fs.rename(tmp, SETTINGS_PATH);
|
|
}
|
|
}
|
|
|
|
let settings: Settings | undefined;
|
|
|
|
export const getSettings = (initialSettings?: AllSettings): Settings => {
|
|
if (!settings) {
|
|
settings = new Settings(initialSettings);
|
|
}
|
|
|
|
return settings;
|
|
};
|
|
|
|
export default Settings;
|