fix(operator,portal): env-driven sign-out URLs + host labels (no more .local in prod)
Operator sign-out hardcoded the dev Authentik end-session URL, so prod logout landed on auth.dezky.local. Mirror the portal's env-driven pattern (NUXT_PUBLIC_AUTH_URL/NUXT_PUBLIC_OPERATOR_URL with .local fallbacks). Expose authUrl/operatorUrl via public runtimeConfig and use them for the Authentik admin links and the cosmetic host labels (sidebar, eyebrows, auth-page hints). Portal: signed-out + webmail copy now derive their hosts from runtime config (new public.mailUrl, NUXT_PUBLIC_MAIL_URL in prod).
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const operatorHost = new URL(useRuntimeConfig().public.operatorUrl).host
|
||||||
import type { IconName } from './UiIcon.vue'
|
import type { IconName } from './UiIcon.vue'
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
@@ -45,7 +46,7 @@ const isSection = (r: NavRow): r is NavSection => 'sec' in r
|
|||||||
<span class="tile"><NodeMark :size="22" fg="#F4F3EE" accent="#D4FF3A" /></span>
|
<span class="tile"><NodeMark :size="22" fg="#F4F3EE" accent="#D4FF3A" /></span>
|
||||||
<div v-if="!collapsed" class="brand-meta">
|
<div v-if="!collapsed" class="brand-meta">
|
||||||
<div class="brand-name">dezky · ops</div>
|
<div class="brand-name">dezky · ops</div>
|
||||||
<div class="brand-host">operator.dezky.local</div>
|
<div class="brand-host">{{ operatorHost }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
modules: ['nuxt-oidc-auth'],
|
modules: ['nuxt-oidc-auth'],
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
// Overridable at runtime via NUXT_PUBLIC_AUTH_URL / NUXT_PUBLIC_OPERATOR_URL
|
||||||
|
// (both set in production). Used for Authentik links and host labels.
|
||||||
|
authUrl: AUTH_URL,
|
||||||
|
operatorUrl: OPERATOR_URL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
css: ['~/assets/styles/tokens.css', '~/assets/styles/base.css'],
|
css: ['~/assets/styles/tokens.css', '~/assets/styles/base.css'],
|
||||||
|
|
||||||
// Auto-import from the shared packages/ui workspace in addition to the
|
// Auto-import from the shared packages/ui workspace in addition to the
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const operatorHost = new URL(useRuntimeConfig().public.operatorUrl).host
|
||||||
// O.3 scaffolding login. Real visual treatment lands in O.4 with the full
|
// O.3 scaffolding login. Real visual treatment lands in O.4 with the full
|
||||||
// design system port. For now: minimal dark-themed bounce to Authentik.
|
// design system port. For now: minimal dark-themed bounce to Authentik.
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ async function signIn() {
|
|||||||
Authentik-issued tokens · platform-admin group required · MFA when enrolled.
|
Authentik-issued tokens · platform-admin group required · MFA when enrolled.
|
||||||
</p>
|
</p>
|
||||||
<button class="primary" @click="signIn">Sign in</button>
|
<button class="primary" @click="signIn">Sign in</button>
|
||||||
<p class="hint">operator.dezky.local</p>
|
<p class="hint">{{ operatorHost }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const operatorHost = new URL(useRuntimeConfig().public.operatorUrl).host
|
||||||
import type { Tenant } from '~/types/tenant'
|
import type { Tenant } from '~/types/tenant'
|
||||||
import type { Partner } from '~/types/partner'
|
import type { Partner } from '~/types/partner'
|
||||||
import type { PlatformUser } from '~/types/user'
|
import type { PlatformUser } from '~/types/user'
|
||||||
@@ -65,7 +66,7 @@ function fmtDate(d: string) {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
eyebrow="Operator · operator.dezky.local"
|
:eyebrow="`Operator · ${operatorHost}`"
|
||||||
title="Platform overview"
|
title="Platform overview"
|
||||||
:subtitle="`${stats.tenants} tenants · ${stats.partners} partners · ${stats.users} platform users`"
|
:subtitle="`${stats.tenants} tenants · ${stats.partners} partners · ${stats.users} platform users`"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const operatorHost = new URL(useRuntimeConfig().public.operatorUrl).host
|
||||||
// Shown when an authenticated-but-non-operator session reaches the operator
|
// Shown when an authenticated-but-non-operator session reaches the operator
|
||||||
// portal (see middleware/require-platform-admin.global.ts). The account is
|
// portal (see middleware/require-platform-admin.global.ts). The account is
|
||||||
// valid in Authentik but lacks platformAdmin — e.g. a partner or tenant user
|
// valid in Authentik but lacks platformAdmin — e.g. a partner or tenant user
|
||||||
@@ -42,7 +43,7 @@ onMounted(() => {
|
|||||||
continue.
|
continue.
|
||||||
</p>
|
</p>
|
||||||
<button class="primary" type="button" @click="signOut">Sign out now</button>
|
<button class="primary" type="button" @click="signOut">Sign out now</button>
|
||||||
<p class="hint">operator.dezky.local</p>
|
<p class="hint">{{ operatorHost }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const authUrl = useRuntimeConfig().public.authUrl
|
||||||
import type { PlatformUser } from '~/types/user'
|
import type { PlatformUser } from '~/types/user'
|
||||||
|
|
||||||
const { data: users, pending, refresh } = await useFetch<PlatformUser[]>('/api/users', {
|
const { data: users, pending, refresh } = await useFetch<PlatformUser[]>('/api/users', {
|
||||||
@@ -41,7 +42,7 @@ async function onInvited() {
|
|||||||
<template #leading><UiIcon name="refresh" :size="13" /></template>
|
<template #leading><UiIcon name="refresh" :size="13" /></template>
|
||||||
Refresh
|
Refresh
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<a href="https://auth.dezky.local/if/admin/" target="_blank" rel="noopener" class="link">
|
<a :href="`${authUrl}/if/admin/`" target="_blank" rel="noopener" class="link">
|
||||||
<UiButton variant="secondary">
|
<UiButton variant="secondary">
|
||||||
<template #leading><UiIcon name="external" :size="13" /></template>
|
<template #leading><UiIcon name="external" :size="13" /></template>
|
||||||
Manage in Authentik
|
Manage in Authentik
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const operatorHost = new URL(useRuntimeConfig().public.operatorUrl).host
|
||||||
// Pricing catalog editor. Operator-only. Each (plan, cycle) is a single row
|
// Pricing catalog editor. Operator-only. Each (plan, cycle) is a single row
|
||||||
// with three independent per-currency amounts (DKK / EUR / USD). Operator
|
// with three independent per-currency amounts (DKK / EUR / USD). Operator
|
||||||
// types clean round numbers in each currency — no FX derivation. Empty cells
|
// types clean round numbers in each currency — no FX derivation. Empty cells
|
||||||
@@ -233,7 +234,7 @@ const sortedPrices = computed<PriceRow[]>(() =>
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
eyebrow="Operator · operator.dezky.local"
|
:eyebrow="`Operator · ${operatorHost}`"
|
||||||
title="Pricing catalog"
|
title="Pricing catalog"
|
||||||
subtitle="One row per plan + cycle, with independent prices per currency. Editing an amount re-prices live customers on that currency at their next billing cycle (no mid-cycle charge) and applies to all new subscriptions."
|
subtitle="One row per plan + cycle, with independent prices per currency. Editing an amount re-prices live customers on that currency at their next billing cycle (no mid-cycle charge) and applies to all new subscriptions."
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const lastSignIn = computed(() => {
|
|||||||
return new Date(iat * 1000)
|
return new Date(iat * 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
const AUTHENTIK = 'https://auth.dezky.local'
|
const AUTHENTIK = useRuntimeConfig().public.authUrl
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
icon: 'key' as const,
|
icon: 'key' as const,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const operatorHost = new URL(useRuntimeConfig().public.operatorUrl).host
|
||||||
// Sign-out landing for the operator portal. /api/auth/sign-out cleared the
|
// Sign-out landing for the operator portal. /api/auth/sign-out cleared the
|
||||||
// local session and bounced through Authentik's end-session endpoint, which
|
// local session and bounced through Authentik's end-session endpoint, which
|
||||||
// ended the IdP session. By the time we render here the user has no
|
// ended the IdP session. By the time we render here the user has no
|
||||||
@@ -24,7 +25,7 @@ function signInAgain() {
|
|||||||
ready — Authentik will ask for fresh credentials.
|
ready — Authentik will ask for fresh credentials.
|
||||||
</p>
|
</p>
|
||||||
<button class="primary" type="button" @click="signInAgain">Sign in again</button>
|
<button class="primary" type="button" @click="signInAgain">Sign in again</button>
|
||||||
<p class="hint">operator.dezky.local</p>
|
<p class="hint">{{ operatorHost }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,15 +10,20 @@
|
|||||||
// 3. 302 the BROWSER through Authentik's dezky-operator end-session URL
|
// 3. 302 the BROWSER through Authentik's dezky-operator end-session URL
|
||||||
// with post_logout_redirect_uri=/signed-out.
|
// with post_logout_redirect_uri=/signed-out.
|
||||||
//
|
//
|
||||||
// The brief URL-bar flash to auth.dezky.local is unavoidable: that's the
|
// The brief URL-bar flash to the Authentik host is unavoidable: that's the
|
||||||
// only host that can clear the Authentik session cookie (server-to-server
|
// only host that can clear the Authentik session cookie (server-to-server
|
||||||
// invalidation alone leaves the browser cookie, which would let the next
|
// invalidation alone leaves the browser cookie, which would let the next
|
||||||
// visit silently re-authorize).
|
// visit silently re-authorize).
|
||||||
|
|
||||||
import { getUserSession, clearUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
import { getUserSession, clearUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
|
||||||
|
|
||||||
const END_SESSION = 'https://auth.dezky.local/application/o/dezky-operator/end-session/'
|
// Environment-driven so one build serves dev (.local) and prod (.eu) — same
|
||||||
const POST_LOGOUT_REDIRECT = 'https://operator.dezky.local/signed-out'
|
// pattern as the customer portal's sign-out.
|
||||||
|
const AUTH_URL = (process.env.NUXT_PUBLIC_AUTH_URL || 'https://auth.dezky.local').replace(/\/$/, '')
|
||||||
|
const OPERATOR_URL = (process.env.NUXT_PUBLIC_OPERATOR_URL || 'https://operator.dezky.local').replace(/\/$/, '')
|
||||||
|
const OIDC_APP_SLUG = process.env.OPERATOR_OIDC_APP_SLUG || 'dezky-operator'
|
||||||
|
const END_SESSION = `${AUTH_URL}/application/o/${OIDC_APP_SLUG}/end-session/`
|
||||||
|
const POST_LOGOUT_REDIRECT = `${OPERATOR_URL}/signed-out`
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const session = await getUserSession(event).catch(() => ({} as any))
|
const session = await getUserSession(event).catch(() => ({} as any))
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default defineNuxtConfig({
|
|||||||
authUrl: process.env.NUXT_PUBLIC_AUTH_URL,
|
authUrl: process.env.NUXT_PUBLIC_AUTH_URL,
|
||||||
portalUrl: process.env.NUXT_PUBLIC_PORTAL_URL,
|
portalUrl: process.env.NUXT_PUBLIC_PORTAL_URL,
|
||||||
bookingUrl: process.env.NUXT_PUBLIC_BOOKING_URL || 'https://booking.dezky.local',
|
bookingUrl: process.env.NUXT_PUBLIC_BOOKING_URL || 'https://booking.dezky.local',
|
||||||
|
mailUrl: process.env.NUXT_PUBLIC_MAIL_URL || 'https://mail.dezky.local',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const mailHost = new URL(useRuntimeConfig().public.mailUrl as string).host
|
||||||
// Users & groups. The Users tab is real — workspace members come from
|
// Users & groups. The Users tab is real — workspace members come from
|
||||||
// /api/tenants/:slug/users (platform-api UserDocument). The Groups,
|
// /api/tenants/:slug/users (platform-api UserDocument). The Groups,
|
||||||
// Invitations and Service-accounts tabs have no backend yet (no Group /
|
// Invitations and Service-accounts tabs have no backend yet (no Group /
|
||||||
@@ -902,7 +903,7 @@ async function submitCreateMailbox() {
|
|||||||
<div v-if="resetResult" class="invite-result">
|
<div v-if="resetResult" class="invite-result">
|
||||||
<div class="ir-check"><UiIcon name="key" :size="20" /></div>
|
<div class="ir-check"><UiIcon name="key" :size="20" /></div>
|
||||||
<div class="ir-title">Password reset</div>
|
<div class="ir-title">Password reset</div>
|
||||||
<p class="ir-sub">Share this securely. It works for both sign-in and webmail at <Mono>mail.dezky.local</Mono>.</p>
|
<p class="ir-sub">Share this securely. It works for both sign-in and webmail at <Mono>{{ mailHost }}</Mono>.</p>
|
||||||
<div class="cred">
|
<div class="cred">
|
||||||
<div class="cred-row">
|
<div class="cred-row">
|
||||||
<span class="cred-k">Email</span><Mono class="cred-v">{{ resetResult.email }}</Mono>
|
<span class="cred-k">Email</span><Mono class="cred-v">{{ resetResult.email }}</Mono>
|
||||||
@@ -1011,7 +1012,7 @@ async function submitCreateMailbox() {
|
|||||||
<div v-if="mailboxResult" class="invite-result">
|
<div v-if="mailboxResult" class="invite-result">
|
||||||
<div class="ir-check"><UiIcon name="check" :size="22" :stroke-width="2.5" /></div>
|
<div class="ir-check"><UiIcon name="check" :size="22" :stroke-width="2.5" /></div>
|
||||||
<div class="ir-title">{{ mailboxResult.email }} is ready</div>
|
<div class="ir-title">{{ mailboxResult.email }} is ready</div>
|
||||||
<p class="ir-sub">Share these securely. They sign in to webmail at <Mono>mail.dezky.local</Mono> with this password — their portal sign-in is unchanged.</p>
|
<p class="ir-sub">Share these securely. They sign in to webmail at <Mono>{{ mailHost }}</Mono> with this password — their portal sign-in is unchanged.</p>
|
||||||
<div class="cred">
|
<div class="cred">
|
||||||
<div class="cred-row">
|
<div class="cred-row">
|
||||||
<span class="cred-k">Mailbox</span><Mono class="cred-v">{{ mailboxResult.email }}</Mono>
|
<span class="cred-k">Mailbox</span><Mono class="cred-v">{{ mailboxResult.email }}</Mono>
|
||||||
@@ -1045,7 +1046,7 @@ async function submitCreateMailbox() {
|
|||||||
<div v-else-if="inviteResult" class="invite-result">
|
<div v-else-if="inviteResult" class="invite-result">
|
||||||
<div class="ir-check"><UiIcon name="check" :size="22" :stroke-width="2.5" /></div>
|
<div class="ir-check"><UiIcon name="check" :size="22" :stroke-width="2.5" /></div>
|
||||||
<div class="ir-title">{{ inviteResult.email }} is ready</div>
|
<div class="ir-title">{{ inviteResult.email }} is ready</div>
|
||||||
<p class="ir-sub">Share these credentials securely. They sign in to the portal and to webmail at <Mono>mail.dezky.local</Mono>.</p>
|
<p class="ir-sub">Share these credentials securely. They sign in to the portal and to webmail at <Mono>{{ mailHost }}</Mono>.</p>
|
||||||
<div class="cred">
|
<div class="cred">
|
||||||
<div class="cred-row">
|
<div class="cred-row">
|
||||||
<span class="cred-k">Email</span><Mono class="cred-v">{{ inviteResult.email }}</Mono>
|
<span class="cred-k">Email</span><Mono class="cred-v">{{ inviteResult.email }}</Mono>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const portalHost = new URL(useRuntimeConfig().public.portalUrl as string).host
|
||||||
// Sign-out landing. /api/auth/sign-out cleared the local session and bounced
|
// Sign-out landing. /api/auth/sign-out cleared the local session and bounced
|
||||||
// through Authentik's end-session endpoint, which ended the IdP session.
|
// through Authentik's end-session endpoint, which ended the IdP session.
|
||||||
// At this point the user has no portal session and no Authentik session —
|
// At this point the user has no portal session and no Authentik session —
|
||||||
@@ -30,7 +31,7 @@ function signInAgain() {
|
|||||||
|
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Closing the tab? Your data stays put on
|
Closing the tab? Your data stays put on
|
||||||
<span class="mono">app.dezky.local</span>.
|
<span class="mono">{{ portalHost }}</span>.
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
</AuthShell>
|
</AuthShell>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ spec:
|
|||||||
value: https://app.dezky.eu
|
value: https://app.dezky.eu
|
||||||
- name: NUXT_PUBLIC_BOOKING_URL
|
- name: NUXT_PUBLIC_BOOKING_URL
|
||||||
value: https://booking.dezky.eu
|
value: https://booking.dezky.eu
|
||||||
|
- name: NUXT_PUBLIC_MAIL_URL
|
||||||
|
value: https://mail.dezky.eu
|
||||||
# Cluster-internal address of platform-api for the nitro proxy.
|
# Cluster-internal address of platform-api for the nitro proxy.
|
||||||
- name: PLATFORM_API_INTERNAL_URL
|
- name: PLATFORM_API_INTERNAL_URL
|
||||||
value: http://platform-api.dezky-apps.svc.cluster.local:3001
|
value: http://platform-api.dezky-apps.svc.cluster.local:3001
|
||||||
|
|||||||
Reference in New Issue
Block a user