feat(portal): real OCIS storage data via refresh-token service auth
The Storage page + endpoint landed earlier but had no working OCIS backend credential. OCIS has no service-account/client-credentials grant and trusts a single issuer, and basic auth resolves no user in our external-IdP setup — so authenticate OcisClient via an OIDC refresh-token bootstrap instead: - One-time headless login of svc-platform-api against the ocis provider (public client ocis-web, issuer .../o/ocis/) yields a refresh token, persisted in Mongo (ocis_credentials) and rotated on every use. - OcisClient mints access tokens with the refresh_token grant; the service user holds the OCIS admin role (OCIS_ADMIN_USER_ID) so libregraph ListAllDrives works. - scripts/bootstrap-ocis.mjs re-runs the bootstrap if the token lapses. - Dashboard Plan card gains a storage capacity bar beside seats; hidden when storage is unavailable. - compose + .env.example: OCIS service OIDC env and admin user id. - docs/NEXT-STEPS: document the mechanism and the dead-end alternatives.
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
// `AdminDashboard`, but the data is real: workspace identity, seats, spend,
|
||||
// plan and recent admin events all come from /api/me + /api/tenants/:slug/*.
|
||||
//
|
||||
// Sections without a real backend source yet (storage usage, mail-flow health,
|
||||
// "open issues" like DMARC/failed-login heuristics) were removed rather than
|
||||
// faked — they return when their backends (OCIS metrics, Stalwart metrics, a
|
||||
// domain-health checker) exist.
|
||||
// Storage usage is real now too (OCIS libregraph via /tenants/:slug/storage) —
|
||||
// shown as a second capacity bar in the Plan card. Sections still without a
|
||||
// backend (mail-flow health, "open issues" like DMARC/failed-login heuristics)
|
||||
// stay removed rather than faked until Stalwart metrics / a domain-health
|
||||
// checker exist.
|
||||
|
||||
import type { IconName } from '~/components/UiIcon.vue'
|
||||
import type { AuditEventDoc, TenantUserDoc } from '~/types/workspace'
|
||||
@@ -30,6 +31,20 @@ const { data: auditRaw } = await useFetch<AuditEventDoc[]>(
|
||||
{ key: 'admin-dash-audit', default: () => [], immediate: !!slug.value, watch: [slug] },
|
||||
)
|
||||
|
||||
// Aggregate storage usage (OCIS) — second capacity bar in the Plan card.
|
||||
interface StorageSummary {
|
||||
available: boolean
|
||||
usedBytes: number
|
||||
quotaBytes: number
|
||||
freeBytes: number
|
||||
}
|
||||
const { data: storage } = await useFetch<StorageSummary | null>(
|
||||
() => `/api/tenants/${slug.value}/storage`,
|
||||
{ key: 'admin-dash-storage', default: () => null, immediate: !!slug.value, watch: [slug] },
|
||||
)
|
||||
const storageAvailable = computed(() => storage.value?.available === true)
|
||||
const storagePct = computed(() => percent(storage.value?.usedBytes ?? 0, storage.value?.quotaBytes ?? 0))
|
||||
|
||||
const seatsUsed = computed(() => (users.value ?? []).filter((u) => u.active !== false).length)
|
||||
const seatsAvailable = computed(() => Math.max(0, seatLimit.value - seatsUsed.value))
|
||||
const seatPct = computed(() =>
|
||||
@@ -149,6 +164,7 @@ const prorated = computed(() => Math.round(monthly.value * (daysUntilRenewal.val
|
||||
</div>
|
||||
|
||||
<div class="progress-block">
|
||||
<div class="bar-label">Seats</div>
|
||||
<div class="progress-bar"><span :style="{ width: `${seatPct}%` }" /></div>
|
||||
<div class="progress-legend">
|
||||
<span>{{ seatsUsed }} active</span>
|
||||
@@ -156,6 +172,15 @@ const prorated = computed(() => Math.round(monthly.value * (daysUntilRenewal.val
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="storageAvailable" class="progress-block">
|
||||
<div class="bar-label">Storage</div>
|
||||
<div class="progress-bar"><span :style="{ width: `${storagePct}%` }" /></div>
|
||||
<div class="progress-legend">
|
||||
<span>{{ formatBytes(storage!.usedBytes) }} used</span>
|
||||
<span>{{ formatBytes(storage!.freeBytes) }} free</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seats-cta">
|
||||
<div class="seats-cta-text">
|
||||
Approaching limit? You can add seats in single increments — billed prorated.
|
||||
@@ -335,6 +360,14 @@ const prorated = computed(() => Math.round(monthly.value * (daysUntilRenewal.val
|
||||
|
||||
/* License progress */
|
||||
.progress-block { margin-bottom: 16px; }
|
||||
.bar-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-mute);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--bg);
|
||||
|
||||
Reference in New Issue
Block a user