diff --git a/src/angular/frontend/angular.json b/src/angular/frontend/angular.json index dc507b1..dc8b2a7 100644 --- a/src/angular/frontend/angular.json +++ b/src/angular/frontend/angular.json @@ -73,6 +73,9 @@ "buildTarget": "frontend:build:development" } }, + "options": { + "proxyConfig": "src/proxy.conf.json" + }, "defaultConfiguration": "development" }, "extract-i18n": { diff --git a/src/angular/frontend/src/app/app.config.ts b/src/angular/frontend/src/app/app.config.ts index a1e7d6f..f103d00 100644 --- a/src/angular/frontend/src/app/app.config.ts +++ b/src/angular/frontend/src/app/app.config.ts @@ -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' + } + ] }; diff --git a/src/angular/frontend/src/app/clients/gandalf/gandalf-client.ts b/src/angular/frontend/src/app/clients/gandalf/gandalf-client.ts new file mode 100644 index 0000000..e3213db --- /dev/null +++ b/src/angular/frontend/src/app/clients/gandalf/gandalf-client.ts @@ -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('GandalfClientBaseAPIToken'); + +export abstract class GandalfClient { + + protected readonly basePath = inject(GandalfClientBaseAPI); + protected readonly http = inject(HttpClient); + + protected handleRequest(request: Observable, onError?: (error: any) => void): Observable { + return request.pipe(catchError((x, y) => this.handleError(x, y, onError))); + } + + private handleError(err: any, _: Observable, onError?: (error: any) => void): Observable { + onError?.(err); + return throwError(() => err); + } +} diff --git a/src/angular/frontend/src/app/clients/gandalf/mithrandir/auth.service.ts b/src/angular/frontend/src/app/clients/gandalf/mithrandir/auth.service.ts new file mode 100644 index 0000000..ae687d1 --- /dev/null +++ b/src/angular/frontend/src/app/clients/gandalf/mithrandir/auth.service.ts @@ -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 { + return this.handleRequest(this.http.get(this.base + '/check')); + } + + login(command: ValidateCredentialsCommand, onError?: (error: any) => void): Observable { + return this.handleRequest(this.http.post(this.base + '/login', command), onError); + } + + loginAsync(command: ValidateCredentialsCommand, onError?: (error: any) => void): Promise { + return lastValueFrom(this.login(command, onError)); + } + +} diff --git a/src/angular/frontend/src/app/components/login/login.component.html b/src/angular/frontend/src/app/components/login/login.component.html index 0e71ecf..ec064e0 100644 --- a/src/angular/frontend/src/app/components/login/login.component.html +++ b/src/angular/frontend/src/app/components/login/login.component.html @@ -1,8 +1,15 @@ -
- - +@if (errors().length > 0) { +
+ @for (error of errors(); track error) { +
{{ error }}
+ } +
+} + + +
- +
diff --git a/src/angular/frontend/src/app/components/login/login.component.ts b/src/angular/frontend/src/app/components/login/login.component.ts index 27052c0..25cd2c8 100644 --- a/src/angular/frontend/src/app/components/login/login.component.ts +++ b/src/angular/frontend/src/app/components/login/login.component.ts @@ -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([]); + + protected async onSubmit($event: Event): Promise { $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'); } } diff --git a/src/angular/frontend/src/proxy.conf.json b/src/angular/frontend/src/proxy.conf.json new file mode 100644 index 0000000..cc88b1c --- /dev/null +++ b/src/angular/frontend/src/proxy.conf.json @@ -0,0 +1,7 @@ +{ + "/api": { + "target": "http://localhost:5055", + "secure": false, + "logLevel": "debug" + } +} diff --git a/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Controllers/AuthController.cs b/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Controllers/AuthController.cs index 9148b6e..5f37591 100644 --- a/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Controllers/AuthController.cs +++ b/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Controllers/AuthController.cs @@ -88,7 +88,7 @@ public class AuthController : ControllerBase { Secure = true, HttpOnly = true, - SameSite = SameSiteMode.Lax, + SameSite = SameSiteMode.None, Expires = tokenRequestResponse.RefreshTokenExpiresAt.AddSeconds(-10) }); diff --git a/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Program.cs b/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Program.cs index 98bf490..bdb0bf4 100644 --- a/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Program.cs +++ b/src/dotnet/Suspectus.Gandalf.Mithrandir.Api/Program.cs @@ -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(); diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Handlers/Security/GetTokensCommandHandler.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Handlers/Security/GetTokensCommandHandler.cs index 60b6c26..9670ea5 100644 --- a/src/dotnet/Suspectus.Gandalf.Palantir.Api/Handlers/Security/GetTokensCommandHandler.cs +++ b/src/dotnet/Suspectus.Gandalf.Palantir.Api/Handlers/Security/GetTokensCommandHandler.cs @@ -70,7 +70,6 @@ public class TokenRequestCommandHandler(TimeProvider timeProvider, IHashids hash { public async Task> 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(); } - validateCredentialsResponse = validateCredentialsResult.GetValue(); + var validateCredentialsResponse = validateCredentialsResult.GetValue(); if (!validateCredentialsResponse.IsValid) { diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs index 8b32f4d..b556619 100644 --- a/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/dotnet/Suspectus.Gandalf.Palantir.Client/Extensions/ServiceCollectionExtensions.cs @@ -6,14 +6,14 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddPalantirClient(this IServiceCollection services) { - services.AddHttpClient(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/auth/"); }) + services.AddHttpClient(opt => { opt.BaseAddress = new Uri("http://localhost:5035/api/internal/auth/"); }) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); - services.AddHttpClient(opt => { opt.BaseAddress = new Uri("https://localhost:7269/api/internal/app/"); }) + services.AddHttpClient(opt => { opt.BaseAddress = new Uri("http://localhost:5035/api/internal/app/"); }) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler //TODO: Remove this in production { ServerCertificateCustomValidationCallback = diff --git a/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs b/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs index dec10f9..902a3ed 100644 --- a/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs +++ b/src/dotnet/Suspectus.Gandalf.Palantir.Client/PalantirClient.cs @@ -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> HandleNotSuccess(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync(); + return $"status: {response.StatusCode}{(string.IsNullOrWhiteSpace(body) ? "" : $" message: {body}")}".AsErrorResult(); + } +} + 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> 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(); + return await HandleNotSuccess(response); } var result = await response.Content.ReadFromJsonAsync(); @@ -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(); + return await HandleNotSuccess(response); } var result = await response.Content.ReadFromJsonAsync(); @@ -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> 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(); + return await HandleNotSuccess(response); } var result = await response.Content.ReadFromJsonAsync(); @@ -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(); + return await HandleNotSuccess(response); } var result = await response.Content.ReadFromJsonAsync();