diff --git a/api/admin.go b/api/admin.go index d6fd081..1834bf4 100644 --- a/api/admin.go +++ b/api/admin.go @@ -15,7 +15,6 @@ import ( const adminCookieName = "pacmacro_admin" type AdminRegistrationRequest struct { - Name string `json:"name"` Pass string `json:"pass"` } @@ -35,7 +34,6 @@ type Admin struct { password string cookieValue string - name string registered bool stateMutex sync.RWMutex socketMutex sync.Mutex @@ -46,7 +44,6 @@ func (a *Admin) Init(players *Players, sockets *Sockets, password string, games a.sockets = sockets a.password = password a.cookieValue = base64.RawURLEncoding.EncodeToString([]byte(password)) - a.name = "" a.registered = false a.connections = make(map[adminSocketConnection]struct{}) if len(games) > 0 { @@ -160,7 +157,6 @@ func (a *Admin) ServeReset(w http.ResponseWriter, r *http.Request) { } // POST /api/admin/register -// JSON "name": administrator display name // JSON "pass": administrator password from ADMIN_PASSWORD func (a *Admin) ServeRegister(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -173,18 +169,12 @@ func (a *Admin) ServeRegister(w http.ResponseWriter, r *http.Request) { return } - name := strings.TrimSpace(request.Name) - if name == "" { - writeJSONError(w, http.StatusBadRequest) - return - } if !credentialsMatch(request.Pass, a.password) { writeJSONError(w, http.StatusUnauthorized) return } a.stateMutex.Lock() - a.name = name a.registered = true a.stateMutex.Unlock() @@ -198,7 +188,7 @@ func (a *Admin) ServeRegister(w http.ResponseWriter, r *http.Request) { }) w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusNoContent) - fmt.Printf("Admin\tServeRegister (/api/admin/register):\tRegistered administrator %q.\n", name) + fmt.Print("Admin\tServeRegister (/api/admin/register):\tRegistered administrator.\n") } // POST /api/admin/update/ diff --git a/api/admin_test.go b/api/admin_test.go index b94e285..e6bc556 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -7,8 +7,6 @@ import ( "testing" ) -const testUser = "test" - type recordingAdminConnection struct { messages [][]byte closed bool @@ -41,7 +39,7 @@ func registerTestAdmin(t *testing.T, admin *Admin, password string) *http.Cookie t, http.MethodPost, "/api/admin/register", - AdminRegistrationRequest{Name: testUser, Pass: password}, + AdminRegistrationRequest{Pass: password}, ) request.Header.Set("X-Forwarded-Proto", "https") response := httptest.NewRecorder() @@ -87,7 +85,7 @@ func TestAdminRegistrationRejectsWrongPasswordAndAllowsSessionRenewal(t *testing t, http.MethodPost, "/api/admin/register", - AdminRegistrationRequest{Name: testUser, Pass: "wrong"}, + AdminRegistrationRequest{Pass: "wrong"}, ) wrongResponse := httptest.NewRecorder() admin.ServeHTTP(wrongResponse, wrongRequest) @@ -100,7 +98,7 @@ func TestAdminRegistrationRejectsWrongPasswordAndAllowsSessionRenewal(t *testing t, http.MethodPost, "/api/admin/register", - AdminRegistrationRequest{Name: "Grace", Pass: "top-secret"}, + AdminRegistrationRequest{Pass: "top-secret"}, ) renewalResponse := httptest.NewRecorder() admin.ServeHTTP(renewalResponse, renewalRequest) diff --git a/frontend/README.md b/frontend/README.md index 4ffcc7b..2994e81 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -58,8 +58,8 @@ Player sessions use only the ID returned by registration; the browser stores tha ## Administrator authentication -Select **Admin** on the registration page and enter the password configured as `ADMIN_PASSWORD` on the Go backend. -The backend verifies it once and returns a session-scoped HttpOnly cookie. +Open `/admin` and enter the password configured as `ADMIN_PASSWORD` on the Go backend. +The backend verifies it and returns a session-scoped HttpOnly cookie. The Angular application cannot read the cookie; the browser sends it automatically to `/api/admin` requests. The admin control panel opens `/admin/map` in a separate tab. That page connects to the authenticated `/api/admin/map/ws` endpoint and receives the normal live game feed without creating a player or sending coordinates. If the session cookie has expired, register as admin again before reopening the map. diff --git a/frontend/src/app/core/api.service.spec.ts b/frontend/src/app/core/api.service.spec.ts index ee01e75..c6a6c63 100644 --- a/frontend/src/app/core/api.service.spec.ts +++ b/frontend/src/app/core/api.service.spec.ts @@ -40,11 +40,11 @@ describe('ApiService', () => { }); it('registers the admin separately and requests cookie credentials', () => { - api.registerAdmin('Test2', 'top-secret').subscribe(); + api.registerAdmin('top-secret').subscribe(); const request = http.expectOne('/api/admin/register'); expect(request.request.method).toBe('POST'); expect(request.request.withCredentials).toBe(true); - expect(request.request.body).toEqual({ name: 'Test2', pass: 'top-secret' }); + expect(request.request.body).toEqual({ pass: 'top-secret' }); expect(request.request.detectContentTypeHeader()).toBe('application/json'); request.flush(null, { status: 204, statusText: 'No Content' }); }); diff --git a/frontend/src/app/core/api.service.ts b/frontend/src/app/core/api.service.ts index c268ab8..d7980a7 100644 --- a/frontend/src/app/core/api.service.ts +++ b/frontend/src/app/core/api.service.ts @@ -24,12 +24,8 @@ export class ApiService { }); } - registerAdmin(name: string, password: string): Observable { - return this.http.post( - '/api/admin/register', - { name, pass: password }, - { withCredentials: true }, - ); + registerAdmin(password: string): Observable { + return this.http.post('/api/admin/register', { pass: password }, { withCredentials: true }); } getPlayers(): Observable { diff --git a/frontend/src/app/core/game.models.ts b/frontend/src/app/core/game.models.ts index c0ac733..54a801c 100644 --- a/frontend/src/app/core/game.models.ts +++ b/frontend/src/app/core/game.models.ts @@ -27,11 +27,6 @@ export enum PlayerType { FlagLeader = 7, } -export enum RegistrationKind { - Player = 'player', - Admin = 'admin', -} - export enum PlayerStatus { Gone = 0, Disconnected = 1, diff --git a/frontend/src/app/pages/_shared-auth.scss b/frontend/src/app/pages/_shared-auth.scss index 914ed33..0bd5c11 100644 --- a/frontend/src/app/pages/_shared-auth.scss +++ b/frontend/src/app/pages/_shared-auth.scss @@ -32,4 +32,4 @@ .form-status { min-height: 1.25rem; color: #9c0754; -} +} \ No newline at end of file diff --git a/frontend/src/app/pages/admin-page/admin-page.component.html b/frontend/src/app/pages/admin-page/admin-page.component.html index 4c3ffc6..6a408c2 100644 --- a/frontend/src/app/pages/admin-page/admin-page.component.html +++ b/frontend/src/app/pages/admin-page/admin-page.component.html @@ -1,66 +1,92 @@ -
-
-

PacMacro control panel

-

Admin

-
+@if (!authenticated()) { + -

Websocket: {{ connectionStatus() }}

-

{{ status() }}

+
+
+

Admin sign in

-
- Open Map - - - -
+
+ + -
- @for (player of players(); track player.id) { -
-
- {{ player.name }} - {{ player.id }} - - {{ isConnected(player) ? 'Connected' : 'Offline' }} - -
+ +

{{ status() }}

+ +
+} @else { +
+
+

PacMacro control panel

+

Admin

+
-
- @for (playerType of playerTypes; track playerType.value) { - - - } -
- - } @empty { -

Players will appear here when they register.

- } - -
+

Websocket: {{ connectionStatus() }}

+

{{ status() }}

+ +
+ Open Map + + + +
+ +
+ @for (player of players(); track player.id) { +
+
+ {{ player.name }} + {{ player.id }} + + {{ isConnected(player) ? 'Connected' : 'Offline' }} + +
+ +
+ @for (playerType of playerTypes; track playerType.value) { + + + } +
+
+ } @empty { +

Players will appear here when they register.

+ } +
+
+} diff --git a/frontend/src/app/pages/admin-page/admin-page.component.scss b/frontend/src/app/pages/admin-page/admin-page.component.scss index 7a12aee..5af9fb4 100644 --- a/frontend/src/app/pages/admin-page/admin-page.component.scss +++ b/frontend/src/app/pages/admin-page/admin-page.component.scss @@ -1,5 +1,10 @@ +@use '../shared-auth'; + :host { display: block; +} + +:host.admin-authenticated { min-height: 100dvh; background: #f5f5f7; color: #111; diff --git a/frontend/src/app/pages/admin-page/admin-page.component.spec.ts b/frontend/src/app/pages/admin-page/admin-page.component.spec.ts index c1b2ec7..51c3536 100644 --- a/frontend/src/app/pages/admin-page/admin-page.component.spec.ts +++ b/frontend/src/app/pages/admin-page/admin-page.component.spec.ts @@ -1,4 +1,5 @@ -import { signal } from '@angular/core'; +import { signal, WritableSignal } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { of, throwError } from 'rxjs'; @@ -53,6 +54,7 @@ describe('AdminPageComponent', () => { const api = { getPlayers: vi.fn(() => of(refreshedPlayers)), updatePlayer: vi.fn(() => of(undefined)), + registerAdmin: vi.fn(() => of(void 0)), updateAdminFlag: vi.fn(() => of(undefined)), resetGame: vi.fn(() => of(undefined)), }; @@ -66,6 +68,8 @@ describe('AdminPageComponent', () => { api.getPlayers.mockReturnValue(of(refreshedPlayers)); api.updatePlayer.mockReset(); api.updatePlayer.mockReturnValue(of(undefined)); + api.registerAdmin.mockClear(); + api.registerAdmin.mockReturnValue(of(void 0)); api.updateAdminFlag.mockReset(); api.updateAdminFlag.mockReturnValue(of(undefined)); api.resetGame.mockReset(); @@ -81,11 +85,70 @@ describe('AdminPageComponent', () => { .compileComponents(); fixture = TestBed.createComponent(AdminPageComponent); + }); + + it('shows the sign-in form instead of the dashboard before authentication', () => { fixture.detectChanges(); - await fixture.whenStable(); + + const page = fixture.nativeElement as HTMLElement; + expect(page.querySelector('.auth-card')).not.toBeNull(); + expect(page.querySelector('.player-list')).toBeNull(); + expect(adminSocket.connect).not.toHaveBeenCalled(); + }); + + it('starts the admin player feed after rendering when already signed in', () => { + harness().authenticated.set(true); + fixture.detectChanges(); + + expect(adminSocket.connect).toHaveBeenCalledOnce(); + }); + + it('requires the administrator password before calling the API', async () => { + harness().loginModel.set({ password: '' }); + fixture.detectChanges(); + + await harness().submit(submitEvent()); + fixture.detectChanges(); + + expect(api.registerAdmin).not.toHaveBeenCalled(); + const page = fixture.nativeElement as HTMLElement; + expect(page.querySelector('.form-status')?.textContent).toContain('administrator password'); + }); + + it('keeps the sign-in form when the administrator password is incorrect', async () => { + api.registerAdmin.mockReturnValue(throwError(() => new HttpErrorResponse({ status: 401 }))); + harness().loginModel.set({ password: 'wrong' }); + fixture.detectChanges(); + + await harness().submit(submitEvent()); + fixture.detectChanges(); + + expect(api.registerAdmin).toHaveBeenCalledWith('wrong'); + const page = fixture.nativeElement as HTMLElement; + expect(page.querySelector('.auth-card')).not.toBeNull(); + expect(page.querySelector('.player-list')).toBeNull(); + expect(page.querySelector('.form-status')?.textContent).toContain('incorrect'); + expect(adminSocket.connect).not.toHaveBeenCalled(); + }); + + it('reveals the dashboard and starts the player feed after a successful sign-in', async () => { + harness().loginModel.set({ password: 'secret' }); + fixture.detectChanges(); + + await harness().submit(submitEvent()); + fixture.detectChanges(); + + expect(api.registerAdmin).toHaveBeenCalledWith('secret'); + const page = fixture.nativeElement as HTMLElement; + expect(page.querySelector('.auth-card')).toBeNull(); + expect(page.querySelector('.player-list')).not.toBeNull(); + expect(adminSocket.connect).toHaveBeenCalledOnce(); }); it('starts the admin player feed and preserves the new-tab map link', () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const page = fixture.nativeElement as HTMLElement; const link = page.querySelector('.admin-map-link'); @@ -96,6 +159,7 @@ describe('AdminPageComponent', () => { }); it('disables Admin mutations until the socket snapshot is ready', () => { + harness().authenticated.set(true); adminSocket.isReady.set(false); fixture.detectChanges(); @@ -118,6 +182,9 @@ describe('AdminPageComponent', () => { }); it('renders the seven ordered type radios in independent groups', () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const page = fixture.nativeElement as HTMLElement; const cards = page.querySelectorAll('.player-card'); const expectedLabels = [ @@ -151,6 +218,9 @@ describe('AdminPageComponent', () => { }); it('shows Edible as Ghost-selected and disables offline player radios', () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const page = fixture.nativeElement as HTMLElement; const cards = page.querySelectorAll('.player-card'); const connectedRadios = cards[0].querySelectorAll('input[type="radio"]'); @@ -165,6 +235,9 @@ describe('AdminPageComponent', () => { }); it('applies a connected player radio selection immediately', async () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const page = fixture.nativeElement as HTMLElement; const antipac = page.querySelector('#type-AAAA-2'); @@ -179,6 +252,9 @@ describe('AdminPageComponent', () => { }); it('demotes the existing Pacman when another player is selected as Pacman', async () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const page = fixture.nativeElement as HTMLElement; page.querySelector('#type-CCCC-1')?.click(); await fixture.whenStable(); @@ -194,6 +270,7 @@ describe('AdminPageComponent', () => { }); it('demotes only the previous holder when assigning a specialized leader role', async () => { + harness().authenticated.set(true); adminSocket.players.set([ { id: 'AAAA', @@ -224,6 +301,8 @@ describe('AdminPageComponent', () => { }); it('restores server-backed selection when an immediate update fails', async () => { + harness().authenticated.set(true); + fixture.detectChanges(); api.updatePlayer.mockReturnValueOnce(throwError(() => new Error('update failed'))); const page = fixture.nativeElement as HTMLElement; page.querySelector('#type-AAAA-3')?.click(); @@ -238,6 +317,9 @@ describe('AdminPageComponent', () => { }); it('replaces the Ghost mutation with a shared Flag Found toggle', async () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const button = findButton('Flag Found'); expect(button?.getAttribute('aria-pressed')).toBe('false'); expect(button?.classList.contains('button-secondary')).toBe(true); @@ -253,6 +335,7 @@ describe('AdminPageComponent', () => { }); it('rolls back a failed Admin flag update', async () => { + harness().authenticated.set(true); adminSocket.isFlagFound.set(true); api.updateAdminFlag.mockReturnValueOnce(throwError(() => new Error('update failed'))); fixture.detectChanges(); @@ -271,7 +354,10 @@ describe('AdminPageComponent', () => { }); it('resets connected and offline non-Leaders to Ghost while preserving Leaders', async () => { + harness().authenticated.set(true); adminSocket.isFlagFound.set(true); + fixture.detectChanges(); + const button = findButton('Reset Game'); button?.click(); await fixture.whenStable(); @@ -291,6 +377,9 @@ describe('AdminPageComponent', () => { }); it('can manually refresh the current player list', async () => { + harness().authenticated.set(true); + fixture.detectChanges(); + const page = fixture.nativeElement as HTMLElement; findButton('Refresh Players')?.click(); await fixture.whenStable(); @@ -306,4 +395,18 @@ describe('AdminPageComponent', () => { (button) => button.textContent?.trim() === label, ); } + + function harness(): AdminPageHarness { + return fixture.componentInstance as unknown as AdminPageHarness; + } }); + +interface AdminPageHarness { + authenticated: WritableSignal; + loginModel: WritableSignal<{ password: string }>; + submit(event: SubmitEvent): Promise; +} + +function submitEvent(): SubmitEvent { + return { preventDefault: vi.fn() } as unknown as SubmitEvent; +} diff --git a/frontend/src/app/pages/admin-page/admin-page.component.ts b/frontend/src/app/pages/admin-page/admin-page.component.ts index fb52b83..1803476 100644 --- a/frontend/src/app/pages/admin-page/admin-page.component.ts +++ b/frontend/src/app/pages/admin-page/admin-page.component.ts @@ -6,7 +6,9 @@ import { inject, signal, } from '@angular/core'; +import { form, FormField, required, submit as submitForm } from '@angular/forms/signals'; import { firstValueFrom } from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; import { AdminSocketService } from '../../core/sockets/admin-socket.service'; import { ApiService } from '../../core/api.service'; @@ -17,13 +19,22 @@ import { PlayerStatus, PlayerType, } from '../../core/game.models'; +import { BrandHeaderComponent } from '../../shared/brand-header/brand-header.component'; + +interface AdminLoginModel { + password: string; +} @Component({ selector: 'pac-admin-page', + imports: [FormField, BrandHeaderComponent], templateUrl: './admin-page.component.html', styleUrl: './admin-page.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, providers: [AdminSocketService], + host: { + '[class.admin-authenticated]': 'authenticated()', + }, }) export class AdminPageComponent { private readonly api = inject(ApiService); @@ -33,6 +44,7 @@ export class AdminPageComponent { protected readonly isFlagFound = this.adminSocket.isFlagFound; protected readonly socketReady = this.adminSocket.isReady; protected readonly connectionStatus = this.adminSocket.status; + protected readonly authenticated = signal(false); protected readonly status = signal(''); protected readonly loadingPlayers = signal(false); protected readonly bulkUpdating = signal(false); @@ -48,8 +60,46 @@ export class AdminPageComponent { this.savingPlayerIds().size > 0, ); + protected readonly loginModel = signal({ password: '' }); + + protected readonly loginForm = form(this.loginModel, (login) => { + required(login.password, { message: 'Enter the administrator password.' }); + }); + constructor() { - afterNextRender(() => this.adminSocket.connect()); + afterNextRender(() => { + if (this.authenticated()) { + this.adminSocket.connect(); + } + }); + } + + protected async submit(event: SubmitEvent): Promise { + event.preventDefault(); + await submitForm(this.loginForm, { + action: async () => this.login(), + onInvalid: () => { + const firstError = this.loginForm().errorSummary()[0]; + this.status.set(firstError?.message ?? 'Enter the administrator password.'); + }, + }); + } + + private async login(): Promise { + const { password } = this.loginForm().value(); + this.status.set('Signing in...'); + + try { + await firstValueFrom(this.api.registerAdmin(password)); + this.authenticated.set(true); + this.adminSocket.connect(); + } catch (error) { + if (error instanceof HttpErrorResponse && error.status === 401) { + this.status.set('The administrator password is incorrect.'); + } else { + this.status.set('Could not sign in. Check the password and the API connection.'); + } + } } protected isConnected(player: Player): boolean { diff --git a/frontend/src/app/pages/register-page/register-page.component.html b/frontend/src/app/pages/register-page/register-page.component.html index effe789..bc7c39e 100644 --- a/frontend/src/app/pages/register-page/register-page.component.html +++ b/frontend/src/app/pages/register-page/register-page.component.html @@ -2,26 +2,12 @@
-

Register

- - +

Register as Player

+
- @if (!registrationForm.adminPassword().hidden()) { - - - } - diff --git a/frontend/src/app/pages/register-page/register-page.component.spec.ts b/frontend/src/app/pages/register-page/register-page.component.spec.ts index c8efaea..e560a89 100644 --- a/frontend/src/app/pages/register-page/register-page.component.spec.ts +++ b/frontend/src/app/pages/register-page/register-page.component.spec.ts @@ -5,7 +5,6 @@ import { of } from 'rxjs'; import { ApiService } from '../../core/api.service'; import { CredentialsService } from '../../core/credentials.service'; -import { RegistrationKind } from '../../core/game.models'; import { RegisterPageComponent } from './register-page.component'; describe('RegisterPageComponent', () => { @@ -28,30 +27,11 @@ describe('RegisterPageComponent', () => { }); }); - it('registers an admin separately without saving player credentials', async () => { - const component = TestBed.createComponent(RegisterPageComponent) - .componentInstance as unknown as RegisterPageHarness; - component.registrationModel.set({ - registrationKind: RegistrationKind.Admin, - name: 'Test', - adminPassword: 'top-secret', - }); - - await component.submit(submitEvent()); - - expect(api.registerAdmin).toHaveBeenCalledWith('Test', 'top-secret'); - expect(api.registerPlayer).not.toHaveBeenCalled(); - expect(credentials.save).not.toHaveBeenCalled(); - expect(router.navigateByUrl).toHaveBeenCalledWith('/admin'); - }); - it('keeps player registration on the player endpoint', async () => { const component = TestBed.createComponent(RegisterPageComponent) .componentInstance as unknown as RegisterPageHarness; component.registrationModel.set({ - registrationKind: RegistrationKind.Player, name: 'Test2', - adminPassword: '', }); await component.submit(submitEvent()); @@ -62,38 +42,22 @@ describe('RegisterPageComponent', () => { expect(router.navigateByUrl).toHaveBeenCalledWith('/'); }); - it('offers only Player and Admin registration kinds', async () => { - const fixture = TestBed.createComponent(RegisterPageComponent); - fixture.detectChanges(); - await fixture.whenStable(); - const options = [ - ...(fixture.nativeElement as HTMLElement).querySelectorAll('option'), - ].map((option) => option.textContent?.trim()); - - expect(options).toEqual(['Player', 'Admin']); - }); - it('uses Signal Forms validation before calling the API', async () => { const component = TestBed.createComponent(RegisterPageComponent) .componentInstance as unknown as RegisterPageHarness; component.registrationModel.set({ - registrationKind: RegistrationKind.Admin, - name: 'Test', - adminPassword: '', + name: ' ', }); await component.submit(submitEvent()); - expect(api.registerAdmin).not.toHaveBeenCalled(); expect(api.registerPlayer).not.toHaveBeenCalled(); }); }); interface RegisterPageHarness { registrationModel: WritableSignal<{ - registrationKind: RegistrationKind; name: string; - adminPassword: string; }>; submit(event: SubmitEvent): Promise; } diff --git a/frontend/src/app/pages/register-page/register-page.component.ts b/frontend/src/app/pages/register-page/register-page.component.ts index 12c8c18..c04ca41 100644 --- a/frontend/src/app/pages/register-page/register-page.component.ts +++ b/frontend/src/app/pages/register-page/register-page.component.ts @@ -1,12 +1,9 @@ -import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; import { form, FormField, - hidden, maxLength, pattern, - required, submit as submitForm, } from '@angular/forms/signals'; import { Router } from '@angular/router'; @@ -14,13 +11,10 @@ import { firstValueFrom } from 'rxjs'; import { ApiService } from '../../core/api.service'; import { CredentialsService } from '../../core/credentials.service'; -import { RegistrationKind } from '../../core/game.models'; import { BrandHeaderComponent } from '../../shared/brand-header/brand-header.component'; interface RegistrationModel { - registrationKind: RegistrationKind; name: string; - adminPassword: string; } @Component({ @@ -36,29 +30,16 @@ export class RegisterPageComponent { private readonly router = inject(Router); protected readonly registrationModel = signal({ - registrationKind: RegistrationKind.Player, name: '', - adminPassword: '', }); protected readonly registrationForm = form(this.registrationModel, (registration) => { - required(registration.registrationKind); - required(registration.name, { message: 'Enter your name.' }); pattern(registration.name, /\S/, { message: 'Enter your name.' }); maxLength(registration.name, 80, { message: 'Your name must be 80 characters or fewer.' }); - hidden(registration.adminPassword, { - when: ({ valueOf }) => valueOf(registration.registrationKind) !== RegistrationKind.Admin, - }); - required(registration.adminPassword, { - message: 'Enter the administrator password.', - when: ({ valueOf }) => valueOf(registration.registrationKind) === RegistrationKind.Admin, - }); }); protected readonly status = signal(''); - protected readonly registrationKinds = RegistrationKind; - protected async submit(event: SubmitEvent): Promise { event.preventDefault(); await submitForm(this.registrationForm, { @@ -71,17 +52,10 @@ export class RegisterPageComponent { } private async register(): Promise { - const { registrationKind, name, adminPassword } = this.registrationForm().value(); - const trimmedName = name.trim(); + const trimmedName = this.registrationForm().value().name.trim(); this.status.set('Registering...'); try { - if (registrationKind === RegistrationKind.Admin) { - await firstValueFrom(this.api.registerAdmin(trimmedName, adminPassword)); - await this.router.navigateByUrl('/admin'); - return; - } - const response = await firstValueFrom(this.api.registerPlayer(trimmedName)); const id = response.id.trim(); if (!id) { @@ -90,11 +64,7 @@ export class RegisterPageComponent { this.credentials.save({ id }); await this.router.navigateByUrl('/'); } catch (error) { - if (error instanceof HttpErrorResponse && error.status === 401) { - this.status.set('The administrator password is incorrect.'); - } else { - this.status.set('Registration failed. Check your details and the API connection.'); - } + this.status.set('Registration failed. Check your details and the API connection.'); } } }