Cleanup, nicer flow
This commit is contained in:
parent
465723fedf
commit
0b64ea1622
15 changed files with 184 additions and 179 deletions
|
@ -1,6 +1,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Settings;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -8,16 +9,14 @@ using Microsoft.Extensions.Logging;
|
|||
namespace API.Controllers;
|
||||
|
||||
[AllowAnonymous]
|
||||
public class OidcController(ILogger<OidcController> logger, IUnitOfWork unitOfWork): BaseApiController
|
||||
public class OidcController(ILogger<OidcController> logger, IUnitOfWork unitOfWork, IMapper mapper): BaseApiController
|
||||
{
|
||||
|
||||
// TODO: Decide what we want to expose here, not really anything useful in it. But the discussion is needed
|
||||
// Public endpoint
|
||||
[HttpGet("config")]
|
||||
public async Task<ActionResult<OidcConfigDto>> GetOidcConfig()
|
||||
public async Task<ActionResult<OidcPublicConfigDto>> GetOidcConfig()
|
||||
{
|
||||
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
return Ok(settings.OidcConfig);
|
||||
return Ok(mapper.Map<OidcPublicConfigDto>(settings.OidcConfig));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
12
API/DTOs/Settings/OidcPublicConfigDto.cs
Normal file
12
API/DTOs/Settings/OidcPublicConfigDto.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#nullable enable
|
||||
namespace API.DTOs.Settings;
|
||||
|
||||
public sealed record OidcPublicConfigDto
|
||||
{
|
||||
/// <inheritdoc cref="OidcConfigDto.Authority"/>
|
||||
public string? Authority { get; set; }
|
||||
/// <inheritdoc cref="OidcConfigDto.ClientId"/>
|
||||
public string? ClientId { get; set; }
|
||||
/// <inheritdoc cref="OidcConfigDto.AutoLogin"/>
|
||||
public bool AutoLogin { get; set; }
|
||||
}
|
|
@ -123,7 +123,7 @@ public static class IdentityServiceExtensions
|
|||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = SetTokenFromQuery,
|
||||
OnTokenValidated = OidcClaimsPrincipalConverter
|
||||
OnTokenValidated = OidcClaimsPrincipalConverter,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -386,7 +386,6 @@ public class AutoMapperProfiles : Profile
|
|||
.ForMember(dest => dest.Overrides, opt => opt.MapFrom(src => src.Overrides ?? new List<MetadataSettingField>()))
|
||||
.ForMember(dest => dest.AgeRatingMappings, opt => opt.MapFrom(src => src.AgeRatingMappings ?? new Dictionary<string, AgeRating>()));
|
||||
|
||||
|
||||
|
||||
CreateMap<OidcConfigDto, OidcPublicConfigDto>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,21 +41,21 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
|
||||
var externalId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(externalId))
|
||||
throw new KavitaException("oidc.errors.missing-external-id");
|
||||
throw new KavitaException("errors.oidc.missing-external-id");
|
||||
|
||||
var user = await unitOfWork.UserRepository.GetByExternalId(externalId, AppUserIncludes.UserPreferences);
|
||||
if (user != null)
|
||||
{
|
||||
// await ProvisionUserSettings(settings, principal, user);
|
||||
//await SyncUserSettings(settings, principal, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
var email = principal.FindFirstValue(ClaimTypes.Email);
|
||||
if (string.IsNullOrEmpty(email))
|
||||
throw new KavitaException("oidc.errors.missing-email");
|
||||
throw new KavitaException("errors.oidc.missing-email");
|
||||
|
||||
if (settings.RequireVerifiedEmail && !principal.HasVerifiedEmail())
|
||||
throw new KavitaException("oidc.errors.email-not-verified");
|
||||
throw new KavitaException("errors.oidc.email-not-verified");
|
||||
|
||||
|
||||
user = await unitOfWork.UserRepository.GetUserByEmailAsync(email, AppUserIncludes.UserPreferences)
|
||||
|
@ -64,11 +64,11 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
|
||||
user.ExternalId = externalId;
|
||||
|
||||
// await ProvisionUserSettings(settings, principal, user);
|
||||
//await SyncUserSettings(settings, principal, user);
|
||||
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
if (roles.Count > 0 && !roles.Contains(PolicyConstants.LoginRole))
|
||||
throw new KavitaException("oidc.errors.disabled-account");
|
||||
throw new KavitaException("errors.oidc.disabled-account");
|
||||
|
||||
return user;
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
{
|
||||
logger.LogError("Failed to create new user from OIDC: {Errors}",
|
||||
res.Errors.Select(x => x.Description).ToString());
|
||||
throw new KavitaException("oidc.errors.creating-user");
|
||||
throw new KavitaException("errors.oidc.creating-user");
|
||||
}
|
||||
|
||||
AddDefaultStreamsToUser(user, mapper);
|
||||
|
@ -151,7 +151,7 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
if (roles.Count == 0) return;
|
||||
|
||||
var errors = await accountService.UpdateRolesForUser(user, roles);
|
||||
if (errors.Any()) throw new KavitaException("oidc.errors.syncing-user");
|
||||
if (errors.Any()) throw new KavitaException("errors.oidc.syncing-user");
|
||||
}
|
||||
|
||||
private async Task SyncLibraries(ClaimsPrincipal claimsPrincipal, AppUser user)
|
||||
|
|
|
@ -11,6 +11,7 @@ import {NavigationEnd, Router} from "@angular/router";
|
|||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {SettingsTabId} from "../sidenav/preference-nav/preference-nav.component";
|
||||
import {WikiLink} from "../_models/wiki";
|
||||
import {OidcService} from "./oidc.service";
|
||||
|
||||
/**
|
||||
* NavItem used to construct the dropdown or NavLinkModal on mobile
|
||||
|
@ -34,6 +35,7 @@ interface NavItem {
|
|||
export class NavService {
|
||||
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly oidcService = inject(OidcService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
@ -173,12 +175,28 @@ export class NavService {
|
|||
}
|
||||
|
||||
logout() {
|
||||
this.oidcService.logout();
|
||||
this.accountService.logout();
|
||||
this.hideNavBar();
|
||||
this.hideSideNav();
|
||||
this.router.navigateByUrl('/login');
|
||||
}
|
||||
|
||||
handleLogin() {
|
||||
this.showNavBar();
|
||||
this.showSideNav();
|
||||
|
||||
// Check if user came here from another url, else send to library route
|
||||
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
|
||||
if (pageResume && pageResume !== '/login') {
|
||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||
this.router.navigateByUrl(pageResume);
|
||||
} else {
|
||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||
this.router.navigateByUrl('/home');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the side nav. When being visible, the side nav will automatically return to previous collapsed state.
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import {DestroyRef, Injectable} from '@angular/core';
|
||||
import {OAuthService} from "angular-oauth2-oidc";
|
||||
import {DestroyRef, effect, inject, Injectable, signal} from '@angular/core';
|
||||
import {OAuthErrorEvent, OAuthService} from "angular-oauth2-oidc";
|
||||
import {BehaviorSubject, from} from "rxjs";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {OidcConfig} from "../admin/_models/oidc-config";
|
||||
import {OidcPublicConfig} from "../admin/_models/oidc-config";
|
||||
import {AccountService} from "./account.service";
|
||||
import {NavService} from "./nav.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {take} from "rxjs/operators";
|
||||
|
||||
|
@ -15,24 +13,29 @@ import {take} from "rxjs/operators";
|
|||
})
|
||||
export class OidcService {
|
||||
|
||||
/*
|
||||
TODO: Further cleanup, nicer handling for the user
|
||||
See: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards
|
||||
Service: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/blob/master/src/app/core/auth.service.ts
|
||||
*/
|
||||
private readonly oauth2 = inject(OAuthService);
|
||||
private readonly httpClient = inject(HttpClient);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
settingsSource = new BehaviorSubject<OidcConfig | null>(null);
|
||||
settings$ = this.settingsSource.asObservable();
|
||||
|
||||
constructor(
|
||||
private oauth2: OAuthService,
|
||||
private httpClient: HttpClient,
|
||||
private accountService: AccountService,
|
||||
private navService: NavService,
|
||||
private router: Router,
|
||||
private destroyRef: DestroyRef,
|
||||
) {
|
||||
private readonly _ready = signal(false);
|
||||
public readonly ready = this._ready.asReadonly();
|
||||
private readonly _settings = signal<OidcPublicConfig | undefined>(undefined);
|
||||
public readonly settings = this._settings.asReadonly();
|
||||
|
||||
constructor() {
|
||||
// log events in dev
|
||||
if (!environment.production) {
|
||||
this.oauth2.events.subscribe(event => {
|
||||
if (event instanceof OAuthErrorEvent) {
|
||||
console.error('OAuthErrorEvent Object:', event);
|
||||
} else {
|
||||
console.debug('OAuthEvent Object:', event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.config().subscribe(oidcSetting => {
|
||||
if (!oidcSetting.authority) {
|
||||
|
@ -42,14 +45,15 @@ export class OidcService {
|
|||
this.oauth2.configure({
|
||||
issuer: oidcSetting.authority,
|
||||
clientId: oidcSetting.clientId,
|
||||
requireHttps: oidcSetting.authority.startsWith("https://"),
|
||||
// Require https in production unless localhost
|
||||
requireHttps: environment.production ? 'remoteOnly' : false,
|
||||
redirectUri: window.location.origin + "/oidc/callback",
|
||||
postLogoutRedirectUri: window.location.origin + "/login",
|
||||
showDebugInformation: true,
|
||||
showDebugInformation: !environment.production,
|
||||
responseType: 'code',
|
||||
scope: "openid profile email roles offline_access",
|
||||
});
|
||||
this.settingsSource.next(oidcSetting);
|
||||
this._settings.set(oidcSetting);
|
||||
this.oauth2.setupAutomaticSilentRefresh();
|
||||
|
||||
this.oauth2.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
|
||||
|
@ -67,7 +71,7 @@ export class OidcService {
|
|||
next: success => {
|
||||
if (!success) return;
|
||||
|
||||
this.tryLogin();
|
||||
this._ready.set(true);
|
||||
},
|
||||
error: error => {
|
||||
console.log(error);
|
||||
|
@ -76,52 +80,21 @@ export class OidcService {
|
|||
})
|
||||
}
|
||||
|
||||
private tryLogin() {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) return;
|
||||
|
||||
if (this.token) {
|
||||
this.accountService.loginByToken(this.token).subscribe({
|
||||
next: _ => {
|
||||
this.doLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
oidcLogin() {
|
||||
login() {
|
||||
this.oauth2.initLoginFlow();
|
||||
}
|
||||
|
||||
config() {
|
||||
return this.httpClient.get<OidcConfig>(this.baseUrl + "oidc/config");
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this.oauth2.getAccessToken();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.oauth2.logOut();
|
||||
}
|
||||
|
||||
private doLogin() {
|
||||
this.navService.showNavBar();
|
||||
this.navService.showSideNav();
|
||||
|
||||
// Check if user came here from another url, else send to library route
|
||||
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
|
||||
if (pageResume && pageResume !== '/login') {
|
||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||
this.router.navigateByUrl(pageResume);
|
||||
} else {
|
||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||
this.router.navigateByUrl('/home');
|
||||
}
|
||||
config() {
|
||||
return this.httpClient.get<OidcPublicConfig>(this.baseUrl + "oidc/config");
|
||||
}
|
||||
|
||||
|
||||
get token() {
|
||||
return this.oauth2.getAccessToken();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,3 +7,9 @@ export interface OidcConfig {
|
|||
provisionUserSettings: boolean;
|
||||
autoLogin: boolean;
|
||||
}
|
||||
|
||||
export interface OidcPublicConfig {
|
||||
authority: string;
|
||||
clientId: string;
|
||||
autoLogin: boolean;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
DestroyRef,
|
||||
DestroyRef, effect,
|
||||
HostListener,
|
||||
inject,
|
||||
OnInit
|
||||
|
@ -100,6 +100,21 @@ export class AppComponent implements OnInit {
|
|||
|
||||
this.localizationService.getLocales().subscribe(); // This will cache the localizations on startup
|
||||
|
||||
// Login automatically when a token is available
|
||||
effect(() => {
|
||||
const ready = this.oidcService.ready();
|
||||
if (!ready || !this.oidcService.token) return;
|
||||
|
||||
this.accountService.loginByToken(this.oidcService.token).subscribe({
|
||||
next: () => {
|
||||
this.navService.handleLogin();
|
||||
},
|
||||
error: err => {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
|
|
|
@ -48,7 +48,6 @@ import {Breakpoint, UtilityService} from "../../../shared/_services/utility.serv
|
|||
import {WikiLink} from "../../../_models/wiki";
|
||||
import {NavLinkModalComponent} from "../nav-link-modal/nav-link-modal.component";
|
||||
import {MetadataService} from "../../../_services/metadata.service";
|
||||
import {OidcService} from "../../../_services/oidc.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-nav-header',
|
||||
|
@ -66,7 +65,6 @@ export class NavHeaderComponent implements OnInit {
|
|||
private readonly searchService = inject(SearchService);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
private readonly oidcService = inject(OidcService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly navService = inject(NavService);
|
||||
|
@ -136,14 +134,6 @@ export class NavHeaderComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.accountService.logout();
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
this.oidcService.logout();
|
||||
this.router.navigateByUrl('/login');
|
||||
}
|
||||
|
||||
moveFocus() {
|
||||
this.document.getElementById('content')?.focus();
|
||||
}
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<ng-container *transloco="let t; read: 'oidc'">
|
||||
<app-splash-container>
|
||||
<ng-container title><h2>{{t('title')}}</h2></ng-container>
|
||||
<ng-container body>
|
||||
|
||||
@if (error.length > 0) {
|
||||
<div class="invalid-feedback mb-2">
|
||||
{{t(error)}}
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn btn-outline-primary" (click)="goToLogin()">{{t('login')}}</button>
|
||||
</ng-container>
|
||||
</app-splash-container>
|
||||
<ng-container *transloco="let t; prefix: 'oidc'">
|
||||
@if (showSplash()) {
|
||||
<app-splash-container>
|
||||
<ng-container title><h2>{{t('title')}}</h2></ng-container>
|
||||
<ng-container body>
|
||||
<button class="btn btn-outline-primary" (click)="goToLogin()">{{t('login')}}</button>
|
||||
</ng-container>
|
||||
</app-splash-container>
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import {ChangeDetectorRef, Component, OnInit} from '@angular/core';
|
||||
import {ChangeDetectorRef, Component, OnInit, signal} from '@angular/core';
|
||||
import {SplashContainerComponent} from "../_components/splash-container/splash-container.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {NavService} from "../../_services/nav.service";
|
||||
import {take} from "rxjs/operators";
|
||||
import {OidcService} from "../../_services/oidc.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-oidc-callback',
|
||||
|
@ -16,16 +15,15 @@ import {OidcService} from "../../_services/oidc.service";
|
|||
templateUrl: './oidc-callback.component.html',
|
||||
styleUrl: './oidc-callback.component.scss'
|
||||
})
|
||||
export class OidcCallbackComponent implements OnInit{
|
||||
export class OidcCallbackComponent implements OnInit {
|
||||
|
||||
error: string = '';
|
||||
showSplash = signal(false);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private router: Router,
|
||||
private navService: NavService,
|
||||
private readonly cdRef: ChangeDetectorRef,
|
||||
private oidcService: OidcService,
|
||||
) {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
|
@ -40,6 +38,9 @@ export class OidcCallbackComponent implements OnInit{
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
// Show back to log in splash only after 1s, for a more seamless experience
|
||||
setTimeout(() => this.showSplash.set(true), 1000);
|
||||
}
|
||||
|
||||
goToLogin() {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<app-splash-container>
|
||||
<ng-container title><h2>{{t('title')}}</h2></ng-container>
|
||||
<ng-container body>
|
||||
<ng-container *ngIf="isLoaded">
|
||||
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow">
|
||||
<ng-container *ngIf="isLoaded()">
|
||||
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow()">
|
||||
<div class="card-text">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label visually-hidden">{{t('username')}}</label>
|
||||
|
@ -22,13 +22,13 @@
|
|||
</div>
|
||||
|
||||
<div class="sign-in">
|
||||
<button class="btn btn-outline-primary" type="submit" [disabled]="isSubmitting">{{t('submit')}}</button>
|
||||
<button class="btn btn-outline-primary" type="submit" [disabled]="isSubmitting()">{{t('submit')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (oidcEnabled) {
|
||||
<button [ngbTooltip]="t('oidc-tooltip')" class="btn btn-outline-primary mt-2" (click)="oidcService.oidcLogin()">{{t('oidc')}}</button>
|
||||
@if (oidcService.ready()) {
|
||||
<button [ngbTooltip]="t('oidc-tooltip')" class="btn btn-outline-primary mt-2" (click)="oidcService.login()">{{t('oidc')}}</button>
|
||||
}
|
||||
|
||||
</ng-container>
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import {AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, computed,
|
||||
DestroyRef, effect, inject,
|
||||
OnInit,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
|
||||
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||
|
@ -13,6 +21,7 @@ import {TRANSLOCO_SCOPE, TranslocoDirective} from "@jsverse/transloco";
|
|||
import {environment} from "../../../environments/environment";
|
||||
import {OidcService} from "../../_services/oidc.service";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -24,6 +33,15 @@ import {forkJoin} from "rxjs";
|
|||
})
|
||||
export class UserLoginComponent implements OnInit {
|
||||
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly memberService = inject(MemberService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly navService = inject(NavService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
protected readonly oidcService = inject(OidcService);
|
||||
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
loginForm: FormGroup = new FormGroup({
|
||||
|
@ -34,27 +52,31 @@ export class UserLoginComponent implements OnInit {
|
|||
/**
|
||||
* If there are no admins on the server, this will enable the registration to kick in.
|
||||
*/
|
||||
firstTimeFlow: boolean = true;
|
||||
firstTimeFlow = signal(true);
|
||||
/**
|
||||
* Used for first time the page loads to ensure no flashing
|
||||
*/
|
||||
isLoaded: boolean = false;
|
||||
isSubmitting = false;
|
||||
oidcEnabled = false;
|
||||
isLoaded = signal(false);
|
||||
isSubmitting = signal(false);
|
||||
/**
|
||||
* undefined until query params are read
|
||||
*/
|
||||
skipAutoLogin = signal<boolean | undefined>(undefined)
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private router: Router,
|
||||
private memberService: MemberService,
|
||||
private toastr: ToastrService,
|
||||
private navService: NavService,
|
||||
private readonly cdRef: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
protected oidcService: OidcService,
|
||||
) {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
}
|
||||
constructor() {
|
||||
this.navService.hideNavBar();
|
||||
this.navService.hideSideNav();
|
||||
|
||||
effect(() => {
|
||||
const skipAutoLogin = this.skipAutoLogin();
|
||||
const oidcConfig = this.oidcService.settings();
|
||||
if (!oidcConfig || skipAutoLogin === undefined) return;
|
||||
|
||||
if (oidcConfig.autoLogin && !skipAutoLogin) {
|
||||
this.oidcService.login()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
|
@ -68,34 +90,24 @@ export class UserLoginComponent implements OnInit {
|
|||
|
||||
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
this.firstTimeFlow = !adminExists;
|
||||
this.firstTimeFlow.set(!adminExists);
|
||||
|
||||
if (this.firstTimeFlow) {
|
||||
if (this.firstTimeFlow()) {
|
||||
this.router.navigateByUrl('registration/register');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoaded = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.isLoaded.set(true);
|
||||
});
|
||||
|
||||
this.route.queryParamMap.subscribe(params => {
|
||||
const val = params.get('apiKey');
|
||||
if (val != null && val.length > 0) {
|
||||
this.login(val);
|
||||
return;
|
||||
}
|
||||
|
||||
const skipAutoLogin = params.get('skipAutoLogin') === 'true';
|
||||
this.oidcService.settings$.subscribe(cfg => {
|
||||
if (!cfg) return;
|
||||
|
||||
this.oidcEnabled = cfg.authority !== "";
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
if (cfg.autoLogin && !skipAutoLogin) {
|
||||
this.oidcService.oidcLogin()
|
||||
}
|
||||
});
|
||||
this.skipAutoLogin.set(params.get('skipAutoLogin') === 'true')
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -104,33 +116,18 @@ export class UserLoginComponent implements OnInit {
|
|||
login(apiKey: string = '') {
|
||||
const model = this.loginForm.getRawValue();
|
||||
model.apiKey = apiKey;
|
||||
this.isSubmitting = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.accountService.login(model).subscribe(() => {
|
||||
this.loginForm.reset();
|
||||
this.doLogin()
|
||||
this.isSubmitting.set(true);
|
||||
this.accountService.login(model).subscribe({
|
||||
next: () => {
|
||||
this.loginForm.reset();
|
||||
this.navService.handleLogin()
|
||||
|
||||
this.isSubmitting = false;
|
||||
this.cdRef.markForCheck();
|
||||
}, err => {
|
||||
this.toastr.error(err.error);
|
||||
this.isSubmitting = false;
|
||||
this.cdRef.markForCheck();
|
||||
this.isSubmitting.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastr.error(err.error);
|
||||
this.isSubmitting.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private doLogin() {
|
||||
this.navService.showNavBar();
|
||||
this.navService.showSideNav();
|
||||
|
||||
// Check if user came here from another url, else send to library route
|
||||
const pageResume = localStorage.getItem('kavita--auth-intersection-url');
|
||||
if (pageResume && pageResume !== '/login') {
|
||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||
this.router.navigateByUrl(pageResume);
|
||||
} else {
|
||||
localStorage.setItem('kavita--auth-intersection-url', '');
|
||||
this.router.navigateByUrl('/home');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,13 +13,6 @@
|
|||
"oidc": {
|
||||
"title": "OpenID Connect Callback",
|
||||
"login": "Back to login screen",
|
||||
"errors": {
|
||||
"missing-external-id": "OpenID Connect provider did not return a valid identifier",
|
||||
"missing-email": "OpenID Connect provider did not return a valid email",
|
||||
"email-not-verified": "Your email must be verified to allow logging in via OpenID Connect",
|
||||
"no-account": "No matching account found",
|
||||
"disabled-account": "This account is disabled, please contact an administrator"
|
||||
},
|
||||
"settings": {
|
||||
"save": "{{common.save}}",
|
||||
"notice": "Notice",
|
||||
|
@ -2455,7 +2448,14 @@
|
|||
"invalid-password-reset-url": "Invalid reset password url",
|
||||
"delete-theme-in-use": "Theme is currently in use by at least one user, cannot delete",
|
||||
"theme-manual-upload": "There was an issue creating Theme from manual upload",
|
||||
"theme-already-in-use": "Theme already exists by that name"
|
||||
"theme-already-in-use": "Theme already exists by that name",
|
||||
"oidc": {
|
||||
"missing-external-id": "OpenID Connect provider did not return a valid identifier",
|
||||
"missing-email": "OpenID Connect provider did not return a valid email",
|
||||
"email-not-verified": "Your email must be verified to allow logging in via OpenID Connect",
|
||||
"no-account": "No matching account found",
|
||||
"disabled-account": "This account is disabled, please contact an administrator"
|
||||
}
|
||||
},
|
||||
|
||||
"metadata-builder": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue