feat(server,web): migrate oauth settings from env to system config (#1061)

This commit is contained in:
Jason Rasmussen 2022-12-09 15:51:42 -05:00 committed by GitHub
parent cefdd86b7f
commit 5e680551b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 2079 additions and 1229 deletions

View file

@ -0,0 +1,126 @@
<script lang="ts">
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { api, SystemConfigFFmpegDto } from '@api';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import _ from 'lodash';
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
import { fade } from 'svelte/transition';
let savedConfig: SystemConfigFFmpegDto;
let defaultConfig: SystemConfigFFmpegDto;
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg)
]);
}
async function saveSetting() {
try {
const { data: configs } = await api.systemConfigApi.getConfig();
const result = await api.systemConfigApi.updateConfig({
ffmpeg: ffmpegConfig,
oauth: configs.oauth
});
ffmpegConfig = result.data.ffmpeg;
savedConfig = result.data.ffmpeg;
notificationController.show({
message: 'FFmpeg settings saved',
type: NotificationType.Info
});
} catch (e) {
console.error('Error [ffmpeg-settings] [saveSetting]', e);
notificationController.show({
message: 'Unable to save settings',
type: NotificationType.Error
});
}
}
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
ffmpegConfig = resetConfig.ffmpeg;
savedConfig = resetConfig.ffmpeg;
notificationController.show({
message: 'Reset FFmpeg settings to the recent saved settings',
type: NotificationType.Info
});
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
ffmpegConfig = configs.ffmpeg;
defaultConfig = configs.ffmpeg;
notificationController.show({
message: 'Reset FFmpeg settings to default',
type: NotificationType.Info
});
}
</script>
<div>
{#await getConfigs() then}
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label="CRF"
bind:value={ffmpegConfig.crf}
required={true}
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="PRESET"
bind:value={ffmpegConfig.preset}
required={true}
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="AUDIO CODEC"
bind:value={ffmpegConfig.targetAudioCodec}
required={true}
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="VIDEO CODEC"
bind:value={ffmpegConfig.targetVideoCodec}
required={true}
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCALING"
bind:value={ffmpegConfig.targetScaling}
required={true}
isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
/>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
</form>
</div>
{/await}
</div>

View file

@ -0,0 +1,147 @@
<script lang="ts">
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { api, SystemConfigOAuthDto } from '@api';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import _ from 'lodash';
import { fade } from 'svelte/transition';
export let oauthConfig: SystemConfigOAuthDto;
let savedConfig: SystemConfigOAuthDto;
let defaultConfig: SystemConfigOAuthDto;
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
api.systemConfigApi.getDefaults().then((res) => res.data.oauth)
]);
}
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
oauthConfig = resetConfig.oauth;
savedConfig = resetConfig.oauth;
notificationController.show({
message: 'Reset OAuth settings to the recent saved settings',
type: NotificationType.Info
});
}
async function saveSetting() {
try {
const { data: currentConfig } = await api.systemConfigApi.getConfig();
const result = await api.systemConfigApi.updateConfig({
ffmpeg: currentConfig.ffmpeg,
oauth: oauthConfig
});
oauthConfig = result.data.oauth;
savedConfig = result.data.oauth;
notificationController.show({
message: 'OAuth settings saved',
type: NotificationType.Info
});
} catch (e) {
console.error('Error [oauth-settings] [saveSetting]', e);
notificationController.show({
message: 'Unable to save settings',
type: NotificationType.Error
});
}
}
async function resetToDefault() {
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
oauthConfig = defaultConfig.oauth;
notificationController.show({
message: 'Reset OAuth settings to default',
type: NotificationType.Info
});
}
</script>
<div class="mt-2">
{#await getConfigs() then}
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="mt-4">
<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
</div>
<hr class="m-4" />
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="ISSUER URL"
bind:value={oauthConfig.issuerUrl}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="CLIENT ID"
bind:value={oauthConfig.clientId}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="CLIENT SECRET"
bind:value={oauthConfig.clientSecret}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCOPE"
bind:value={oauthConfig.scope}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.scope == savedConfig.scope)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="BUTTON TEXT"
bind:value={oauthConfig.buttonText}
required={false}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
/>
<div class="mt-4">
<SettingSwitch
title="AUTO REGISTER"
subtitle="Automatically register new users after singning in with OAuth"
bind:checked={oauthConfig.autoRegister}
disabled={!oauthConfig.enabled}
/>
</div>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
</form>
</div>
{/await}
</div>

View file

@ -0,0 +1,56 @@
<script lang="ts">
import { slide } from 'svelte/transition';
export let title: string;
export let subtitle = '';
let isOpen = false;
const toggle = () => (isOpen = !isOpen);
</script>
<div class="border-b-[1px] border-gray-200 dark:border-gray-700 py-4">
<div class="flex justify-between place-items-center">
<div>
<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h2>
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
</div>
<button
on:click={toggle}
aria-expanded={isOpen}
class="immich-circle-icon-button hover:bg-immich-primary/10 dark:text-immich-dark-fg hover:dark:bg-immich-dark-primary/20 rounded-full p-3 flex place-items-center place-content-center transition-all"
>
<svg
style="tran"
width="20"
height="20"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
{#if isOpen}
<ul transition:slide={{ duration: 250 }} class="mb-2 ml-4">
<slot />
</ul>
{/if}
</div>
<style>
svg {
transition: transform 0.2s ease-in;
}
[aria-expanded='true'] svg {
transform: rotate(0.5turn);
}
</style>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let showResetToDefault = true;
</script>
<div class="flex justify-between gap-2 mx-4 mt-8">
<div class="left">
{#if showResetToDefault}
<button
on:click|preventDefault={() => dispatch('reset-to-default')}
class="text-sm dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75 text-immich-primary hover:text-immich-primary/75 font-medium bg-none"
>
Reset to default
</button>
{/if}
</div>
<div class="right">
<button
on:click|preventDefault={() => dispatch('reset')}
class="text-sm bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>Reset
</button>
<button
type="submit"
on:click={() => dispatch('save')}
class="text-sm bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>Save
</button>
</div>
</div>

View file

@ -0,0 +1,51 @@
<script lang="ts" context="module">
export enum SettingInputFieldType {
TEXT = 'text',
NUMBER = 'number',
PASSWORD = 'password'
}
</script>
<script lang="ts">
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
export let inputType: SettingInputFieldType;
export let value: string;
export let label: string;
export let required = false;
export let disabled = false;
export let isEdited: boolean;
const handleInput = (e: Event) => {
value = (e.target as HTMLInputElement).value;
};
</script>
<div class="m-4 flex flex-col gap-2">
<div class="flex place-items-center gap-1">
<label class="immich-form-label" for={label}>{label.toUpperCase()} </label>
{#if required}
<div class="text-red-400">*</div>
{/if}
{#if isEdited}
<div
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="text-gray-500 text-xs italic"
>
Unsaved change
</div>
{/if}
</div>
<input
class="immich-form-input"
id={label}
name={label}
type={inputType}
{required}
{value}
on:input={handleInput}
{disabled}
/>
</div>

View file

@ -0,0 +1,81 @@
<script lang="ts">
export let title: string;
export let subtitle = '';
export let checked = false;
export let disabled = false;
</script>
<div class="flex justify-between mx-4 place-items-center">
<div>
<h2 class="immich-form-label">
{title.toUpperCase()}
</h2>
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
</div>
<label class="relative inline-block w-[36px] h-[10px]" {disabled}>
<input
class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
type="checkbox"
bind:checked
{disabled}
/>
{#if disabled}
<span class="slider-disable" />
{:else}
<span class="slider" />
{/if}
</label>
</div>
<style>
.slider,
.slider-disable {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 34px;
}
input:disabled {
cursor: not-allowed;
}
.slider:before,
.slider-disable:before {
position: absolute;
content: '';
height: 20px;
width: 20px;
left: 0px;
right: 0px;
bottom: -4px;
background-color: gray;
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider-disable {
background-color: gray;
}
input:checked + .slider {
background-color: #adcbfa;
}
input:checked + .slider:before {
-webkit-transform: translateX(18px);
-ms-transform: translateX(18px);
transform: translateX(18px);
background-color: #4250af;
}
</style>

View file

@ -1,97 +0,0 @@
<script lang="ts">
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { api, SystemConfigResponseItem } from '@api';
import { onMount } from 'svelte';
let isSaving = false;
let items: Array<SystemConfigResponseItem & { originalValue: string }> = [];
const refreshConfig = async () => {
const { data: systemConfig } = await api.systemConfigApi.getConfig();
items = systemConfig.config.map((item) => ({ ...item, originalValue: item.value }));
};
onMount(() => refreshConfig());
const handleSave = async () => {
try {
isSaving = true;
const updates = items
.filter((item) => item.value !== item.originalValue)
.map(({ key, value }) => ({ key, value: value || null }));
if (updates.length > 0) {
await api.systemConfigApi.updateConfig({ config: updates });
refreshConfig();
}
notificationController.show({
message: `Saved settings`,
type: NotificationType.Info
});
} catch (e) {
console.error('Error [updateSystemConfig]', e);
notificationController.show({
message: `Unable to save changes.`,
type: NotificationType.Error
});
} finally {
isSaving = false;
}
};
</script>
<section>
<table class="text-left my-4 w-full">
<thead
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
>
<tr class="flex w-full place-items-center">
<th class="text-center w-1/2 font-medium text-sm">Setting</th>
<th class="text-center w-1/2 font-medium text-sm">Value</th>
</tr>
</thead>
<tbody class="rounded-md block border dark:border-immich-dark-gray">
{#each items as item, i}
<tr
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
i % 2 == 0 ? 'bg-slate-50 dark:bg-[#181818]' : 'bg-immich-bg dark:bg-immich-dark-bg'
}`}
>
<td class="text-sm px-4 w-1/2 text-ellipsis">
{item.name}
</td>
<td class="text-sm px-4 w-1/2 text-ellipsis">
<input
style="text-align: center"
class="immich-form-input"
id={item.key}
disabled={isSaving}
name={item.key}
type="text"
bind:value={item.value}
placeholder={item.defaultValue + ''}
/>
</td>
</tr>
{/each}
</tbody>
</table>
<div class="flex justify-end">
<button
on:click={handleSave}
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
disabled={isSaving}
>
{#if isSaving}
<LoadingSpinner />
{:else}
Save
{/if}
</button>
</div>
</section>

View file

@ -1,91 +0,0 @@
<script lang="ts">
import { UserResponseDto } from '@api';
import { createEventDispatcher } from 'svelte';
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
export let allUsers: Array<UserResponseDto>;
const dispatch = createEventDispatcher();
const isDeleted = (user: UserResponseDto): boolean => {
return user.deletedAt != null;
};
const locale = navigator.language;
const deleteDateFormat: Intl.DateTimeFormatOptions = {
month: 'long',
day: 'numeric',
year: 'numeric'
};
const getDeleteDate = (user: UserResponseDto): string => {
let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
deletedAt.setDate(deletedAt.getDate() + 7);
return deletedAt.toLocaleString(locale, deleteDateFormat);
};
</script>
<table class="text-left w-full my-5">
<thead
class="border rounded-md mb-4 bg-gray-50 flex text-immich-primary w-full h-12 dark:bg-immich-dark-gray dark:text-immich-dark-primary dark:border-immich-dark-gray"
>
<tr class="flex w-full place-items-center">
<th class="text-center w-1/4 font-medium text-sm">Email</th>
<th class="text-center w-1/4 font-medium text-sm">First name</th>
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
<th class="text-center w-1/4 font-medium text-sm">Action</th>
</tr>
</thead>
<tbody
class="overflow-y-auto rounded-md w-full max-h-[320px] block border dark:border-immich-dark-gray"
>
{#each allUsers as user, i}
<tr
class={`text-center flex place-items-center w-full h-[80px] dark:text-immich-dark-fg ${
isDeleted(user)
? 'bg-red-300 dark:bg-red-900'
: i % 2 == 0
? 'bg-immich-gray dark:bg-immich-dark-gray/75'
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
<td class="text-sm px-4 w-1/4 text-ellipsis">
{#if !isDeleted(user)}
<button
on:click={() => {
dispatch('edit-user', { user });
}}
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
><PencilOutline size="16" /></button
>
<button
on:click={() => {
dispatch('delete-user', { user });
}}
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
><TrashCanOutline size="16" /></button
>
{/if}
{#if isDeleted(user)}
<button
on:click={() => {
dispatch('restore-user', { user });
}}
class="bg-immich-primary dark:bg-immich-dark-primary text-gray-100 dark:text-gray-700 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
title={`scheduled removal on ${getDeleteDate(user)}`}
><DeleteRestore size="16" /></button
>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
<button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>