AI Implementation feature(823): Landing Page and Login/Register Modal (#11)

This commit was merged in pull request #11.
This commit is contained in:
2026-07-21 18:34:45 +00:00
parent 8476e4a59f
commit 685a8bca84
51 changed files with 2354 additions and 226 deletions
+101 -1
View File
@@ -1,5 +1,5 @@
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import {
readStoredSession,
@@ -20,6 +20,38 @@ interface RefreshResponse {
user: CurrentUser;
}
export interface LoginSuccess {
ok: true;
accessToken: string;
expiresIn: number;
user: CurrentUser;
}
export interface LoginFailure {
ok: false;
status: number;
code: string;
message: string;
}
export type LoginResult = LoginSuccess | LoginFailure;
export interface RegisterSuccess {
ok: true;
accessToken: string;
expiresIn: number;
user: CurrentUser;
}
export interface RegisterFailure {
ok: false;
status: number;
code: string;
message: string;
}
export type RegisterResult = RegisterSuccess | RegisterFailure;
@Injectable({ providedIn: 'root' })
export class AuthService {
private http: HttpClient;
@@ -36,6 +68,64 @@ export class AuthService {
readonly currentUser = computed(() => this.user());
readonly isAuthenticated = computed(() => !!this.user());
async ensureCsrf(): Promise<void> {
try {
await firstValueFrom(
this.http.get('/api/v1/auth/csrf', { withCredentials: true }),
);
} catch {
// middleware will still mint the cookie on the next response
}
}
async login(creds: { username: string; password: string }): Promise<LoginResult> {
await this.ensureCsrf();
try {
const res = await firstValueFrom(
this.http.post<{ accessToken: string; expiresIn: number; user: CurrentUser }>(
'/api/v1/auth/login',
creds,
{ withCredentials: true, observe: 'response' },
),
);
const success: LoginSuccess = {
ok: true,
accessToken: res.body!.accessToken,
expiresIn: res.body!.expiresIn,
user: res.body!.user,
};
return success;
} catch (e) {
return mapAuthError(e as HttpErrorResponse);
}
}
async register(creds: {
username: string;
password: string;
passwordConfirm: string;
}): Promise<RegisterResult> {
await this.ensureCsrf();
try {
const res = await firstValueFrom(
this.http.post<{ accessToken: string; expiresIn: number; user: CurrentUser }>(
'/api/v1/auth/register',
creds,
{ withCredentials: true, observe: 'response' },
),
);
const success: RegisterSuccess = {
ok: true,
accessToken: res.body!.accessToken,
expiresIn: res.body!.expiresIn,
user: res.body!.user,
};
return success;
} catch (e) {
return mapAuthError(e as HttpErrorResponse) as RegisterFailure;
}
}
setSession(token: string, user: CurrentUser): void {
this.accessToken.set(token);
this.user.set(user);
@@ -104,4 +194,14 @@ export class AuthService {
return false;
}
}
}
function mapAuthError(err: HttpErrorResponse): LoginFailure {
const body = err.error as { code?: string; message?: string } | null;
return {
ok: false,
status: err.status ?? 0,
code: body?.code ?? 'INTERNAL',
message: body?.message ?? err.message ?? 'Authentication failed',
};
}