prototype login frontend

This commit is contained in:
Christian Werner 2025-07-14 23:22:41 +02:00
parent ae59139fdf
commit a3a9e57dea
12 changed files with 148 additions and 37 deletions

View File

@ -73,6 +73,9 @@
"buildTarget": "frontend:build:development"
}
},
"options": {
"proxyConfig": "src/proxy.conf.json"
},
"defaultConfiguration": "development"
},
"extract-i18n": {

View File

@ -2,7 +2,17 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import {GandalfClientBaseAPI} from './clients/gandalf/gandalf-client';
import {provideHttpClient} from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
{
provide: GandalfClientBaseAPI,
useValue: 'http://localhost:5055'
}
]
};

View File

@ -0,0 +1,20 @@
import {inject, InjectionToken} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {catchError, Observable, ObservableInput, throwError} from 'rxjs';
export const GandalfClientBaseAPI = new InjectionToken<string>('GandalfClientBaseAPIToken');
export abstract class GandalfClient {
protected readonly basePath = inject(GandalfClientBaseAPI);
protected readonly http = inject(HttpClient);
protected handleRequest<T>(request: Observable<T>, onError?: (error: any) => void): Observable<T> {
return request.pipe(catchError((x, y) => this.handleError(x, y, onError)));
}
private handleError<T>(err: any, _: Observable<T>, onError?: (error: any) => void): Observable<never> {
onError?.(err);
return throwError(() => err);
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import {GandalfClient} from '../gandalf-client';
import {lastValueFrom, Observable, ObservableInput} from 'rxjs';
export interface ValidateCredentialsCommand {
usernameOrEmail: string;
password: string;
keep: boolean;
}
@Injectable({
providedIn: 'root'
})
export class AuthService extends GandalfClient {
private base = '/api/auth';
check(): Observable<boolean> {
return this.handleRequest(this.http.get<boolean>(this.base + '/check'));
}
login(command: ValidateCredentialsCommand, onError?: (error: any) => void): Observable<void> {
return this.handleRequest(this.http.post<void>(this.base + '/login', command), onError);
}
loginAsync(command: ValidateCredentialsCommand, onError?: (error: any) => void): Promise<void> {
return lastValueFrom(this.login(command, onError));
}
}

View File

@ -1,8 +1,15 @@
<form (ngSubmit)="onSubmit($event)">
<input required type="text" name="username" placeholder="Username">
<input required type="password" name="password" placeholder="*****">
@if (errors().length > 0) {
<div class="errors">
@for (error of errors(); track error) {
<div class="error">{{ error }}</div>
}
</div>
}
<form [formGroup]="loginFormGroup" (ngSubmit)="onSubmit($event)">
<input required type="text" name="username" placeholder="Username" formControlName="usernameOrEmail">
<input required type="password" name="password" placeholder="*****" formControlName="password">
<div class="group">
<input id="remember" type="checkbox" name="remember">
<input id="remember" type="checkbox" name="remember" formControlName="keep">
<label for="remember">Keep me signed in</label><br>
<input type="submit" value="Login">
</div>

View File

@ -1,10 +1,12 @@
import { Component } from '@angular/core';
import {FormsModule} from '@angular/forms';
import {Component, inject, signal} from '@angular/core';
import {FormControl, FormGroup, FormsModule, FormSubmittedEvent, ReactiveFormsModule} from '@angular/forms';
import {AuthService, ValidateCredentialsCommand} from '../../clients/gandalf/mithrandir/auth.service';
@Component({
selector: 'app-login',
imports: [
FormsModule
FormsModule,
ReactiveFormsModule
],
templateUrl: './login.component.html',
styleUrl: './login.component.scss',
@ -12,8 +14,29 @@ import {FormsModule} from '@angular/forms';
})
export class LoginComponent {
onSubmit($event: Event): void {
private auth = inject(AuthService);
protected loginFormGroup = new FormGroup({
usernameOrEmail: new FormControl('housemasterr'),
password: new FormControl('kR0pNCspBKx8lOzAIch5'),
keep: new FormControl(false),
});
protected errors = signal<string[]>([]);
protected async onSubmit($event: Event): Promise<void> {
$event.preventDefault();
console.log('Login form submitted');
this.errors.set([]);
if (!this.loginFormGroup.valid) {
return;
}
await this.auth.loginAsync(this.loginFormGroup.value as unknown as ValidateCredentialsCommand, error => {
this.errors.set([...this.errors(), error.error.trim()]);
});
console.log('login successful');
}
}

View File

@ -0,0 +1,7 @@
{
"/api": {
"target": "http://localhost:5055",
"secure": false,
"logLevel": "debug"
}
}

View File

@ -88,7 +88,7 @@ public class AuthController : ControllerBase
{
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Lax,
SameSite = SameSiteMode.None,
Expires = tokenRequestResponse.RefreshTokenExpiresAt.AddSeconds(-10)
});

View File

@ -26,6 +26,12 @@ var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true) // allow any origin
//.WithOrigins("https://localhost:44351")); // Allow only this origin can also have multiple origins separated with comma
.AllowCredentials()); // allow credentials
}
app.UseHttpsRedirection();

View File

@ -70,7 +70,6 @@ public class TokenRequestCommandHandler(TimeProvider timeProvider, IHashids hash
{
public async Task<Result<TokenRequestResponse>> Handle(TokenRequestCommand command, CancellationToken cancellationToken)
{
ValidateCredentialsResponse validateCredentialsResponse;
CreateTokensCommand createTokensCommand;
if (command.GrandType == GrandType.Password)
{
@ -85,7 +84,7 @@ public class TokenRequestCommandHandler(TimeProvider timeProvider, IHashids hash
return validateCredentialsResult.AsErrorResult<TokenRequestResponse, ValidateCredentialsResponse>();
}
validateCredentialsResponse = validateCredentialsResult.GetValue();
var validateCredentialsResponse = validateCredentialsResult.GetValue();
if (!validateCredentialsResponse.IsValid)
{

View File

@ -6,14 +6,14 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPalantirClient(this IServiceCollection services)
{
services.AddHttpClient<IPalantirInternalAuthClient, PalantirInternalAuthClient>(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/auth/"); })
services.AddHttpClient<IPalantirInternalAuthClient, PalantirInternalAuthClient>(opt => { opt.BaseAddress = new Uri("http://localhost:5035/api/internal/auth/"); })
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
services.AddHttpClient<IPalantirInternalAppClient, PalantirInternalAppClient>(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/app/"); })
services.AddHttpClient<IPalantirInternalAppClient, PalantirInternalAppClient>(opt => { opt.BaseAddress = new Uri("http://localhost:5035/api/internal/app/"); })
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production
{
ServerCertificateCustomValidationCallback =

View File

@ -7,6 +7,22 @@ using Suspectus.Gandalf.Core.Abstractions.Extensions;
namespace Suspectus.Gandalf.Palantir.Client;
public abstract class ClientBase
{
protected readonly HttpClient Http;
protected ClientBase(HttpClient http)
{
Http = http;
}
protected static async Task<Result<T>> HandleNotSuccess<T>(HttpResponseMessage response)
{
var body = await response.Content.ReadAsStringAsync();
return $"status: {response.StatusCode}{(string.IsNullOrWhiteSpace(body) ? "" : $" message: {body}")}".AsErrorResult<T>();
}
}
public interface IPalantirClient
{
IPalantirInternalClient Internal { get; init; }
@ -58,24 +74,19 @@ public class PalantirClient : IPalantirClient
public IPalantirInternalClient Internal { get; init; }
}
public class PalantirInternalAuthClient : IPalantirInternalAuthClient
public class PalantirInternalAuthClient : ClientBase, IPalantirInternalAuthClient
{
private readonly HttpClient _http;
public PalantirInternalAuthClient(HttpClient http)
{
_http = http;
}
public PalantirInternalAuthClient(HttpClient http) : base(http) {}
public async Task<Result<bool>> ValidateCredentials(ValidateCredentialsCommand validateCredentialsCommand)
{
try
{
var response = await _http.PostAsJsonAsync("validate-credentials", validateCredentialsCommand);
var response = await Http.PostAsJsonAsync("validate-credentials", validateCredentialsCommand);
if (!response.IsSuccessStatusCode)
{
return $"status: {response.StatusCode}".AsErrorResult<bool>();
return await HandleNotSuccess<bool>(response);
}
var result = await response.Content.ReadFromJsonAsync<bool>();
@ -91,11 +102,11 @@ public class PalantirInternalAuthClient : IPalantirInternalAuthClient
{
try
{
var response = await _http.PostAsJsonAsync("token", command);
var response = await Http.PostAsJsonAsync("token", command);
if (!response.IsSuccessStatusCode)
{
return $"status: {response.StatusCode}".AsErrorResult<TokenRequestResponse?>();
return await HandleNotSuccess<TokenRequestResponse?>(response);
}
var result = await response.Content.ReadFromJsonAsync<TokenRequestResponse?>();
@ -108,24 +119,19 @@ public class PalantirInternalAuthClient : IPalantirInternalAuthClient
}
}
public class PalantirInternalAppClient : IPalantirInternalAppClient
public class PalantirInternalAppClient : ClientBase, IPalantirInternalAppClient
{
private readonly HttpClient _http;
public PalantirInternalAppClient(HttpClient http)
{
_http = http;
}
public PalantirInternalAppClient(HttpClient http) : base(http) {}
public async Task<Result<AppInfo>> GetInfo(string appId)
{
try
{
var response = await _http.GetAsync($"info/{appId}");
var response = await Http.GetAsync($"info/{appId}");
if (!response.IsSuccessStatusCode)
{
return $"status: {response.StatusCode}".AsErrorResult<AppInfo>();
return await HandleNotSuccess<AppInfo>(response);
}
var result = await response.Content.ReadFromJsonAsync<AppInfo>();
@ -147,11 +153,11 @@ public class PalantirInternalAppClient : IPalantirInternalAppClient
{
try
{
var response = await _http.GetAsync($"master/info");
var response = await Http.GetAsync($"master/info");
if (!response.IsSuccessStatusCode)
{
return $"status: {response.StatusCode}".AsErrorResult<AppInfo>();
return await HandleNotSuccess<AppInfo>(response);
}
var result = await response.Content.ReadFromJsonAsync<AppInfo>();