ddd test 2

This commit is contained in:
2026-03-23 00:23:56 +01:00
parent fe9c1dd1c0
commit 69944d050a
129 changed files with 2486 additions and 813 deletions

View File

@@ -0,0 +1,74 @@
# Auth Domain
Handles user authentication (login and registration) against the configured server endpoint. Provides the login, register, and user-bar UI components.
## Module map
```
auth/
├── application/
│ └── auth.service.ts HTTP login/register against the active server endpoint
├── feature/
│ ├── login/ Login form component
│ ├── register/ Registration form component
│ └── user-bar/ Displays current user or login/register links
└── index.ts Barrel exports
```
## Service overview
`AuthService` resolves the API base URL from `ServerDirectoryFacade`, then makes POST requests for login and registration. It does not hold session state itself; after a successful login the calling component stores `currentUserId` in localStorage and dispatches `UsersActions.setCurrentUser` into the NgRx store.
```mermaid
graph TD
Login[LoginComponent]
Register[RegisterComponent]
UserBar[UserBarComponent]
Auth[AuthService]
SD[ServerDirectoryFacade]
Store[NgRx Store]
Login --> Auth
Register --> Auth
UserBar --> Store
Auth --> SD
Login --> Store
click Auth "application/auth.service.ts" "HTTP login/register"
click Login "feature/login/" "Login form"
click Register "feature/register/" "Registration form"
click UserBar "feature/user-bar/" "Current user display"
click SD "../server-directory/application/server-directory.facade.ts" "Resolves API URL"
```
## Login flow
```mermaid
sequenceDiagram
participant User
participant Login as LoginComponent
participant Auth as AuthService
participant SD as ServerDirectoryFacade
participant API as Server API
participant Store as NgRx Store
User->>Login: Submit credentials
Login->>Auth: login(username, password)
Auth->>SD: getApiBaseUrl()
SD-->>Auth: https://server/api
Auth->>API: POST /api/auth/login
API-->>Auth: { userId, displayName }
Auth-->>Login: success
Login->>Store: UsersActions.setCurrentUser
Login->>Login: localStorage.setItem(currentUserId)
```
## Registration flow
Registration follows the same pattern but posts to `/api/auth/register` with an additional `displayName` field. On success the user is treated as logged in and the same store dispatch happens.
## User bar
`UserBarComponent` reads the current user from the NgRx store. When logged in it shows the user's display name; when not logged in it shows links to the login and register views.

View File

@@ -0,0 +1,76 @@
<div class="h-full grid place-items-center bg-background">
<div class="w-[360px] bg-card border border-border rounded-xl p-6 shadow-sm">
<div class="flex items-center gap-2 mb-4">
<ng-icon
name="lucideLogIn"
class="w-5 h-5 text-primary"
/>
<h1 class="text-lg font-semibold text-foreground">Login</h1>
</div>
<div class="space-y-3">
<div>
<label
for="login-username"
class="block text-xs text-muted-foreground mb-1"
>Username</label
>
<input
[(ngModel)]="username"
type="text"
id="login-username"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
/>
</div>
<div>
<label
for="login-password"
class="block text-xs text-muted-foreground mb-1"
>Password</label
>
<input
[(ngModel)]="password"
type="password"
id="login-password"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
/>
</div>
<div>
<label
for="login-server"
class="block text-xs text-muted-foreground mb-1"
>Server App</label
>
<select
[(ngModel)]="serverId"
id="login-server"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
>
@for (s of servers(); track s.id) {
<option [value]="s.id">{{ s.name }}</option>
}
</select>
</div>
@if (error()) {
<p class="text-xs text-destructive">{{ error() }}</p>
}
<button
(click)="submit()"
type="button"
class="w-full px-3 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90"
>
Login
</button>
<div class="text-xs text-muted-foreground text-center mt-2">
No account?
<button
type="button"
(click)="goRegister()"
class="text-primary hover:underline"
>
Register
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,100 @@
/* eslint-disable max-statements-per-line */
import {
Component,
inject,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideLogIn } from '@ng-icons/lucide';
import { AuthService } from '../../application/auth.service';
import { ServerDirectoryFacade } from '../../../server-directory';
import { UsersActions } from '../../../../store/users/users.actions';
import { User } from '../../../../shared-kernel';
import { STORAGE_KEY_CURRENT_USER_ID } from '../../../../core/constants';
@Component({
selector: 'app-login',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgIcon
],
viewProviders: [provideIcons({ lucideLogIn })],
templateUrl: './login.component.html'
})
/**
* Login form allowing existing users to authenticate against a selected server.
*/
export class LoginComponent {
serversSvc = inject(ServerDirectoryFacade);
servers = this.serversSvc.servers;
username = '';
password = '';
serverId: string | undefined = this.serversSvc.activeServer()?.id;
error = signal<string | null>(null);
private auth = inject(AuthService);
private store = inject(Store);
private route = inject(ActivatedRoute);
private router = inject(Router);
/** TrackBy function for server list rendering. */
trackById(_index: number, item: { id: string }) { return item.id; }
/** Validate and submit the login form, then navigate to search on success. */
submit() {
this.error.set(null);
const sid = this.serverId || this.serversSvc.activeServer()?.id;
this.auth.login({ username: this.username.trim(),
password: this.password,
serverId: sid }).subscribe({
next: (resp) => {
if (sid)
this.serversSvc.setActiveServer(sid);
const user: User = {
id: resp.id,
oderId: resp.id,
username: resp.username,
displayName: resp.displayName,
status: 'online',
role: 'member',
joinedAt: Date.now()
};
try { localStorage.setItem(STORAGE_KEY_CURRENT_USER_ID, resp.id); } catch {}
this.store.dispatch(UsersActions.setCurrentUser({ user }));
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')?.trim();
if (returnUrl?.startsWith('/')) {
this.router.navigateByUrl(returnUrl);
return;
}
this.router.navigate(['/search']);
},
error: (err) => {
this.error.set(err?.error?.error || 'Login failed');
}
});
}
/** Navigate to the registration page. */
goRegister() {
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')?.trim();
this.router.navigate(['/register'], {
queryParams: returnUrl ? { returnUrl } : undefined
});
}
}

View File

@@ -0,0 +1,89 @@
<div class="h-full grid place-items-center bg-background">
<div class="w-[380px] bg-card border border-border rounded-xl p-6 shadow-sm">
<div class="flex items-center gap-2 mb-4">
<ng-icon
name="lucideUserPlus"
class="w-5 h-5 text-primary"
/>
<h1 class="text-lg font-semibold text-foreground">Register</h1>
</div>
<div class="space-y-3">
<div>
<label
for="register-username"
class="block text-xs text-muted-foreground mb-1"
>Username</label
>
<input
[(ngModel)]="username"
type="text"
id="register-username"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
/>
</div>
<div>
<label
for="register-display-name"
class="block text-xs text-muted-foreground mb-1"
>Display Name</label
>
<input
[(ngModel)]="displayName"
type="text"
id="register-display-name"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
/>
</div>
<div>
<label
for="register-password"
class="block text-xs text-muted-foreground mb-1"
>Password</label
>
<input
[(ngModel)]="password"
type="password"
id="register-password"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
/>
</div>
<div>
<label
for="register-server"
class="block text-xs text-muted-foreground mb-1"
>Server App</label
>
<select
[(ngModel)]="serverId"
id="register-server"
class="w-full px-3 py-2 rounded border border-border bg-secondary text-foreground"
>
@for (s of servers(); track s.id) {
<option [value]="s.id">{{ s.name }}</option>
}
</select>
</div>
@if (error()) {
<p class="text-xs text-destructive">{{ error() }}</p>
}
<button
(click)="submit()"
type="button"
class="w-full px-3 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90"
>
Create Account
</button>
<div class="text-xs text-muted-foreground text-center mt-2">
Have an account?
<button
type="button"
(click)="goLogin()"
class="text-primary hover:underline"
>
Login
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,102 @@
/* eslint-disable max-statements-per-line */
import {
Component,
inject,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideUserPlus } from '@ng-icons/lucide';
import { AuthService } from '../../application/auth.service';
import { ServerDirectoryFacade } from '../../../server-directory';
import { UsersActions } from '../../../../store/users/users.actions';
import { User } from '../../../../shared-kernel';
import { STORAGE_KEY_CURRENT_USER_ID } from '../../../../core/constants';
@Component({
selector: 'app-register',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgIcon
],
viewProviders: [provideIcons({ lucideUserPlus })],
templateUrl: './register.component.html'
})
/**
* Registration form allowing new users to create an account on a selected server.
*/
export class RegisterComponent {
serversSvc = inject(ServerDirectoryFacade);
servers = this.serversSvc.servers;
username = '';
displayName = '';
password = '';
serverId: string | undefined = this.serversSvc.activeServer()?.id;
error = signal<string | null>(null);
private auth = inject(AuthService);
private store = inject(Store);
private route = inject(ActivatedRoute);
private router = inject(Router);
/** TrackBy function for server list rendering. */
trackById(_index: number, item: { id: string }) { return item.id; }
/** Validate and submit the registration form, then navigate to search on success. */
submit() {
this.error.set(null);
const sid = this.serverId || this.serversSvc.activeServer()?.id;
this.auth.register({ username: this.username.trim(),
password: this.password,
displayName: this.displayName.trim(),
serverId: sid }).subscribe({
next: (resp) => {
if (sid)
this.serversSvc.setActiveServer(sid);
const user: User = {
id: resp.id,
oderId: resp.id,
username: resp.username,
displayName: resp.displayName,
status: 'online',
role: 'member',
joinedAt: Date.now()
};
try { localStorage.setItem(STORAGE_KEY_CURRENT_USER_ID, resp.id); } catch {}
this.store.dispatch(UsersActions.setCurrentUser({ user }));
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')?.trim();
if (returnUrl?.startsWith('/')) {
this.router.navigateByUrl(returnUrl);
return;
}
this.router.navigate(['/search']);
},
error: (err) => {
this.error.set(err?.error?.error || 'Registration failed');
}
});
}
/** Navigate to the login page. */
goLogin() {
const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')?.trim();
this.router.navigate(['/login'], {
queryParams: returnUrl ? { returnUrl } : undefined
});
}
}

View File

@@ -0,0 +1,35 @@
<div class="h-10 border-b border-border bg-card flex items-center justify-end px-3 gap-2">
<div class="flex-1"></div>
@if (user()) {
<div class="flex items-center gap-2 text-sm">
<ng-icon
name="lucideUser"
class="w-4 h-4 text-muted-foreground"
/>
<span class="text-foreground">{{ user()?.displayName }}</span>
</div>
} @else {
<button
type="button"
(click)="goto('login')"
class="px-2 py-1 text-sm rounded bg-secondary hover:bg-secondary/80 flex items-center gap-1"
>
<ng-icon
name="lucideLogIn"
class="w-4 h-4"
/>
Login
</button>
<button
type="button"
(click)="goto('register')"
class="px-2 py-1 text-sm rounded bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-1"
>
<ng-icon
name="lucideUserPlus"
class="w-4 h-4"
/>
Register
</button>
}
</div>

View File

@@ -0,0 +1,37 @@
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideUser,
lucideLogIn,
lucideUserPlus
} from '@ng-icons/lucide';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
@Component({
selector: 'app-user-bar',
standalone: true,
imports: [CommonModule, NgIcon],
viewProviders: [
provideIcons({ lucideUser,
lucideLogIn,
lucideUserPlus })
],
templateUrl: './user-bar.component.html'
})
/**
* Compact user status bar showing the current user with login/register navigation links.
*/
export class UserBarComponent {
store = inject(Store);
user = this.store.selectSignal(selectCurrentUser);
private router = inject(Router);
/** Navigate to the specified authentication page. */
goto(path: 'login' | 'register') {
this.router.navigate([`/${path}`]);
}
}