From f82255961b2a39a230b796eba3950fbad08f75ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20M=C3=BCller?= Date: Sun, 6 Sep 2026 21:13:12 +0000 Subject: [PATCH] Sign in through the admin app instead of this one (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.plutodev.de/Nachklang/Calendar_Frontend/pulls/20 Co-authored-by: Patrick Müller Co-committed-by: Patrick Müller --- CLAUDE.md | 45 +++++- src/app/components/event/event.component.ts | 80 +++++++--- src/app/models/session.ts | 4 - src/app/models/user.ts | 15 +- src/app/pages/admin/admin.component.html | 42 +++--- src/app/pages/admin/admin.component.ts | 156 +++++++++----------- src/app/services/admin-auth.service.ts | 57 +++++++ src/app/services/api.service.ts | 155 +++++-------------- src/app/services/utils.service.ts | 31 ++-- src/environments/environment.prod.ts | 3 +- src/environments/environment.ts | 7 +- 11 files changed, 316 insertions(+), 279 deletions(-) delete mode 100644 src/app/models/session.ts create mode 100644 src/app/services/admin-auth.service.ts diff --git a/CLAUDE.md b/CLAUDE.md index 83dc009..a19a87e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,35 @@ No linter is configured in this project. Angular 18 single-page app for managing calendar events for the "Nachklang" organization. Uses Angular Material for UI, RxJS for async data, and reactive forms. No NgRx — state lives in component local variables. -**Environments:** -- Dev: `http://localhost:3000` (expects backend running locally) -- Prod: `https://api.nachklang.art` +**Environments** (`src/environments/`): `apiUrl` and `adminAppUrl`. +- Dev: `http://localhost:3000` (expects backend running locally) / `http://localhost:3002` +- Prod: `https://api.nachklang.art` / `https://admin.nachklang.art` + +## Authentication + +**This app has no login form and no accounts of its own.** Since the auth cutover +(`docs/calendar-auth-migration.md` in the API repo) it shares one identity with the tickets, +feedback and admin apps: accounts live in the admin app, and the session is an httpOnly +cookie on `.nachklang.art` that the *API's* host sets. Consequences that cannot be designed +around: + +- `withCredentials: true` is mandatory on every call (`api.service.ts` sets it once). Without + it the browser sends no cookie and the API answers 401. +- This app can never read or verify the session. It calls `GET /admin/me` and believes the + answer; the API's `requireAppAccess('calendar')` is the actual gate. +- 401 and 403 mean different things and must not be collapsed. 401 means "nobody is signed + in" and is the only one worth redirecting to the login page - redirecting on 403 produces a + loop where signing in succeeds and lands straight back on the refusal. See `failure` in + `admin.component.ts`. +- Sign-out ends the session for *all four* apps; there is only one. + +The dev server must be reachable at a port the API trusts. `ng serve` defaults to 4200, which +is in better-auth's dev `localhostOrigins` and in the admin app's +`NEXT_PUBLIC_ALLOWED_REDIRECT_ORIGINS`; another port fails sign-out and the return redirect, +not the sign-in. + +**The public calendar stays anonymous.** `GET /calendar/events/public/json` needs no session +at all, because nachklang.art reads it to show the next upcoming event. **Routing** (`app.routing.ts`): - `/` → `LandingpageComponent` @@ -27,8 +53,9 @@ Angular 18 single-page app for managing calendar events for the "Nachklang" orga - `**` → `NotfoundComponent` **Service layer** (`src/app/services/`): -- `api.service.ts` — all HTTP calls to the backend REST API; session credentials are passed as query params (`sessionId`, `sessionKey`) -- `utils.service.ts` — localStorage helpers for persisting session and user data +- `api.service.ts` — all HTTP calls to the backend REST API; carries the session cookie via `withCredentials`, holds no credential itself +- `admin-auth.service.ts` — where signing in happens: the admin app's login URL (with a `?redirect=` back here) and sign-out +- `utils.service.ts` — caches the signed-in user's display name for unsaved draft rows. Cosmetic only; the server takes the author from the session **Data models** (`src/app/models/`): `Event`, `User`, `Session` @@ -36,4 +63,10 @@ Angular 18 single-page app for managing calendar events for the "Nachklang" orga **Calendar-specific behavior:** Birthday calendar auto-sets recurrence to YEARLY. Events have a `status` field (`DRAFT` / `DELETED`). -**Session lifecycle:** `checkSession()` is called on `AdminComponent` init; on failure it redirects to `/`. +**Session lifecycle:** `AdminComponent` calls `me()` on init. 401 redirects to the admin app's +login carrying this URL as the return target; 403 (or a signed-in account without the +`calendar` permission) shows a refusal with Reload/Sign out; anything else shows "cannot be +reached". None of those three redirect, on purpose. + +**Dead since the cutover:** the unrouted `LoginComponent`. `src/app/models/session.ts` is +gone; there is no session type here any more, because this app never handles one. diff --git a/src/app/components/event/event.component.ts b/src/app/components/event/event.component.ts index da13ed8..5533cc1 100644 --- a/src/app/components/event/event.component.ts +++ b/src/app/components/event/event.component.ts @@ -3,7 +3,9 @@ import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {MatDialog} from '@angular/material/dialog'; import {Event} from '../../models/event'; +import {HttpErrorResponse} from '@angular/common/http'; import {ApiService} from '../../services/api.service'; +import {AdminAuthService} from '../../services/admin-auth.service'; import {EventMovePopupComponent} from "../event-move-popup/event-move-popup.component"; @Component({ @@ -71,20 +73,21 @@ export class EventComponent implements OnInit, OnDestroy { } if(this.event.eventId === undefined) { - this.api.createEvent(this.event).pipe(takeUntil(this.destroy$)).subscribe((res: any) => { - console.log(res); - - if(res.eventId) { - this.event!.eventId = res.eventId; - } else { - this.showCreateError = true; - return; - } + this.api.createEvent(this.event).pipe(takeUntil(this.destroy$)).subscribe({ + next: (res: any) => { + if(res.eventId) { + this.event!.eventId = res.eventId; + } else { + this.showCreateError = true; + } + }, + error: this.handleWriteError('The new event') }); } else { // Update existing event - this.api.updateEvent(this.event).pipe(takeUntil(this.destroy$)).subscribe((res: any) => { - console.log(res); + this.api.updateEvent(this.event).pipe(takeUntil(this.destroy$)).subscribe({ + next: () => {}, + error: this.handleWriteError('Your change') }); } } @@ -180,15 +183,48 @@ export class EventComponent implements OnInit, OnDestroy { let deleteConfirmed = window.confirm(`Are you sure you want to delete "${this.event!.name}"? This action cannot be undone.`); if(deleteConfirmed && this.event) { - this.api.deleteEvent(this.event).pipe(takeUntil(this.destroy$)).subscribe((res: any) => { - console.log(res); - if(res.message) { - this.deleteEvent.next(this.event!.eventId); - } + this.api.deleteEvent(this.event).pipe(takeUntil(this.destroy$)).subscribe({ + next: (res: any) => { + if(res.message) { + this.deleteEvent.next(this.event!.eventId); + } + }, + error: this.handleWriteError('The deletion') }); } } + /** + * What to do when a write fails. + * + * Before the auth cutover none of the writes here had an error callback, so + * a failure was invisible: the row closed, nothing was saved, and the user + * had every reason to think it had been. A 401 is now a routine event - the + * session expires, or is ended from another app or another tab - so silence + * is no longer survivable. + * + * A 401 means the session is gone, and nothing on this page can be saved + * until it comes back, so it goes straight to the login carrying this page + * as the return target. Everything else says what happened and leaves the + * user where they are, with their edits still on screen. + */ + private handleWriteError(action: string): (error: HttpErrorResponse) => void { + return (error: HttpErrorResponse) => { + if (error.status === 401) { + window.alert(`Your session has expired, so ${action} was not saved. Signing you in again.`); + AdminAuthService.goToLogin(); + return; + } + + if (error.status === 403) { + window.alert(`${action} failed: this account no longer has access to the calendar.`); + return; + } + + window.alert(`${action} failed. Please try again. (${error.status || 'no response from the server'})`); + }; + } + triggerMove() { if(this.editActive) { window.alert('Please save your changes before moving the event to a different calendar.'); @@ -204,11 +240,13 @@ export class EventComponent implements OnInit, OnDestroy { movePopup.afterClosed().pipe(takeUntil(this.destroy$)).subscribe(result => { // If popup is dismissed, undefined will be returned if(result) { - this.api.moveEvent(result).pipe(takeUntil(this.destroy$)).subscribe((res: any) => { - console.log(res); - // Uses the same interface as delete as from the calendar table perspective it is the same action - // as a delete - this.deleteEvent.next(result.eventId); + this.api.moveEvent(result).pipe(takeUntil(this.destroy$)).subscribe({ + next: () => { + // Uses the same interface as delete as from the calendar table perspective it is the same action + // as a delete + this.deleteEvent.next(result.eventId); + }, + error: this.handleWriteError('The move') }); } }); diff --git a/src/app/models/session.ts b/src/app/models/session.ts deleted file mode 100644 index df8d9c5..0000000 --- a/src/app/models/session.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Session { - sessionId: number; - sessionKey: string; -} diff --git a/src/app/models/user.ts b/src/app/models/user.ts index 4216773..a00e56b 100644 --- a/src/app/models/user.ts +++ b/src/app/models/user.ts @@ -1,7 +1,14 @@ +/** + * The signed-in account, as returned by GET /admin/me. + * + * Replaces the old calendar-local user: there is no `userId` int and no + * `isActive` flag any more. A disabled account cannot reach this app at all - + * the API answers 403 before the handler runs - so "signed in" and "allowed" + * are the same state here, and `apps` says which apps they may open. + */ export interface User { - userId: number; - fullName: string; - passwordHash: string; + id: string; email: string; - isActive: boolean; + fullName: string; + apps: string[]; } diff --git a/src/app/pages/admin/admin.component.html b/src/app/pages/admin/admin.component.html index f411626..ccef8ab 100644 --- a/src/app/pages/admin/admin.component.html +++ b/src/app/pages/admin/admin.component.html @@ -1,27 +1,25 @@ -
-

Please log in:

- -
- -
- -

-

If you dont' have an account yet, please use the following form to register:

- -
- -
- -
- -
-

Passwords have to use uppercase and lowercase letters, numbers and must have at least 12 characters!

-

Passwords do not match!

- + +
+

Signing you in…

+
-
+
+

This account does not have access to the calendar.

+

Ask an administrator to grant it in the admin app, then reload.

+ + +
+
+

The administration service cannot be reached right now.

+ +
+
Logged in as {{getUserName()}} -  (inactive)     |   diff --git a/src/app/pages/admin/admin.component.ts b/src/app/pages/admin/admin.component.ts index 6b50870..1379455 100644 --- a/src/app/pages/admin/admin.component.ts +++ b/src/app/pages/admin/admin.component.ts @@ -1,10 +1,11 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import {Subject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; +import {HttpErrorResponse} from '@angular/common/http'; import {ApiService} from '../../services/api.service'; import {UtilsService} from '../../services/utils.service'; +import {AdminAuthService} from '../../services/admin-auth.service'; import {Event} from '../../models/event'; -import {Session} from '../../models/session'; import {User} from '../../models/user'; @Component({ @@ -19,15 +20,20 @@ export class AdminComponent implements OnInit, OnDestroy { isLoggedIn: boolean = false; events: Event[] = []; selectedCalendar: string = ''; - password: string = ''; name: string = ''; - email: string = ''; eventFilter: string = 'future'; // Default value for filter eventSorting: string = 'start_asc'; // Default value for sorting - isActive: boolean = false; - registerEmail: string = ''; - registerPassword: string = ''; - registerPasswordConfirm: string = ''; + + /** + * Why the page is not showing events, when it is not. + * + * 'denied' and 'unavailable' are kept apart on purpose. Only a 401 is worth + * sending someone to the login page; bouncing them there for a 403 produces + * a loop where signing in succeeds and lands them straight back here, and + * bouncing them for an unreachable API produces the same loop with no way + * out at all. + */ + failure: 'denied' | 'unavailable' | null = null; constructor( private api: ApiService @@ -35,16 +41,29 @@ export class AdminComponent implements OnInit, OnDestroy { } ngOnInit(): void { - if (UtilsService.getSessionInfoFromLocalStorage().sessionId !== -1) { - this.api.checkSession(UtilsService.getSessionInfoFromLocalStorage()).pipe(takeUntil(this.destroy$)).subscribe((user: User) => { - if(user.userId != null && user.userId !== -1) { - this.isLoggedIn = true; - this.name = user.fullName; - this.isActive = user.isActive; - this.getEvents(); + this.api.me().pipe(takeUntil(this.destroy$)).subscribe({ + next: (user: User) => { + this.isLoggedIn = true; + this.name = user.fullName; + UtilsService.saveNameToLocalStorage(user.fullName); + + // Signed in, but not for this app. The API would answer 403 to + // every events call, so say so once instead of failing per call. + if (!user.apps.includes('calendar')) { + this.failure = 'denied'; + return; } - }); - } + + this.getEvents(); + }, + error: (error: HttpErrorResponse) => { + if (error.status === 401) { + AdminAuthService.goToLogin(); + return; + } + this.failure = error.status === 403 ? 'denied' : 'unavailable'; + } + }); } ngOnDestroy(): void { @@ -59,20 +78,33 @@ export class AdminComponent implements OnInit, OnDestroy { return; } - this.api.getEvents(this.selectedCalendar).pipe(takeUntil(this.destroy$)).subscribe((events: Event[]): void => { - for (let event of events) { - if(event.status !== 'DELETED') { - this.events.push({ - ...event, - startDateTime: new Date(event.startDateTime), - endDateTime: new Date(event.endDateTime), - createdDate: new Date(event.createdDate), - lastModifiedDate: new Date(event.lastModifiedDate) - }); + this.api.getEvents(this.selectedCalendar).pipe(takeUntil(this.destroy$)).subscribe({ + next: (events: Event[]): void => { + for (let event of events) { + if (event.status !== 'DELETED') { + this.events.push({ + ...event, + startDateTime: new Date(event.startDateTime), + endDateTime: new Date(event.endDateTime), + createdDate: new Date(event.createdDate), + lastModifiedDate: new Date(event.lastModifiedDate) + }); + } } + this.filterEvents(); + this.sortEvents(); + }, + // Without this a failed load is indistinguishable from an empty + // calendar - which is exactly what the old bundle looks like against + // the post-cutover API, and how a deploy in progress gets mistaken for + // lost data. + error: (error: HttpErrorResponse): void => { + if (error.status === 401) { + AdminAuthService.goToLogin(); + return; + } + this.failure = error.status === 403 ? 'denied' : 'unavailable'; } - this.filterEvents(); - this.sortEvents(); }); } @@ -156,68 +188,20 @@ export class AdminComponent implements OnInit, OnDestroy { }); } - login(): void { - this.api.login(this.email, this.password).pipe(takeUntil(this.destroy$)).subscribe((session: Session): void => { - if(session.sessionId != null && session.sessionId !== -1) { - UtilsService.saveSessionInfoToLocalStorage(session.sessionId, session.sessionKey); - - // Get user info - this.api.checkSession(UtilsService.getSessionInfoFromLocalStorage()).pipe(takeUntil(this.destroy$)).subscribe((user: User) => { - if(user.userId != null && user.userId !== -1) { - this.isLoggedIn = true; - this.name = user.fullName; - this.isActive = user.isActive; - this.getEvents(); - } else { - alert('Login unsuccessful. Please check if you provided the correct username and password.'); - } - }); - } - }, (error) => { - alert('Login unsuccessful. Reported problem from server: ' + error?.error?.message); - }); - } - - register(): void { - this.api.register(this.registerEmail, this.name, this.registerPassword).pipe(takeUntil(this.destroy$)).subscribe((session: Session): void => { - if(session.sessionId != null && session.sessionId !== -1) { - UtilsService.saveSessionInfoToLocalStorage(session.sessionId, session.sessionKey); - this.isLoggedIn = true; - this.getEvents(); - alert('An email was sent to your Nachklang address. Please click the link in the email to activate your account. You can\'t use this application before the activation.'); - } else { - alert('Registration unsuccessful. Please contact Patrick.'); - } - }, (error) => { - alert('Registration unsuccessful. Reported problem from server: ' + error?.error?.message); - }); + /** + * There is no sign-in form here any more, and no account creation: accounts + * exist only by invitation from the admin app. Both are one redirect. + */ + signIn(): void { + AdminAuthService.goToLogin(); } logout(): void { - UtilsService.clearSessionInfo(); - this.isLoggedIn = false; + UtilsService.clearName(); + void AdminAuthService.signOut(); } - checkUserInactive(): boolean { - return !this.isActive; - } - - checkPasswordsMatch(): boolean { - return this.registerPassword === this.registerPasswordConfirm; - } - - checkPasswordPolicy(): boolean { - let isLongEnough = this.registerPassword.length >= 12; - - var lowercaseRegex = /[a-z]/g - let hasLowercase = lowercaseRegex.test(this.registerPassword); - - var uppercaseRegex = /[A-Z]/g - let hasUppercase = uppercaseRegex.test(this.registerPassword); - - var numberRegex = /[0-9]/g - let hasNumbers = numberRegex.test(this.registerPassword); - - return isLongEnough && hasLowercase && hasUppercase && hasNumbers; + reload(): void { + window.location.reload(); } } diff --git a/src/app/services/admin-auth.service.ts b/src/app/services/admin-auth.service.ts new file mode 100644 index 0000000..95a400f --- /dev/null +++ b/src/app/services/admin-auth.service.ts @@ -0,0 +1,57 @@ +import {Injectable} from '@angular/core'; +import {environment} from '../../environments/environment'; + +/** + * Everything to do with *where* signing in happens. This app holds no + * credential of its own: the session is an httpOnly cookie on + * .nachklang.art that the API sets, so the calendar can neither read it nor + * mint one. It can only send the browser somewhere that can. + * + * Mirrors the same file in nachklang-tickets and nachklang-feedback - the three + * apps are one product with one sign-in. + */ +@Injectable({providedIn: 'root'}) +export class AdminAuthService { + + /** + * The admin app's login URL, carrying where to come back to. The admin app + * validates that target against its own allowlist of origins, so a + * `?redirect=` it does not recognise is dropped rather than followed. + */ + static loginUrl(returnTo?: string): string { + const target = returnTo ?? (window.location.pathname + window.location.search); + const absolute = new URL(target, window.location.origin).toString(); + return `${environment.adminAppUrl}/login?redirect=${encodeURIComponent(absolute)}`; + } + + static goToLogin(returnTo?: string): void { + AdminAuthService.leave(AdminAuthService.loginUrl(returnTo)); + } + + /** + * Ends the session for every app, not just this one - there is only one + * session. The user lands back on the admin login with this page as the + * return target, so signing out by accident costs one click to undo. + */ + static async signOut(): Promise { + // Captured before the request: afterwards the page may already be gone. + const target = AdminAuthService.loginUrl(); + try { + await fetch(`${environment.apiUrl}/admin/auth/sign-out`, { + method: 'POST', + credentials: 'include', + headers: {'Content-Type': 'application/json'}, + body: '{}' + }); + } catch { + // A network failure still leaves the browser better off at the login + // page than on a signed-in-looking shell it can no longer refresh. + } + AdminAuthService.leave(target); + } + + /** Assigning through a variable rather than a literal keeps this one place. */ + private static leave(url: string): void { + window.location.href = url; + } +} diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 2649cf6..3302f9e 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -1,152 +1,71 @@ import {Injectable} from '@angular/core'; -import {HttpClient, HttpParams} from '@angular/common/http'; +import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {Event} from '../models/event'; -import {UtilsService} from './utils.service'; -import { environment } from './../../environments/environment'; -import {Session} from '../models/session'; +import {environment} from './../../environments/environment'; import {User} from '../models/user'; +/** + * Every call here is cross-origin to the API and carries the shared session + * cookie, which is what `withCredentials` means and why it is not optional: + * without it the browser sends no cookie and the API answers 401. + * + * Before the auth cutover each call appended a sessionId/sessionKey pair to the + * query string instead - credentials in URLs, and so in access logs, browser + * history and Referer headers (DEFERRED_SECURITY.md item 1). There is no + * credential left in this file at all. + */ @Injectable({ providedIn: 'root' }) export class ApiService { apiUrl = environment.apiUrl + '/calendar/events/'; - userApiUrl = environment.apiUrl + '/calendar/users/'; + + // Sending the cookie is the whole authentication story; nothing else here + // says who the caller is. + private readonly withSession = {withCredentials: true}; constructor( private http: HttpClient ) { } - register(email: string, fullName: string, password: string): Observable { - try { - let registerEvent: any = { - "email": email, - "fullName": fullName, - "password": password - }; - - return this.http.post(this.userApiUrl + 'register', registerEvent); - } catch (exception) { - console.log('Error fetching events from API'); - } - return new Observable(); - } - - login(email: string, password: string): Observable { - try { - let loginEvent: any = { - "email": email, - "password": password - }; - - return this.http.post(this.userApiUrl + 'login', loginEvent); - } catch (exception) { - console.log('Error fetching events from API'); - } - return new Observable(); - } - - checkSession(session: Session): Observable { - try { - return this.http.post(this.userApiUrl + 'checkSessionValid', session); - } catch (exception) { - console.log('Error fetching events from API'); - } - return new Observable(); + /** + * Who is signed in, across all four apps. 401 means "nobody" and 403 means + * "signed in, but this account may not use the calendar" - the caller has to + * tell those apart, because only the first one is worth a trip to the login + * page. + */ + me(): Observable { + return this.http.get(environment.apiUrl + '/admin/me', this.withSession); } getEvents(calendar: string): Observable { - try { - let session = UtilsService.getSessionInfoFromLocalStorage(); - - let params = new HttpParams(); - params = params.append('sessionId', session.sessionId); - params = params.append('sessionKey', session.sessionKey); - return this.http.get((this.apiUrl + calendar + '/json'), {params}); - } catch (exception) { - console.log('Error fetching events from API'); - } - return new Observable(); + return this.http.get(this.apiUrl + calendar + '/json', this.withSession); } updateEvent(event: Event): Observable { - try { - let session = UtilsService.getSessionInfoFromLocalStorage(); - - let params = new HttpParams(); - params = params.append('sessionId', session.sessionId); - params = params.append('sessionKey', session.sessionKey); - - let updateEvent: any = event; - - return this.http.put(this.apiUrl + updateEvent.eventId, updateEvent, {params}); - } catch (exception) { - console.log('Error updating event'); - } - return new Observable(); + return this.http.put(this.apiUrl + event.eventId, event, this.withSession); } createEvent(event: Event): Observable { - try { - let session = UtilsService.getSessionInfoFromLocalStorage(); - - let params = new HttpParams(); - params = params.append('sessionId', session.sessionId); - params = params.append('sessionKey', session.sessionKey); - - // Automatically set birthdays to recurring - if(event.calendarId === 5) { - event.repeatFrequency = 'YEARLY'; - } - - let createEvent: any = event; - - return this.http.post(this.apiUrl, createEvent, {params}); - } catch (exception) { - console.log('Error creating event'); + // Automatically set birthdays to recurring + if (event.calendarId === 5) { + event.repeatFrequency = 'YEARLY'; } - return new Observable(); + + return this.http.post(this.apiUrl, event, this.withSession); } deleteEvent(event: Event): Observable { - try { - let session = UtilsService.getSessionInfoFromLocalStorage(); - - let params = new HttpParams(); - params = params.append('sessionId', session.sessionId); - params = params.append('sessionKey', session.sessionKey); - - let deleteEvent: any = event; - - return this.http.delete(this.apiUrl + deleteEvent.eventId, { - headers: { - 'Content-Type': 'application/json' - }, - body: deleteEvent, - params - }); - } catch (exception) { - console.log('Error deleting event'); - } - return new Observable(); + return this.http.delete(this.apiUrl + event.eventId, { + headers: {'Content-Type': 'application/json'}, + body: event, + withCredentials: true + }); } moveEvent(event: Event): Observable { - try { - let session = UtilsService.getSessionInfoFromLocalStorage(); - - let params = new HttpParams(); - params = params.append('sessionId', session.sessionId); - params = params.append('sessionKey', session.sessionKey); - - let updateEvent: any = event; - - return this.http.put(this.apiUrl + 'move/' + updateEvent.eventId, updateEvent, {params}); - } catch (exception) { - console.log('Error updating event'); - } - return new Observable(); + return this.http.put(this.apiUrl + 'move/' + event.eventId, event, this.withSession); } } diff --git a/src/app/services/utils.service.ts b/src/app/services/utils.service.ts index a6a01be..e70f309 100644 --- a/src/app/services/utils.service.ts +++ b/src/app/services/utils.service.ts @@ -1,5 +1,4 @@ import {Injectable} from '@angular/core'; -import {Session} from '../models/session'; @Injectable({ providedIn: 'root' @@ -9,24 +8,24 @@ export class UtilsService { constructor() { } + /** + * The signed-in user's name, cached so a freshly added draft row can show an + * author before it has been saved. Purely cosmetic: the server records the + * author from the session, never from anything the client sends. + * + * The session itself is an httpOnly cookie and is deliberately not here - + * this app used to keep a sessionId/sessionKey pair in localStorage and + * append it to every URL, which is what the auth cutover removed. + */ + static saveNameToLocalStorage(name: string): void { + localStorage.setItem('name', name); + } + static getNameFromLocalStorage(): string { return localStorage.getItem('name') ?? ''; } - static saveSessionInfoToLocalStorage(sessionId: number, sessionKey: string): void { - localStorage.setItem('sessionId', sessionId.toString()); - localStorage.setItem('sessionKey', sessionKey); - } - - static getSessionInfoFromLocalStorage(): Session { - return { - sessionId: parseInt((localStorage.getItem('sessionId') ?? '-1'), 10), - sessionKey: localStorage.getItem('sessionKey') ?? '' - } - } - - static clearSessionInfo(): void { - localStorage.setItem('sessionId', '-1'); - localStorage.setItem('sessionKey', ''); + static clearName(): void { + localStorage.removeItem('name'); } } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 5831ec9..d06532c 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,4 +1,5 @@ export const environment = { production: true, - apiUrl: 'https://api.nachklang.art' + apiUrl: 'https://api.nachklang.art', + adminAppUrl: 'https://admin.nachklang.art' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index a40ab69..fdad968 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -4,7 +4,12 @@ export const environment = { production: false, - apiUrl: 'http://localhost:3000' + apiUrl: 'http://localhost:3000', + // Where signing in happens. This app has no login form of its own since the + // auth cutover: accounts live in the admin app, and the session is a cookie + // on .nachklang.art that all four apps share. In dev that is one host, so + // the cookie is scoped to localhost and the port does not matter. + adminAppUrl: 'http://localhost:3002' }; /*