💄 TOTP auth

This commit is contained in:
itskovacs 2025-10-28 18:19:11 +01:00
parent c0daaf5820
commit 040f58fafe
2 changed files with 48 additions and 8 deletions

View File

@ -1,11 +1,21 @@
<section class="flex flex-col md:flex-row h-screen items-center"> <section class="flex flex-col md:flex-row h-screen items-center">
<div <div
class="relative bg-white dark:bg-surface-900 w-full md:max-w-md lg:max-w-full md:mx-auto md:w-1/2 xl:w-1/3 h-screen px-6 lg:px-16 xl:px-12 flex items-center justify-center"> class="relative bg-white dark:bg-surface-900 w-full md:max-w-md lg:max-w-full md:mx-auto md:w-1/2 xl:w-1/3 h-screen px-6 lg:px-16 xl:px-12 flex items-center justify-center">
<div class="max-w-xl w-full mx-auto h-100"> <div class="max-w-xl w-full mx-auto">
<div class="max-w-32 mx-auto"> <div class="max-w-32 mx-auto">
<img src="favicon.png" /> <img src="favicon.png" />
</div> </div>
<div class="block lg:hidden w-full">
<div class="mt-4 text-center">
<div class="text-4xl font-bold leading-none">
Welcome to TRIP
</div>
<div class="mt-3 text-lg tracking-tight leading-6 text-gray-400">Tourism and Recreational Interest Points.
</div>
</div>
</div>
<div class="text-xl md:text-2xl font-bold leading-tight mt-10"> <div class="text-xl md:text-2xl font-bold leading-tight mt-10">
{{ isRegistering ? "Register" : "Sign in" }} {{ isRegistering ? "Register" : "Sign in" }}
</div> </div>
@ -18,6 +28,18 @@
</div> </div>
} }
@if (pendingOTP) {
<div pFocusTrap>
<p class="mt-8 text-center font-light text-gray-500">
Enter your TOTP code
</p>
<div class="mt-4 flex gap-2 flex-col items-center">
<p-inputotp [(ngModel)]="otp" [integerOnly]="true" size="large" [length]="6" />
<p-button text label="Confirm" (click)="verifyTOTP()" />
</div>
</div>
} @else {
@defer () { @defer () {
@if (authParams?.oidc) { @if (authParams?.oidc) {
<div class="mt-8 text-center"> <div class="mt-8 text-center">
@ -91,6 +113,7 @@
<p-skeleton width="80px" height="2.5rem" /> <p-skeleton width="80px" height="2.5rem" />
</div> </div>
} }
}
</div> </div>
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-sm flex flex-col items-center gap-2"> <div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-sm flex flex-col items-center gap-2">

View File

@ -1,15 +1,16 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FloatLabelModule } from 'primeng/floatlabel'; import { FloatLabelModule } from 'primeng/floatlabel';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { FocusTrapModule } from 'primeng/focustrap'; import { FocusTrapModule } from 'primeng/focustrap';
import { AuthParams, AuthService } from '../../services/auth.service'; import { AuthParams, AuthService, TOTPRequired, Token } from '../../services/auth.service';
import { MessageModule } from 'primeng/message'; import { MessageModule } from 'primeng/message';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { SkeletonModule } from 'primeng/skeleton'; import { SkeletonModule } from 'primeng/skeleton';
import { InputOtpModule } from 'primeng/inputotp';
import { take } from 'rxjs'; import { take } from 'rxjs';
@Component({ @Component({
@ -19,10 +20,12 @@ import { take } from 'rxjs';
FloatLabelModule, FloatLabelModule,
ReactiveFormsModule, ReactiveFormsModule,
ButtonModule, ButtonModule,
FormsModule,
InputTextModule, InputTextModule,
SkeletonModule, SkeletonModule,
FocusTrapModule, FocusTrapModule,
MessageModule, MessageModule,
InputOtpModule,
], ],
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrl: './auth.component.scss', styleUrl: './auth.component.scss',
@ -33,6 +36,9 @@ export class AuthComponent implements OnInit {
authForm: FormGroup; authForm: FormGroup;
error: string | undefined; error: string | undefined;
isRegistering: boolean = false; isRegistering: boolean = false;
pendingOTP: string = '';
otp: string = '';
username: string = '';
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@ -99,11 +105,12 @@ export class AuthComponent implements OnInit {
this.authService.login(this.authForm.value).subscribe({ this.authService.login(this.authForm.value).subscribe({
next: (data) => { next: (data) => {
if (!data.access_token) { if ((data as Token)?.access_token) this.router.navigateByUrl(this.redirectURL);
this.error = 'Authentication failed';
return; // If we're here, it means it's OTP time
} this.username = (data as TOTPRequired).username;
this.router.navigateByUrl(this.redirectURL); this.pendingOTP = (data as TOTPRequired).pending_code;
this.authForm.reset();
}, },
error: () => { error: () => {
this.authForm.reset(); this.authForm.reset();
@ -111,6 +118,16 @@ export class AuthComponent implements OnInit {
}); });
} }
verifyTOTP(): void {
this.error = '';
this.authService.verify_totp(this.username, this.pendingOTP, this.otp).subscribe({
next: (token) => {
if (token) this.router.navigateByUrl(this.redirectURL);
},
error: () => (this.otp = ''),
});
}
getAuthParams() { getAuthParams() {
this.authService this.authService
.authParams() .authParams()