Close the adversarial review round: auth hardening, data fidelity against the platform context, backend grants and docker coherence
This commit is contained in:
+49
-13
@@ -34,7 +34,21 @@ export interface RouteParams {
|
||||
// Path parsing & generation
|
||||
// ============================================================================
|
||||
|
||||
function parsePath(pathname: string): Route {
|
||||
/**
|
||||
* Decode one path segment. Ids on the ActivityPods backend are absolute IRIs, so
|
||||
* they are percent-encoded by `routeToPath`; a malformed escape (a raw IRI that
|
||||
* was never encoded) is returned verbatim rather than throwing — the captured
|
||||
* text IS the id in that case.
|
||||
*/
|
||||
function decodeSegment(segment: string): string {
|
||||
try {
|
||||
return decodeURIComponent(segment);
|
||||
} catch {
|
||||
return segment;
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePath(pathname: string): Route {
|
||||
const path = pathname.replace(/\/+$/, '') || '/';
|
||||
|
||||
if (path === '/' || path === '') return { page: 'welcome' };
|
||||
@@ -49,10 +63,14 @@ function parsePath(pathname: string): Route {
|
||||
if (path === '/profile/share') return { page: 'share-profile' };
|
||||
if (path === '/profile/connect') return { page: 'connect' };
|
||||
|
||||
// /events/:id/...
|
||||
const eventMatch = path.match(/^\/events\/([^/]+)(?:\/(.+))?$/);
|
||||
// /events/:id/... — the id is an absolute IRI, encoded by `routeToPath` so it
|
||||
// holds no `/`; the sub-route is matched from the tail, and a raw (unencoded)
|
||||
// IRI still parses as the whole remainder. An ENCODED id may itself contain a
|
||||
// `/edit` segment without being read as a sub-route, which is why encoding is
|
||||
// mandatory on every navigation.
|
||||
const eventMatch = path.match(/^\/events\/(.+?)(?:\/(edit|invite|participants|meeting-points))?$/);
|
||||
if (eventMatch) {
|
||||
const eventId = eventMatch[1]!;
|
||||
const eventId = decodeSegment(eventMatch[1]!);
|
||||
const sub = eventMatch[2];
|
||||
if (!sub) return { page: 'event-detail', eventId };
|
||||
if (sub === 'edit') return { page: 'update-event', eventId };
|
||||
@@ -61,10 +79,10 @@ function parsePath(pathname: string): Route {
|
||||
if (sub === 'meeting-points') return { page: 'meeting-points', eventId };
|
||||
}
|
||||
|
||||
// /users/:id
|
||||
const userMatch = path.match(/^\/users\/([^/]+)$/);
|
||||
// /users/:id — an absolute actor URI, encoded for the same reason.
|
||||
const userMatch = path.match(/^\/users\/(.+)$/);
|
||||
if (userMatch) {
|
||||
return { page: 'user-profile', userId: userMatch[1]! };
|
||||
return { page: 'user-profile', userId: decodeSegment(userMatch[1]!) };
|
||||
}
|
||||
|
||||
return { page: 'welcome' };
|
||||
@@ -77,21 +95,39 @@ export function routeToPath(route: Route): string {
|
||||
case 'events': return '/events';
|
||||
case 'create-event': return '/events/new';
|
||||
case 'login': return '/login';
|
||||
case 'event-detail': return `/events/${route.eventId}`;
|
||||
case 'update-event': return `/events/${route.eventId}/edit`;
|
||||
case 'invite': return `/events/${route.eventId}/invite`;
|
||||
case 'participants': return `/events/${route.eventId}/participants`;
|
||||
case 'meeting-points': return `/events/${route.eventId}/meeting-points`;
|
||||
// Ids are absolute IRIs: they MUST be percent-encoded into a single path
|
||||
// segment, or the `/` and `:` inside them are read as route structure and
|
||||
// navigation falls through to the welcome page. `parsePath` decodes them.
|
||||
case 'event-detail': return `/events/${encodeURIComponent(route.eventId)}`;
|
||||
case 'update-event': return `/events/${encodeURIComponent(route.eventId)}/edit`;
|
||||
case 'invite': return `/events/${encodeURIComponent(route.eventId)}/invite`;
|
||||
case 'participants': return `/events/${encodeURIComponent(route.eventId)}/participants`;
|
||||
case 'meeting-points': return `/events/${encodeURIComponent(route.eventId)}/meeting-points`;
|
||||
case 'profile': return '/profile';
|
||||
case 'edit-profile': return '/profile/edit';
|
||||
case 'friends': return '/profile/friends';
|
||||
case 'share-profile': return '/profile/share';
|
||||
case 'connect': return '/profile/connect';
|
||||
case 'user-profile': return `/users/${route.userId}`;
|
||||
case 'user-profile': return `/users/${encodeURIComponent(route.userId)}`;
|
||||
case 'settings': return '/settings';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a path to an event sub-route, encoding the id. Screens navigate by path
|
||||
* rather than by `Route`, so this is the one place that keeps a raw IRI out of
|
||||
* the URL structure.
|
||||
*/
|
||||
export function eventPath(eventId: string, sub?: 'edit' | 'invite' | 'participants' | 'meeting-points'): string {
|
||||
const base = `/events/${encodeURIComponent(eventId)}`;
|
||||
return sub ? `${base}/${sub}` : base;
|
||||
}
|
||||
|
||||
/** Build a path to a user profile, encoding the actor URI. */
|
||||
export function userPath(userId: string): string {
|
||||
return `/users/${encodeURIComponent(userId)}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Router context
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user