diff --git a/admin/assets/css/main.css b/admin/assets/css/main.css index 2256283..51a816e 100644 --- a/admin/assets/css/main.css +++ b/admin/assets/css/main.css @@ -339,6 +339,12 @@ main { color: var(--accent-text); } +.badge.group { + background: var(--ok-dim); + color: var(--ok); + border: 1px solid transparent; +} + .card-actions { display: flex; gap: 6px; diff --git a/admin/components/GroupEditor.vue b/admin/components/GroupEditor.vue new file mode 100644 index 0000000..6dd2962 --- /dev/null +++ b/admin/components/GroupEditor.vue @@ -0,0 +1,112 @@ + + + diff --git a/admin/components/RouteCard.vue b/admin/components/RouteCard.vue index 5b542e8..f2767d3 100644 --- a/admin/components/RouteCard.vue +++ b/admin/components/RouteCard.vue @@ -9,6 +9,7 @@ {{ route.name || "(untitled)" }} {{ route.id }} {{ route.lang }} + {{ groupName }}
@@ -33,7 +34,7 @@ import type { Route } from "~/types"; import { FILTER_LABELS, fmtMatch } from "~/types"; -const props = defineProps<{ route: Route }>(); +const props = defineProps<{ route: Route; groupName?: string }>(); const emit = defineEmits<{ (e: "toggle", route: Route): void; (e: "edit", route: Route): void; diff --git a/admin/components/RouteEditor.vue b/admin/components/RouteEditor.vue index b9f2088..2d07e74 100644 --- a/admin/components/RouteEditor.vue +++ b/admin/components/RouteEditor.vue @@ -14,6 +14,14 @@
+
+ + +
Create a group first (Groups tab).
+
@@ -68,14 +76,14 @@ diff --git a/admin/composables/useGroups.ts b/admin/composables/useGroups.ts new file mode 100644 index 0000000..f608918 --- /dev/null +++ b/admin/composables/useGroups.ts @@ -0,0 +1,53 @@ +import type { Group } from "~/types"; + +export function useGroupsApi() { + const groups = ref([]); + const isSuper = ref(false); + const loading = ref(false); + const needLogin = ref(false); + const error = ref(""); + + async function load(): Promise { + loading.value = true; + error.value = ""; + needLogin.value = false; + try { + const res = await fetch("/admin/api/groups", { + headers: { accept: "application/json" }, + credentials: "same-origin", + }); + if (res.status === 401) { + needLogin.value = true; + return; + } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { groups?: Group[]; isSuper?: boolean }; + groups.value = data.groups ?? []; + isSuper.value = data.isSuper ?? false; + } catch (err) { + error.value = err instanceof Error ? err.message : String(err); + } finally { + loading.value = false; + } + } + + async function save(next: Group[]): Promise { + const res = await fetch("/admin/api/groups", { + method: "PUT", + headers: { "content-type": "application/json" }, + credentials: "same-origin", + body: JSON.stringify({ groups: next }), + }); + if (res.status === 401) { + needLogin.value = true; + throw new Error("unauthorized"); + } + if (!res.ok) { + const data = (await res.json().catch(() => ({}))) as { error?: string }; + throw new Error(data.error ?? `HTTP ${res.status}`); + } + groups.value = next; + } + + return { groups, isSuper, loading, needLogin, error, load, save }; +} diff --git a/admin/composables/useMe.ts b/admin/composables/useMe.ts new file mode 100644 index 0000000..6e2ff39 --- /dev/null +++ b/admin/composables/useMe.ts @@ -0,0 +1,30 @@ +import type { Me } from "~/types"; + +export function useMeApi() { + const me = ref(null); + const loading = ref(false); + const needLogin = ref(false); + + async function load(): Promise { + loading.value = true; + needLogin.value = false; + try { + const res = await fetch("/admin/api/me", { + headers: { accept: "application/json" }, + credentials: "same-origin", + }); + if (res.status === 401) { + needLogin.value = true; + return; + } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + me.value = (await res.json()) as Me; + } catch { + me.value = null; + } finally { + loading.value = false; + } + } + + return { me, loading, needLogin, load }; +} diff --git a/admin/pages/admin.vue b/admin/pages/admin.vue index 2923e7c..e262d1e 100644 --- a/admin/pages/admin.vue +++ b/admin/pages/admin.vue @@ -9,7 +9,16 @@
- + + Sign out
@@ -18,7 +27,9 @@

Config Console

-

Access denied — this GitHub account is not in ADMIN_USER_IDS.

+

+ Access denied — this GitHub account is not an admin or group admin. +

Sign in with GitHub to manage routes.

Sign in with GitHub
@@ -29,6 +40,14 @@ + @@ -53,6 +72,7 @@ v-for="(r, i) in routes" :key="r.id" :route="r" + :group-name="groupName(r.groupId)" :style="{ animationDelay: i * 45 + 'ms' }" @toggle="onToggle" @edit="openEdit" @@ -66,6 +86,51 @@ + + diff --git a/admin/types.ts b/admin/types.ts index 05ef82d..9daa259 100644 --- a/admin/types.ts +++ b/admin/types.ts @@ -14,6 +14,21 @@ export interface Route { threadId?: string; }; lang?: string; + groupId?: string; +} + +export interface Group { + id: string; + name: string; + adminIds: string[]; + owners?: string[]; +} + +export interface Me { + login: string; + userId: string; + isSuper: boolean; + groups: Group[]; } export const FILTER_TYPES = ["event", "repo", "actor", "action", "branch", "keyword"] as const;