chore(portal,operator): upgrade to Nuxt 4

Upgrade both Nuxt apps to Nuxt 4.4.6 (vue-tsc 3, TypeScript 5.6, undici 7) and add a root tsconfig.json to each app. Fix the strict-null / noUncheckedIndexedAccess errors surfaced by Nuxt 4's stricter generated tsconfig and vue-tsc 3. Drop the nuxt-oidc-auth pnpm patch (Nuxt 4 fixes the prepare:types crash natively).
This commit is contained in:
Ronni Baslund
2026-05-30 08:02:43 +02:00
parent 0bd4e5498e
commit 17ffd95a70
25 changed files with 888 additions and 706 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ const initials = computed(() =>
.split(' ')
.filter(Boolean)
.slice(0, 2)
.map((p) => p[0].toUpperCase())
.map((p) => p.charAt(0).toUpperCase())
.join(''),
)
+3 -2
View File
@@ -18,9 +18,10 @@ try { oidc = useOidcAuth() } catch { oidc = null }
const open = ref(false)
const rootRef = ref<HTMLElement | null>(null)
const displayName = computed(() => {
const displayName = computed<string>(() => {
const u = oidc?.user?.value
return u?.userInfo?.name || u?.userName || 'Anne Hansen'
const name = (u?.userInfo as { name?: string } | undefined)?.name
return name || (u?.userName as string | undefined) || 'Anne Hansen'
})
const email = computed(() => {
const u = oidc?.user?.value
@@ -1,20 +1,17 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends { id: string; current?: boolean; trusted?: boolean }">
// "..." menu for a single device row. The menu is teleported to <body> so it
// escapes any overflow/clip on the table — same pattern as the React design
// source. Closes on outside-click, Escape, or scroll.
//
// Generic over the device type so the emitted payload keeps the caller's full
// device shape (label, os, …) instead of narrowing to a subset.
interface DeviceLike {
id: string
current?: boolean
trusted?: boolean
}
const props = defineProps<{ device: DeviceLike }>()
defineProps<{ device: T }>()
const emit = defineEmits<{
rename: [DeviceLike]
trust: [DeviceLike]
history: [DeviceLike]
revoke: [DeviceLike]
rename: [T]
trust: [T]
history: [T]
revoke: [T]
}>()
const open = ref(false)
@@ -24,7 +24,7 @@ const emit = defineEmits<{ 'update:modelValue': [Presence] }>()
const open = ref(false)
const rootRef = ref<HTMLElement | null>(null)
const current = computed(() => opts.find((o) => o.value === props.modelValue) ?? opts[0])
const current = computed(() => opts.find((o) => o.value === props.modelValue) ?? opts[0]!)
function pick(v: Presence) {
emit('update:modelValue', v)
+2 -1
View File
@@ -36,7 +36,8 @@ const geometry = computed(() => {
})
const line = pts.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${y.toFixed(1)}`).join(' ')
const area = `${line} L ${props.width} ${props.height} L 0 ${props.height} Z`
const last = { x: pts[pts.length - 1][0], y: pts[pts.length - 1][1] }
const lastPt = pts[pts.length - 1]!
const last = { x: lastPt[0], y: lastPt[1] }
return { line, area, last, min, max }
})
</script>
+5 -3
View File
@@ -2,7 +2,7 @@
"name": "@dezky/portal",
"version": "0.0.1",
"private": true,
"description": "Dezky customer-facing portal — Nuxt 3",
"description": "Dezky customer-facing portal — Nuxt 4",
"scripts": {
"dev": "nuxt dev --host 0.0.0.0 --port 3000 --dotenv ../../.env",
"build": "nuxt build",
@@ -11,14 +11,16 @@
"lint": "eslint ."
},
"dependencies": {
"nuxt": "^3.13.0",
"nuxt": "^4.4.6",
"nuxt-oidc-auth": "1.0.0-beta.11",
"undici": "^7.2.1",
"vue": "^3.5.0",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.5.0"
"typescript": "^5.6.0",
"vue-tsc": "^3.2.6"
},
"packageManager": "pnpm@9.12.0"
}
+2 -2
View File
@@ -302,9 +302,9 @@ function confirmPause() {
<div class="lead">Pick a new tier. We'll prorate the difference and apply it on your next invoice.</div>
<div class="plan-options">
<button v-for="p in [
{ id: 'basic', name: 'Basic', price: '49 DKK / seat / mo', d: 'Mail · Drev · 50 GB' },
{ id: 'basic', name: 'Basic', price: '49 DKK / seat / mo', d: 'Mail · Drev · 50 GB', current: false },
{ id: 'business', name: 'Business · current', price: '78 DKK / seat / mo', d: 'Everything in Basic + Møder + Chat · 200 GB', current: true },
{ id: 'enterprise', name: 'Enterprise', price: 'from 140 DKK / seat / mo', d: 'SSO contracts · audit log retention · 1 TB' },
{ id: 'enterprise', name: 'Enterprise', price: 'from 140 DKK / seat / mo', d: 'SSO contracts · audit log retention · 1 TB', current: false },
]" :key="p.id" :class="['plan-card', { active: p.current }]">
<div class="plan-name">{{ p.name }}</div>
<Mono dim>{{ p.price }}</Mono>
+5 -1
View File
@@ -18,6 +18,10 @@ const editTemplate = ref<typeof TEMPLATES[number] | null>(null)
const subject = ref('')
const body = ref('')
const testSent = ref(false)
function sendTest() {
testSent.value = true
setTimeout(() => (testSent.value = false), 2500)
}
const publishOpen = ref(false)
const publishState = ref<'confirm' | 'publishing' | 'done'>('confirm')
@@ -384,7 +388,7 @@ const renderedBody = computed(() =>
Reset to default
</UiButton>
<div style="flex: 1" />
<UiButton variant="secondary" @click="testSent = true; setTimeout(() => testSent = false, 2500)">
<UiButton variant="secondary" @click="sendTest">
<template #leading><UiIcon name="mail" :size="13" /></template>
{{ testSent ? 'Sent to anne@dezky.com ✓' : 'Send test to me' }}
</UiButton>
+12 -6
View File
@@ -17,12 +17,18 @@ const inviteStep = ref(1)
const seatsOpen = ref(false)
const seatsExtra = ref(5)
const stats = [
{ label: 'Seats used', value: '11 / 25', delta: '+2 this week', deltaTone: 'up' as const, hint: '' },
{ label: 'Storage', value: '1.4 TB', delta: '64% of 2.2 TB', hint: '' },
{ label: 'Mail flow', value: 'Healthy', hint: '99.98% · last 7d' },
{ label: 'Monthly spend', value: '1.940 DKK', hint: 'next invoice 01 Jun' },
] as const
const stats: Array<{
label: string
value: string
delta?: string
deltaTone?: 'up' | 'down'
hint: string
}> = [
{ label: 'Seats used', value: '11 / 25', delta: '+2 this week', deltaTone: 'up', hint: '' },
{ label: 'Storage', value: '1.4 TB', delta: '64% of 2.2 TB', hint: '' },
{ label: 'Mail flow', value: 'Healthy', hint: '99.98% · last 7d' },
{ label: 'Monthly spend', value: '1.940 DKK', hint: 'next invoice 01 Jun' },
]
const recent = sampleAudit.slice(0, 6)
+1 -1
View File
@@ -605,7 +605,7 @@ Mikkel Sørensen,mikkel@baslund.dk,admin,Engineering,business</pre>
<label v-for="r in ['member', 'admin', 'owner'] as const" :key="r" class="role-row" :class="{ active: roleChoice === r }">
<input type="radio" :value="r" v-model="roleChoice" />
<div>
<div class="role-name">{{ r[0].toUpperCase() + r.slice(1) }}</div>
<div class="role-name">{{ r.charAt(0).toUpperCase() + r.slice(1) }}</div>
<Mono dim>
{{ r === 'member' ? 'Standard access to apps' :
r === 'admin' ? 'Manage users, billing, and settings' :
+1 -1
View File
@@ -59,7 +59,7 @@ function tenantColor(slug?: string): string {
if (!slug) return 'var(--text-mute)'
let h = 0
for (let i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) | 0
return PALETTE[Math.abs(h) % PALETTE.length]
return PALETTE[Math.abs(h) % PALETTE.length]!
}
function eventTone(e: ActivityEvent): AuditRow['tone'] {
+407 -331
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ import { getUserSession } from 'nuxt-oidc-auth/runtime/server/utils/session.js'
function decodeJwtClaims(token: string): Record<string, unknown> {
const parts = token.split('.')
if (parts.length < 2) throw new Error('Not a JWT')
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/')
const payload = parts[1]!.replace(/-/g, '+').replace(/_/g, '/')
const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'))
}
@@ -17,7 +17,7 @@ const ADMIN_GROUPS = new Set(['dezky-platform-admins', 'authentik Admins'])
function decodeJwtClaims(token: string): Record<string, unknown> {
const parts = token.split('.')
if (parts.length < 2) throw new Error('Not a JWT')
const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/')
const payload = parts[1]!.replace(/-/g, '+').replace(/_/g, '/')
const padded = payload + '='.repeat((4 - (payload.length % 4)) % 4)
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'))
}
+6
View File
@@ -0,0 +1,6 @@
{
// Root tsconfig for `nuxt typecheck` / IDEs. Extends the config Nuxt
// generates into .nuxt/ on `nuxt prepare` (aliases, auto-imports, etc.).
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}