Compare commits
4 Commits
0.8.2
...
5a34d0a60f
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a34d0a60f | |||
| 1846a00d07 | |||
| 4c04f1cb56 | |||
| df69a2dc6b |
+14
-5
@@ -61,6 +61,10 @@ export default function App() {
|
|||||||
// Cloud-spezifisch: Liste der Studios auf der Instanz (für Erst-Setup-Check).
|
// Cloud-spezifisch: Liste der Studios auf der Instanz (für Erst-Setup-Check).
|
||||||
// null = noch nicht geladen; Array = geladen.
|
// null = noch nicht geladen; Array = geladen.
|
||||||
const [cloudStudios, setCloudStudios] = useState(null);
|
const [cloudStudios, setCloudStudios] = useState(null);
|
||||||
|
// true, wenn listStudios() fehlschlug (Kong/API nicht erreichbar). Wird
|
||||||
|
// genutzt, um NICHT fälschlich den Init-/Registrierungs-Screen zu zeigen —
|
||||||
|
// ein API-Fehler ist nicht dasselbe wie "Instanz hat 0 Studios".
|
||||||
|
const [cloudUnreachable, setCloudUnreachable] = useState(false);
|
||||||
// Passwort-Reset-Flow: Supabase löst beim Klick auf Reset-Link in der Mail
|
// Passwort-Reset-Flow: Supabase löst beim Klick auf Reset-Link in der Mail
|
||||||
// ein PASSWORD_RECOVERY-Event aus → wir zeigen dann das Reset-Formular.
|
// ein PASSWORD_RECOVERY-Event aus → wir zeigen dann das Reset-Formular.
|
||||||
const [passwordRecovery, setPasswordRecovery] = useState(false);
|
const [passwordRecovery, setPasswordRecovery] = useState(false);
|
||||||
@@ -87,13 +91,15 @@ export default function App() {
|
|||||||
console.error("Cloud-Resume load failed:", e);
|
console.error("Cloud-Resume load failed:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Studios der Instanz holen — entscheidet später, ob CloudSetup oder Login zeigt
|
// Studios der Instanz holen — entscheidet später, ob CloudSetup oder Login zeigt.
|
||||||
|
// Bei Fehler (Kong/API down): NICHT [] setzen (das hieße "Instanz leer" →
|
||||||
|
// fälschlich Init-Screen). Stattdessen Unreachable-Flag → Login bleibt.
|
||||||
try {
|
try {
|
||||||
const list = await storage.listStudios?.();
|
const list = await storage.listStudios?.();
|
||||||
if (!cancelled) setCloudStudios(list || []);
|
if (!cancelled) { setCloudStudios(list || []); setCloudUnreachable(false); }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("listStudios failed:", e);
|
console.error("listStudios failed:", e);
|
||||||
if (!cancelled) setCloudStudios([]);
|
if (!cancelled) { setCloudStudios(null); setCloudUnreachable(true); }
|
||||||
}
|
}
|
||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -499,7 +505,10 @@ export default function App() {
|
|||||||
// ohne Login läuft (sonst kommt man bei einem fehlerhaften Setup-Screen nie
|
// ohne Login läuft (sonst kommt man bei einem fehlerhaften Setup-Screen nie
|
||||||
// an ein neueres Build, das den Bug fixt).
|
// an ein neueres Build, das den Bug fixt).
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
if (isCloudBackend && cloudStudios !== null && cloudStudios.length === 0) {
|
// Init-/Registrierungs-Screen NUR wenn der API-Call erfolgreich war UND
|
||||||
|
// wirklich 0 Studios lieferte. Bei !cloudUnreachable ausgeschlossen, dass
|
||||||
|
// ein Kong/API-Fehler (cloudStudios === null) hier fälschlich Init zeigt.
|
||||||
|
if (isCloudBackend && !cloudUnreachable && cloudStudios !== null && cloudStudios.length === 0) {
|
||||||
const cloudUrl = localStorage.getItem("rapport_cloud_url") || "";
|
const cloudUrl = localStorage.getItem("rapport_cloud_url") || "";
|
||||||
return <>
|
return <>
|
||||||
<CloudSetup cloudInit={cloudInit} cloudUrl={cloudUrl} />
|
<CloudSetup cloudInit={cloudInit} cloudUrl={cloudUrl} />
|
||||||
@@ -507,7 +516,7 @@ export default function App() {
|
|||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
return <>
|
return <>
|
||||||
<Login verifyLogin={verifyLogin} settings={data.settings} version="0.8.2" />
|
<Login verifyLogin={verifyLogin} settings={data.settings} version="0.8.2" cloudUnreachable={cloudUnreachable} />
|
||||||
<UpdateNotifier />
|
<UpdateNotifier />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,10 +125,13 @@ export class SupabaseAdapter {
|
|||||||
// Kein Auth nötig (RPC läuft als SECURITY DEFINER).
|
// Kein Auth nötig (RPC läuft als SECURITY DEFINER).
|
||||||
async listStudios() {
|
async listStudios() {
|
||||||
const { data, error } = await this.client.rpc("list_studios");
|
const { data, error } = await this.client.rpc("list_studios");
|
||||||
if (error) {
|
// WICHTIG: Fehler NICHT zu [] verschlucken. Ein leeres Array bedeutet
|
||||||
console.error("listStudios:", error.message);
|
// "Instanz hat 0 Studios" → das Frontend zeigt dann den Registrierungs-/
|
||||||
return [];
|
// Init-Screen. Ein Netzwerk-/API-Fehler (Kong down, Port 8000 nicht
|
||||||
}
|
// erreichbar) ist aber NICHT dasselbe wie "leer" — würden wir hier []
|
||||||
|
// zurückgeben, landet ein eingeloggter User nach Reload fälschlich im
|
||||||
|
// Init-Flow. Daher: werfen und den Caller entscheiden lassen.
|
||||||
|
if (error) throw new Error("listStudios: " + error.message);
|
||||||
return data || [];
|
return data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -16,7 +16,7 @@ function writeAttempts(state) {
|
|||||||
try { sessionStorage.setItem(ATTEMPT_KEY, JSON.stringify(state)); } catch {}
|
try { sessionStorage.setItem(ATTEMPT_KEY, JSON.stringify(state)); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Login({ verifyLogin, settings, version }) {
|
export default function Login({ verifyLogin, settings, version, cloudUnreachable = false }) {
|
||||||
// Backend-Modus aus localStorage (per-Device, ähnlich Dark Mode).
|
// Backend-Modus aus localStorage (per-Device, ähnlich Dark Mode).
|
||||||
// Beim Wechsel wird die App neu geladen, damit der Storage-Adapter neu initialisiert.
|
// Beim Wechsel wird die App neu geladen, damit der Storage-Adapter neu initialisiert.
|
||||||
const [backend, setBackend] = useState(() => localStorage.getItem("rapport_backend") || "local");
|
const [backend, setBackend] = useState(() => localStorage.getItem("rapport_backend") || "local");
|
||||||
@@ -236,6 +236,12 @@ export default function Login({ verifyLogin, settings, version }) {
|
|||||||
<div style={{ width: 32, height: 1.5, background: "#ddd8d0", margin: "16px auto 0" }} />
|
<div style={{ width: 32, height: 1.5, background: "#ddd8d0", margin: "16px auto 0" }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{cloudUnreachable && (
|
||||||
|
<div style={{ marginBottom: 18, padding: "9px 14px", background: "#fff5f0", borderRadius: 8, border: "1px solid #f5c9b0", fontSize: 11, color: "#b5621e", textAlign: "center", lineHeight: 1.5 }}>
|
||||||
|
Server nicht erreichbar. Bitte Verbindung prüfen und neu laden.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{isCloud && studios.length > 1 && (
|
{isCloud && studios.length > 1 && (
|
||||||
<div style={{ marginBottom: 14 }}>
|
<div style={{ marginBottom: 14 }}>
|
||||||
|
|||||||
@@ -11,44 +11,55 @@
|
|||||||
-- Buckets sind PRIVATE — Zugriff nur über signierte URLs (zeitlich begrenzt).
|
-- Buckets sind PRIVATE — Zugriff nur über signierte URLs (zeitlich begrenzt).
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|
||||||
insert into storage.buckets (id, name, public)
|
-- Hinweis: KEINE `public`-Spalte angeben. Beim Postgres-Init existiert sie in
|
||||||
|
-- storage.buckets noch nicht (die fügt die Storage-API erst beim Boot per
|
||||||
|
-- eigener Migration hinzu). Default ist `false` → Buckets sind privat, wie
|
||||||
|
-- gewünscht. Würden wir `public` referenzieren, bräche der Init hier ab und
|
||||||
|
-- ALLE folgenden Migrations (inkl. ensure_profile in 0005) liefen nicht mehr.
|
||||||
|
insert into storage.buckets (id, name)
|
||||||
values
|
values
|
||||||
('receipts', 'receipts', false),
|
('receipts', 'receipts'),
|
||||||
('logos', 'logos', false)
|
('logos', 'logos')
|
||||||
on conflict (id) do nothing;
|
on conflict (id) do nothing;
|
||||||
|
|
||||||
-- ────────────────────────────────────────────────────────────────────────────
|
-- ────────────────────────────────────────────────────────────────────────────
|
||||||
-- RLS-Policies auf storage.objects
|
-- RLS-Policies auf storage.objects
|
||||||
-- ────────────────────────────────────────────────────────────────────────────
|
-- ────────────────────────────────────────────────────────────────────────────
|
||||||
-- Prinzip: erste Pfad-Komponente ist studio_id; Zugriff nur wenn Member.
|
-- Prinzip: erste Pfad-Komponente ist studio_id; Zugriff nur wenn Member.
|
||||||
-- `(storage.foldername(name))[1]` gibt die erste Pfad-Komponente zurück.
|
-- `split_part(name, '/', 1)` gibt die erste Pfad-Komponente zurück.
|
||||||
|
--
|
||||||
|
-- Bewusst NICHT storage.foldername() benutzen: die Storage-API droppt/erstellt
|
||||||
|
-- diese Funktion bei ihren eigenen Boot-Migrations neu. Eine Policy-Abhängigkeit
|
||||||
|
-- darauf würde diesen Drop blockieren ("cannot drop function foldername") und
|
||||||
|
-- die Storage-API in eine Crash-Loop schicken. split_part ist ein eingebautes
|
||||||
|
-- Postgres-Builtin ohne diese Kopplung.
|
||||||
|
|
||||||
create policy "rapport_storage_read"
|
create policy "rapport_storage_read"
|
||||||
on storage.objects for select
|
on storage.objects for select
|
||||||
using (
|
using (
|
||||||
bucket_id in ('receipts','logos')
|
bucket_id in ('receipts','logos')
|
||||||
and is_studio_member( (storage.foldername(name))[1]::uuid )
|
and is_studio_member( split_part(name, '/', 1)::uuid )
|
||||||
);
|
);
|
||||||
|
|
||||||
create policy "rapport_storage_insert"
|
create policy "rapport_storage_insert"
|
||||||
on storage.objects for insert
|
on storage.objects for insert
|
||||||
with check (
|
with check (
|
||||||
bucket_id in ('receipts','logos')
|
bucket_id in ('receipts','logos')
|
||||||
and is_studio_member( (storage.foldername(name))[1]::uuid )
|
and is_studio_member( split_part(name, '/', 1)::uuid )
|
||||||
);
|
);
|
||||||
|
|
||||||
create policy "rapport_storage_update"
|
create policy "rapport_storage_update"
|
||||||
on storage.objects for update
|
on storage.objects for update
|
||||||
using (
|
using (
|
||||||
bucket_id in ('receipts','logos')
|
bucket_id in ('receipts','logos')
|
||||||
and is_studio_member( (storage.foldername(name))[1]::uuid )
|
and is_studio_member( split_part(name, '/', 1)::uuid )
|
||||||
);
|
);
|
||||||
|
|
||||||
create policy "rapport_storage_delete"
|
create policy "rapport_storage_delete"
|
||||||
on storage.objects for delete
|
on storage.objects for delete
|
||||||
using (
|
using (
|
||||||
bucket_id in ('receipts','logos')
|
bucket_id in ('receipts','logos')
|
||||||
and is_studio_member( (storage.foldername(name))[1]::uuid )
|
and is_studio_member( split_part(name, '/', 1)::uuid )
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ────────────────────────────────────────────────────────────────────────────
|
-- ────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user