feat(web): filter by ownerId and surface missing-role 401 state
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
import type { ZitadelAuth } from "@kuns/zitadel-auth";
|
import type { ZitadelAuth } from "@kuns/zitadel-auth";
|
||||||
|
|
||||||
|
/** API call failure carrying the HTTP status (401 = authenticated but not authorized). */
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public status: number,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Access the bootstrap-provided Zitadel auth instance + a small JSON helper for /api calls.
|
// Access the bootstrap-provided Zitadel auth instance + a small JSON helper for /api calls.
|
||||||
// By the time any component mounts, the plugin has gated auth, so `auth` is authenticated.
|
// By the time any component mounts, the plugin has gated auth, so `auth` is authenticated.
|
||||||
// `auth.fetch` auto-attaches the Bearer access token.
|
// `auth.fetch` auto-attaches the Bearer access token.
|
||||||
@@ -16,7 +26,7 @@ export function useAuth() {
|
|||||||
} catch {
|
} catch {
|
||||||
// non-JSON error body
|
// non-JSON error body
|
||||||
}
|
}
|
||||||
throw new Error(message);
|
throw new ApiError(message, res.status);
|
||||||
}
|
}
|
||||||
if (res.status === 204) return undefined as T;
|
if (res.status === 204) return undefined as T;
|
||||||
return (await res.json()) as T;
|
return (await res.json()) as T;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ApiError } from "~/composables/useAuth";
|
||||||
|
|
||||||
interface List {
|
interface List {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
ownerId?: string | null;
|
||||||
}
|
}
|
||||||
interface Task {
|
interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,6 +13,7 @@ interface Task {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
source: string;
|
source: string;
|
||||||
consumed: boolean;
|
consumed: boolean;
|
||||||
|
ownerId?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +26,21 @@ const loadingLists = ref(true);
|
|||||||
const loadingTasks = ref(false);
|
const loadingTasks = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Authenticated but the token lacks the required Zitadel project role → API answers 401.
|
||||||
|
const missingRole = ref(false);
|
||||||
|
|
||||||
|
// Defense-in-depth: the server already scopes every query by the token's sub; additionally
|
||||||
|
// hide anything not owned by the current user. Absent ownerId = legacy (pre-multi-user) row.
|
||||||
|
const mine = (x: { ownerId?: string | null }) => !x.ownerId || x.ownerId === auth.user?.sub;
|
||||||
|
|
||||||
|
function handleApiError(e: unknown) {
|
||||||
|
if (e instanceof ApiError && e.status === 401) {
|
||||||
|
missingRole.value = true;
|
||||||
|
} else {
|
||||||
|
error.value = (e as Error).message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const title = ref("");
|
const title = ref("");
|
||||||
const description = ref("");
|
const description = ref("");
|
||||||
const showNote = ref(false);
|
const showNote = ref(false);
|
||||||
@@ -43,12 +62,12 @@ async function refreshLists() {
|
|||||||
loadingLists.value = true;
|
loadingLists.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
lists.value = await api<List[]>("/lists");
|
lists.value = (await api<List[]>("/lists")).filter(mine);
|
||||||
if (lists.value.length && !lists.value.some((l) => l.id === selectedId.value)) {
|
if (lists.value.length && !lists.value.some((l) => l.id === selectedId.value)) {
|
||||||
await selectList(lists.value[0]!.id);
|
await selectList(lists.value[0]!.id);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = (e as Error).message;
|
handleApiError(e);
|
||||||
} finally {
|
} finally {
|
||||||
loadingLists.value = false;
|
loadingLists.value = false;
|
||||||
}
|
}
|
||||||
@@ -66,9 +85,9 @@ async function refreshTasks() {
|
|||||||
loadingTasks.value = true;
|
loadingTasks.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
tasks.value = await api<Task[]>(`/lists/${selectedId.value}/tasks`);
|
tasks.value = (await api<Task[]>(`/lists/${selectedId.value}/tasks`)).filter(mine);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = (e as Error).message;
|
handleApiError(e);
|
||||||
} finally {
|
} finally {
|
||||||
loadingTasks.value = false;
|
loadingTasks.value = false;
|
||||||
}
|
}
|
||||||
@@ -95,7 +114,7 @@ async function addTask() {
|
|||||||
showNote.value = false;
|
showNote.value = false;
|
||||||
titleInput.value?.focus();
|
titleInput.value?.focus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = (e as Error).message;
|
handleApiError(e);
|
||||||
} finally {
|
} finally {
|
||||||
adding.value = false;
|
adding.value = false;
|
||||||
}
|
}
|
||||||
@@ -135,7 +154,19 @@ onMounted(() => {
|
|||||||
<main class="content">
|
<main class="content">
|
||||||
<p v-if="error" class="error" role="alert">{{ error }}</p>
|
<p v-if="error" class="error" role="alert">{{ error }}</p>
|
||||||
|
|
||||||
<template v-if="loadingLists">
|
<template v-if="missingRole">
|
||||||
|
<div class="empty">
|
||||||
|
<p class="empty-mark">⚠</p>
|
||||||
|
<p class="empty-title">No access yet</p>
|
||||||
|
<p class="muted">
|
||||||
|
You're signed in, but your account is missing the “user” role for ClaudeDo.
|
||||||
|
Ask the admin to grant it in Zitadel, then sign in again.
|
||||||
|
</p>
|
||||||
|
<button class="link" @click="auth.logout()">Sign out</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="loadingLists">
|
||||||
<p class="muted">Loading lists…</p>
|
<p class="muted">Loading lists…</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -172,7 +203,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer v-if="lists.length" class="dock">
|
<footer v-if="lists.length && !missingRole" class="dock">
|
||||||
<button
|
<button
|
||||||
v-if="lists.length > 1"
|
v-if="lists.length > 1"
|
||||||
class="list-pill"
|
class="list-pill"
|
||||||
|
|||||||
Reference in New Issue
Block a user