Cleanup login page, custom button text

This commit is contained in:
Amelia 2025-06-30 20:27:53 +02:00
parent 54fb4c7a8a
commit 4c0faa755d
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
12 changed files with 76 additions and 32 deletions

View file

@ -3,12 +3,8 @@ using API.Entities.Enums;
namespace API.DTOs.Settings; namespace API.DTOs.Settings;
public class OidcConfigDto public record OidcConfigDto: OidcPublicConfigDto
{ {
/// <inheritdoc cref="ServerSettingKey.OidcAuthority"/>
public string? Authority { get; set; }
/// <inheritdoc cref="ServerSettingKey.OidcClientId"/>
public string? ClientId { get; set; }
/// <inheritdoc cref="ServerSettingKey.OidcProvisionAccounts"/> /// <inheritdoc cref="ServerSettingKey.OidcProvisionAccounts"/>
public bool ProvisionAccounts { get; set; } public bool ProvisionAccounts { get; set; }
/// <inheritdoc cref="ServerSettingKey.OidcRequireVerifiedEmail"/> /// <inheritdoc cref="ServerSettingKey.OidcRequireVerifiedEmail"/>
@ -16,12 +12,9 @@ public class OidcConfigDto
/// <inheritdoc cref="ServerSettingKey.OidcProvisionUserSettings"/> /// <inheritdoc cref="ServerSettingKey.OidcProvisionUserSettings"/>
public bool ProvisionUserSettings { get; set; } public bool ProvisionUserSettings { get; set; }
/// <inheritdoc cref="ServerSettingKey.OidcAutoLogin"/> /// <inheritdoc cref="ServerSettingKey.OidcAutoLogin"/>
public bool AutoLogin { get; set; }
/// <inheritdoc cref="ServerSettingKey.DisablePasswordAuthentication"/>
public bool DisablePasswordAuthentication { get; set; }
/// <summary> /// <summary>
/// Returns true if the <see cref="Authority"/> has been set /// Returns true if the <see cref="OidcPublicConfigDto.Authority"/> has been set
/// </summary> /// </summary>
public bool Enabled => Authority != ""; public bool Enabled => Authority != "";
} }

View file

@ -1,14 +1,18 @@
#nullable enable #nullable enable
using API.Entities.Enums;
namespace API.DTOs.Settings; namespace API.DTOs.Settings;
public sealed record OidcPublicConfigDto public record OidcPublicConfigDto
{ {
/// <inheritdoc cref="OidcConfigDto.Authority"/> /// <inheritdoc cref="ServerSettingKey.OidcAuthority"/>
public string? Authority { get; set; } public string? Authority { get; set; }
/// <inheritdoc cref="OidcConfigDto.ClientId"/> /// <inheritdoc cref="ServerSettingKey.OidcClientId"/>
public string? ClientId { get; set; } public string? ClientId { get; set; }
/// <inheritdoc cref="OidcConfigDto.AutoLogin"/> /// <inheritdoc cref="ServerSettingKey.OidcAutoLogin"/>
public bool AutoLogin { get; set; } public bool AutoLogin { get; set; }
/// <inheritdoc cref="OidcConfigDto.DisablePasswordAuthentication"/> /// <inheritdoc cref="ServerSettingKey.DisablePasswordAuthentication"/>
public bool DisablePasswordAuthentication { get; set; } public bool DisablePasswordAuthentication { get; set; }
/// <inheritdoc cref="ServerSettingKey.OidcProviderName"/>
public string ProviderName { get; set; } = string.Empty;
} }

View file

@ -259,6 +259,7 @@ public static class Seed
new() { Key = ServerSettingKey.OidcRequireVerifiedEmail, Value = "true"}, new() { Key = ServerSettingKey.OidcRequireVerifiedEmail, Value = "true"},
new() { Key = ServerSettingKey.OidcProvisionUserSettings, Value = "false"}, new() { Key = ServerSettingKey.OidcProvisionUserSettings, Value = "false"},
new() { Key = ServerSettingKey.DisablePasswordAuthentication, Value = "false"}, new() { Key = ServerSettingKey.DisablePasswordAuthentication, Value = "false"},
new() { Key = ServerSettingKey.OidcProviderName, Value = "OpenID Connect"},
new() {Key = ServerSettingKey.EmailHost, Value = string.Empty}, new() {Key = ServerSettingKey.EmailHost, Value = string.Empty},
new() {Key = ServerSettingKey.EmailPort, Value = string.Empty}, new() {Key = ServerSettingKey.EmailPort, Value = string.Empty},

View file

@ -232,4 +232,10 @@ public enum ServerSettingKey
/// </summary> /// </summary>
[Description("DisablePasswordAuthentication")] [Description("DisablePasswordAuthentication")]
DisablePasswordAuthentication = 46, DisablePasswordAuthentication = 46,
/// <summary>
/// Name of your provider, used to display on the login screen
/// </summary>
/// <remarks>Default to OpenID Connect</remarks>
[Description("OidcProviderName")]
OidcProviderName = 47,
} }

View file

@ -157,6 +157,10 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
destination.OidcConfig ??= new OidcConfigDto(); destination.OidcConfig ??= new OidcConfigDto();
destination.OidcConfig.DisablePasswordAuthentication = bool.Parse(row.Value); destination.OidcConfig.DisablePasswordAuthentication = bool.Parse(row.Value);
break; break;
case ServerSettingKey.OidcProviderName:
destination.OidcConfig ??= new OidcConfigDto();
destination.OidcConfig.ProviderName = row.Value;
break;
case ServerSettingKey.LicenseKey: case ServerSettingKey.LicenseKey:
case ServerSettingKey.EnableAuthentication: case ServerSettingKey.EnableAuthentication:
case ServerSettingKey.EmailServiceUrl: case ServerSettingKey.EmailServiceUrl:

View file

@ -464,6 +464,13 @@ public class SettingsService : ISettingsService
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.OidcProviderName &&
updateSettingsDto.OidcConfig.ProviderName + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.OidcConfig.ProviderName + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
} }
private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto) private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)

View file

@ -7,6 +7,7 @@ export interface OidcConfig {
provisionUserSettings: boolean; provisionUserSettings: boolean;
autoLogin: boolean; autoLogin: boolean;
disablePasswordAuthentication: boolean; disablePasswordAuthentication: boolean;
providerName: string;
} }
export interface OidcPublicConfig { export interface OidcPublicConfig {
@ -14,4 +15,5 @@ export interface OidcPublicConfig {
clientId: string; clientId: string;
autoLogin: boolean; autoLogin: boolean;
disablePasswordAuthentication: boolean; disablePasswordAuthentication: boolean;
providerName: string;
} }

View file

@ -10,6 +10,7 @@
</div> </div>
<h4>{{t('provider')}}</h4> <h4>{{t('provider')}}</h4>
<div class="text-muted">{{t('manual-save')}}</div>
<ng-container> <ng-container>
<div class="row g-0 mt-4 mb-4"> <div class="row g-0 mt-4 mb-4">
@if (settingsForm.get('authority'); as formControl) { @if (settingsForm.get('authority'); as formControl) {
@ -62,8 +63,22 @@
<div class="setting-section-break"></div> <div class="setting-section-break"></div>
<h4>{{t('behaviour')}}</h4> <h4>{{t('behaviour')}}</h4>
<ng-container> <ng-container>
<div class="row g-0 mt-4 mb-4">
@if (settingsForm.get('providerName'); as formControl) {
<app-setting-item [title]="t('providerName')" [subtitle]="t('providerName-tooltip')">
<ng-template #view>
{{formControl.value}}
</ng-template>
<ng-template #edit>
<input id="oid-providerName" aria-describedby="oidc-providerName-validations" class="form-control"
formControlName="providerName" type="text"
[class.is-invalid]="formControl.invalid && !formControl.untouched">
</ng-template>
</app-setting-item>
}
</div>
<div class="row g-0 mt-4 mb-4"> <div class="row g-0 mt-4 mb-4">
@if(settingsForm.get('provisionAccounts'); as formControl) { @if(settingsForm.get('provisionAccounts'); as formControl) {
<app-setting-switch [title]="t('provisionAccounts')" [subtitle]="t('provisionAccounts-tooltip')"> <app-setting-switch [title]="t('provisionAccounts')" [subtitle]="t('provisionAccounts-tooltip')">

View file

@ -1,4 +1,4 @@
import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, DestroyRef, OnInit} from '@angular/core';
import {TranslocoDirective} from "@jsverse/transloco"; import {TranslocoDirective} from "@jsverse/transloco";
import {ServerSettings} from "../_models/server-settings"; import {ServerSettings} from "../_models/server-settings";
import { import {
@ -14,7 +14,8 @@ import {SettingsService} from "../settings.service";
import {OidcConfig} from "../_models/oidc-config"; import {OidcConfig} from "../_models/oidc-config";
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
import {map, of} from "rxjs"; import {debounceTime, distinctUntilChanged, filter, map, of, switchMap, tap} from "rxjs";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'app-manage-open-idconnect', selector: 'app-manage-open-idconnect',
@ -36,6 +37,7 @@ export class ManageOpenIDConnectComponent implements OnInit {
constructor( constructor(
private settingsService: SettingsService, private settingsService: SettingsService,
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private destroyRef: DestroyRef,
) { ) {
} }
@ -52,12 +54,27 @@ export class ManageOpenIDConnectComponent implements OnInit {
this.settingsForm.addControl('provisionUserSettings', new FormControl(this.oidcSettings.provisionUserSettings, [])); this.settingsForm.addControl('provisionUserSettings', new FormControl(this.oidcSettings.provisionUserSettings, []));
this.settingsForm.addControl('autoLogin', new FormControl(this.oidcSettings.autoLogin, [])); this.settingsForm.addControl('autoLogin', new FormControl(this.oidcSettings.autoLogin, []));
this.settingsForm.addControl('disablePasswordAuthentication', new FormControl(this.oidcSettings.disablePasswordAuthentication, [])); this.settingsForm.addControl('disablePasswordAuthentication', new FormControl(this.oidcSettings.disablePasswordAuthentication, []));
this.settingsForm.addControl('providerName', new FormControl(this.oidcSettings.providerName, []));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.settingsForm.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef),
filter(() => {
// Do not auto save when provider settings have changed
const settings: OidcConfig = this.settingsForm.getRawValue();
return settings.authority == this.oidcSettings.authority && settings.clientId == this.oidcSettings.clientId;
}),
tap(() => this.save())
).subscribe();
} }
}) });
} }
save() { save() {
if (!this.settingsForm.valid) return;
const data = this.settingsForm.getRawValue(); const data = this.settingsForm.getRawValue();
const newSettings = Object.assign({}, this.serverSettings); const newSettings = Object.assign({}, this.serverSettings);
newSettings.oidcConfig = data as OidcConfig; newSettings.oidcConfig = data as OidcConfig;

View file

@ -42,16 +42,9 @@
class="d-inline-block" class="d-inline-block"
style="object-fit: contain;" style="object-fit: contain;"
/> />
{{t('oidc')}} {{oidcService.settings()?.providerName || t('oidc')}}
</button> </button>
} }
@if (!showPasswordLogin()) {
<div class="text-muted mt-2">
{{t('bypass')}}
<span class="text-muted clickable" (click)="forceShowPasswordLogin.set(true)">{{t('here')}}</span>
</div>
}
} }
</ng-container> </ng-container>
</app-splash-container> </app-splash-container>

View file

@ -59,9 +59,9 @@ export class UserLoginComponent implements OnInit {
* undefined until query params are read * undefined until query params are read
*/ */
skipAutoLogin = signal<boolean | undefined>(undefined); skipAutoLogin = signal<boolean | undefined>(undefined);
/** /**
* Display the login form, regardless if the password authentication is disabled (admins can still log in) * Display the login form, regardless if the password authentication is disabled (admins can still log in)
* Set from query
*/ */
forceShowPasswordLogin = signal(false); forceShowPasswordLogin = signal(false);
/** /**
@ -121,6 +121,7 @@ export class UserLoginComponent implements OnInit {
} }
this.skipAutoLogin.set(params.get('skipAutoLogin') === 'true') this.skipAutoLogin.set(params.get('skipAutoLogin') === 'true')
this.forceShowPasswordLogin.set(params.get('forceShowPassword') === 'true');
}); });
} }

View file

@ -6,9 +6,7 @@
"password-validation": "{{validation.password-validation}}", "password-validation": "{{validation.password-validation}}",
"forgot-password": "Forgot Password?", "forgot-password": "Forgot Password?",
"submit": "Sign in", "submit": "Sign in",
"oidc": "Log in with OpenID Connect", "oidc": "OpenID Connect"
"bypass": "Password login has been disabled. Bypass ",
"here": "here"
}, },
"oidc": { "oidc": {
@ -18,11 +16,12 @@
"settings": { "settings": {
"save": "{{common.save}}", "save": "{{common.save}}",
"notice": "Notice", "notice": "Notice",
"restart-required": "Changing OpenID Connect Authority requires a restart", "restart-required": "Changing Authority, or Client ID requires a manual restart of Kavita to take effect.",
"provider": "Provider", "provider": "Provider",
"behaviour": "Behaviour", "behaviour": "Behaviour",
"field-required": "{{name}} is required when {{other}} is set", "field-required": "{{name}} is required when {{other}} is set",
"invalidUri": "The provider URL is not valid", "invalidUri": "The provider URL is not valid",
"manual-save": "Changing provider settings requires a manual save",
"authority": "Authority", "authority": "Authority",
"authority-tooltip": "The URL to your OpenID Connect provider", "authority-tooltip": "The URL to your OpenID Connect provider",
@ -37,7 +36,9 @@
"autoLogin": "Auto login", "autoLogin": "Auto login",
"autoLogin-tooltip": "Auto redirect to OpenID Connect provider when opening the login screen", "autoLogin-tooltip": "Auto redirect to OpenID Connect provider when opening the login screen",
"disablePasswordAuthentication": "Disable password authentication", "disablePasswordAuthentication": "Disable password authentication",
"disablePasswordAuthentication-tooltip": "Users with the admin role can bypass this restriction" "disablePasswordAuthentication-tooltip": "Users with the admin role can bypass this restriction",
"providerName": "Provider name",
"providerName-tooltip": "Name show on the login screen"
} }
}, },