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";
|
||||
|
||||
/** 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.
|
||||
// By the time any component mounts, the plugin has gated auth, so `auth` is authenticated.
|
||||
// `auth.fetch` auto-attaches the Bearer access token.
|
||||
@@ -16,7 +26,7 @@ export function useAuth() {
|
||||
} catch {
|
||||
// non-JSON error body
|
||||
}
|
||||
throw new Error(message);
|
||||
throw new ApiError(message, res.status);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ApiError } from "~/composables/useAuth";
|
||||
|
||||
interface List {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId?: string | null;
|
||||
}
|
||||
interface Task {
|
||||
id: string;
|
||||
@@ -10,6 +13,7 @@ interface Task {
|
||||
description: string | null;
|
||||
source: string;
|
||||
consumed: boolean;
|
||||
ownerId?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +26,21 @@ const loadingLists = ref(true);
|
||||
const loadingTasks = ref(false);
|
||||
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 description = ref("");
|
||||
const showNote = ref(false);
|
||||
@@ -43,12 +62,12 @@ async function refreshLists() {
|
||||
loadingLists.value = true;
|
||||
error.value = null;
|
||||
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)) {
|
||||
await selectList(lists.value[0]!.id);
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
handleApiError(e);
|
||||
} finally {
|
||||
loadingLists.value = false;
|
||||
}
|
||||
@@ -66,9 +85,9 @@ async function refreshTasks() {
|
||||
loadingTasks.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
tasks.value = await api<Task[]>(`/lists/${selectedId.value}/tasks`);
|
||||
tasks.value = (await api<Task[]>(`/lists/${selectedId.value}/tasks`)).filter(mine);
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
handleApiError(e);
|
||||
} finally {
|
||||
loadingTasks.value = false;
|
||||
}
|
||||
@@ -95,7 +114,7 @@ async function addTask() {
|
||||
showNote.value = false;
|
||||
titleInput.value?.focus();
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
handleApiError(e);
|
||||
} finally {
|
||||
adding.value = false;
|
||||
}
|
||||
@@ -135,7 +154,19 @@ onMounted(() => {
|
||||
<main class="content">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -172,7 +203,7 @@ onMounted(() => {
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<footer v-if="lists.length" class="dock">
|
||||
<footer v-if="lists.length && !missingRole" class="dock">
|
||||
<button
|
||||
v-if="lists.length > 1"
|
||||
class="list-pill"
|
||||
|
||||
Reference in New Issue
Block a user