mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
fix(spaces): call Better Auth at its real URL, not through SvelteKit
All the Space-UI fetches were using relative `/api/auth/...` paths,
which hit the SvelteKit dev server (port 5173) — where those routes
don't exist — and 404'd. The web app has no `/api/auth` proxy; every
Better Auth call must go direct to mana-auth (port 3001 in dev).
Root cause parallels how packages/shared-auth/authService already
works: it builds `${authBaseUrl}/api/auth/...` against
window.__PUBLIC_MANA_AUTH_URL__ or the env fallback.
Fix:
- New helper $lib/data/scope/auth-fetch.ts exposes authFetch(path, init)
that prepends the auth base URL and includes credentials by default.
Same resolution order as shared-auth's authService (injected global,
env, localhost:3001 fallback).
- Updated every organization-endpoint caller to use authFetch:
active-space.svelte.ts (list, get-active-member, set-active)
SpaceSwitcher (list, set-active)
SpaceCreateDialog (create, set-active)
accept-invitation page (get-invitation, accept, reject)
/spaces/members page (list-members, list-invitations, invite-member,
cancel-invitation, remove-member)
- active-space now treats Better Auth's 400 as "no active org" too
(not just 404) so the bootstrap falls through to auto-activation.
Trusted origins already include http://localhost:5173 — no CORS change.
0 errors across 7203 files.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e677c9066
commit
becba67dad
7 changed files with 71 additions and 47 deletions
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
import { SPACE_TYPES, SPACE_TYPE_LABELS, SPACE_TYPE_DESCRIPTIONS } from '@mana/shared-branding';
|
||||
import type { SpaceType } from '@mana/shared-types';
|
||||
import { loadActiveSpace } from '$lib/data/scope';
|
||||
import { loadActiveSpace, authFetch } from '$lib/data/scope';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -74,10 +74,8 @@
|
|||
if (legalEntity.trim()) metadata.legalEntity = legalEntity.trim();
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/organization/create', {
|
||||
const res = await authFetch('/api/auth/organization/create', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
slug: derivedSlug || undefined,
|
||||
|
|
@ -90,10 +88,8 @@
|
|||
}
|
||||
const created = (await res.json()) as { id: string };
|
||||
// Activate the new space so the user lands inside it on reload.
|
||||
await fetch('/api/auth/organization/set-active', {
|
||||
await authFetch('/api/auth/organization/set-active', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ organizationId: created.id }),
|
||||
});
|
||||
await loadActiveSpace({ force: true });
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
*/
|
||||
|
||||
import { onDestroy } from 'svelte';
|
||||
import { getActiveSpace, loadActiveSpace, type ActiveSpace } from '$lib/data/scope';
|
||||
import { getActiveSpace, loadActiveSpace, authFetch, type ActiveSpace } from '$lib/data/scope';
|
||||
import { SPACE_TYPE_LABELS } from '@mana/shared-branding';
|
||||
import { isSpaceType, isSpaceTier } from '@mana/shared-types';
|
||||
import SpaceCreateDialog from './SpaceCreateDialog.svelte';
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
loadingList = true;
|
||||
loadError = null;
|
||||
try {
|
||||
const res = await fetch('/api/auth/organization/list', { credentials: 'include' });
|
||||
const res = await authFetch('/api/auth/organization/list');
|
||||
if (!res.ok) throw new Error(`list failed: ${res.status}`);
|
||||
const raw = (await res.json()) as Array<{
|
||||
id: string;
|
||||
|
|
@ -97,10 +97,8 @@
|
|||
}
|
||||
switching = true;
|
||||
try {
|
||||
const res = await fetch('/api/auth/organization/set-active', {
|
||||
const res = await authFetch('/api/auth/organization/set-active', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ organizationId: id }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`set-active failed: ${res.status}`);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
import type { SpaceType, SpaceTier } from '@mana/shared-types';
|
||||
import { isSpaceType, isSpaceTier } from '@mana/shared-types';
|
||||
import { authFetch } from './auth-fetch';
|
||||
|
||||
export interface ActiveSpace {
|
||||
id: string;
|
||||
|
|
@ -129,8 +130,11 @@ export const __endpoints = {
|
|||
};
|
||||
|
||||
async function fetchActiveMember(): Promise<ActiveSpace | null> {
|
||||
const res = await fetch(__endpoints.active, { credentials: 'include' });
|
||||
if (res.status === 404) return null; // no active org
|
||||
const res = await authFetch(__endpoints.active);
|
||||
// Better Auth returns 400/404 when no organization is active yet —
|
||||
// treat both as "not active" so the bootstrap can fall through to
|
||||
// auto-activation.
|
||||
if (res.status === 404 || res.status === 400) return null;
|
||||
if (!res.ok) throw new Error(`get-active-member failed: ${res.status}`);
|
||||
const raw = (await res.json()) as {
|
||||
role?: string;
|
||||
|
|
@ -141,17 +145,15 @@ async function fetchActiveMember(): Promise<ActiveSpace | null> {
|
|||
}
|
||||
|
||||
async function fetchOrganizations(): Promise<ActiveSpace[]> {
|
||||
const res = await fetch(__endpoints.list, { credentials: 'include' });
|
||||
const res = await authFetch(__endpoints.list);
|
||||
if (!res.ok) throw new Error(`organization/list failed: ${res.status}`);
|
||||
const raws = (await res.json()) as RawOrg[];
|
||||
return raws.map((r) => rawToActiveSpace(r, 'owner'));
|
||||
}
|
||||
|
||||
async function setActiveOnServer(organizationId: string): Promise<void> {
|
||||
const res = await fetch(__endpoints.setActive, {
|
||||
const res = await authFetch(__endpoints.setActive, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ organizationId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`organization/set-active failed: ${res.status}`);
|
||||
|
|
|
|||
41
apps/mana/apps/web/src/lib/data/scope/auth-fetch.ts
Normal file
41
apps/mana/apps/web/src/lib/data/scope/auth-fetch.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Thin wrapper around fetch that prepends the mana-auth base URL and
|
||||
* always includes credentials. The web app does NOT proxy /api/auth
|
||||
* through SvelteKit — every Better Auth request goes directly to
|
||||
* mana-auth at http://localhost:3001 (dev) or https://auth.mana.how
|
||||
* (prod). Using a relative `/api/auth/...` path 404s against the
|
||||
* SvelteKit dev server.
|
||||
*
|
||||
* Resolution order (same as packages/shared-auth/authService):
|
||||
* 1. window.__PUBLIC_MANA_AUTH_URL__ — set by the page loader
|
||||
* 2. process.env.PUBLIC_MANA_AUTH_URL — build-time env
|
||||
* 3. http://localhost:3001 — dev fallback
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export function authBaseUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injected = (window as unknown as { __PUBLIC_MANA_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_AUTH_URL__;
|
||||
if (injected) return injected.replace(/\/$/, '');
|
||||
}
|
||||
return (process.env.PUBLIC_MANA_AUTH_URL || 'http://localhost:3001').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch against the mana-auth server. Always sends cookies (credentials
|
||||
* include) so Better Auth can resolve the session. Path must start with
|
||||
* a slash — e.g. `/api/auth/organization/list`.
|
||||
*/
|
||||
export function authFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
const url = `${authBaseUrl()}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
return fetch(url, {
|
||||
credentials: 'include',
|
||||
...init,
|
||||
headers: {
|
||||
...(init.body ? { 'content-type': 'application/json' } : {}),
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -33,3 +33,5 @@ export {
|
|||
} from './scoped-db';
|
||||
|
||||
export { applyVisibility, isVisibleToCurrentUser, type Visibility } from './visibility';
|
||||
|
||||
export { authFetch, authBaseUrl } from './auth-fetch';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
*/
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { getActiveSpace } from '$lib/data/scope';
|
||||
import { getActiveSpace, authFetch } from '$lib/data/scope';
|
||||
import { SPACE_TYPE_LABELS } from '@mana/shared-branding';
|
||||
|
||||
interface Member {
|
||||
|
|
@ -52,13 +52,11 @@
|
|||
loadError = null;
|
||||
try {
|
||||
const [memRes, invRes] = await Promise.all([
|
||||
fetch(
|
||||
`/api/auth/organization/list-members?organizationId=${encodeURIComponent(activeSpace.id)}`,
|
||||
{ credentials: 'include' }
|
||||
authFetch(
|
||||
`/api/auth/organization/list-members?organizationId=${encodeURIComponent(activeSpace.id)}`
|
||||
),
|
||||
fetch(
|
||||
`/api/auth/organization/list-invitations?organizationId=${encodeURIComponent(activeSpace.id)}`,
|
||||
{ credentials: 'include' }
|
||||
authFetch(
|
||||
`/api/auth/organization/list-invitations?organizationId=${encodeURIComponent(activeSpace.id)}`
|
||||
),
|
||||
]);
|
||||
if (memRes.ok) {
|
||||
|
|
@ -92,10 +90,8 @@
|
|||
inviteError = null;
|
||||
inviteSuccess = null;
|
||||
try {
|
||||
const res = await fetch('/api/auth/organization/invite-member', {
|
||||
const res = await authFetch('/api/auth/organization/invite-member', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: inviteEmail.trim(),
|
||||
role: inviteRole,
|
||||
|
|
@ -117,10 +113,8 @@
|
|||
}
|
||||
|
||||
async function cancelInvitation(id: string) {
|
||||
const res = await fetch('/api/auth/organization/cancel-invitation', {
|
||||
const res = await authFetch('/api/auth/organization/cancel-invitation', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ invitationId: id }),
|
||||
});
|
||||
if (res.ok) await refresh();
|
||||
|
|
@ -129,10 +123,8 @@
|
|||
async function removeMember(userId: string) {
|
||||
if (!activeSpace) return;
|
||||
if (!confirm('Mitglied wirklich entfernen?')) return;
|
||||
const res = await fetch('/api/auth/organization/remove-member', {
|
||||
const res = await authFetch('/api/auth/organization/remove-member', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ memberIdOrEmail: userId, organizationId: activeSpace.id }),
|
||||
});
|
||||
if (res.ok) await refresh();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { SPACE_TYPE_LABELS } from '@mana/shared-branding';
|
||||
import { isSpaceType, type SpaceType } from '@mana/shared-types';
|
||||
import { loadActiveSpace } from '$lib/data/scope';
|
||||
import { loadActiveSpace, authFetch } from '$lib/data/scope';
|
||||
|
||||
interface InvitationPayload {
|
||||
id: string;
|
||||
|
|
@ -52,9 +52,8 @@
|
|||
loading = true;
|
||||
loadError = null;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/auth/organization/get-invitation?id=${encodeURIComponent(invitationId)}`,
|
||||
{ credentials: 'include' }
|
||||
const res = await authFetch(
|
||||
`/api/auth/organization/get-invitation?id=${encodeURIComponent(invitationId)}`
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Einladung nicht gefunden (${res.status})`);
|
||||
|
|
@ -82,20 +81,16 @@
|
|||
submitting = true;
|
||||
actionError = null;
|
||||
try {
|
||||
const res = await fetch('/api/auth/organization/accept-invitation', {
|
||||
const res = await authFetch('/api/auth/organization/accept-invitation', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ invitationId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(await res.text());
|
||||
}
|
||||
// Activate the newly-joined space so the dashboard opens inside it.
|
||||
await fetch('/api/auth/organization/set-active', {
|
||||
await authFetch('/api/auth/organization/set-active', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ organizationId: invitation.organizationId }),
|
||||
});
|
||||
await loadActiveSpace({ force: true });
|
||||
|
|
@ -111,10 +106,8 @@
|
|||
submitting = true;
|
||||
actionError = null;
|
||||
try {
|
||||
const res = await fetch('/api/auth/organization/reject-invitation', {
|
||||
const res = await authFetch('/api/auth/organization/reject-invitation', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ invitationId }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue