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:
@@ -15,7 +15,7 @@ const initials = computed(() =>
|
||||
.split(' ')
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((p) => p[0].toUpperCase())
|
||||
.map((p) => p.charAt(0).toUpperCase())
|
||||
.join(''),
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ const route = useRoute()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const displayName = computed(() => user.value?.userInfo?.name || user.value?.userName || 'operator')
|
||||
const displayName = computed<string>(() => {
|
||||
const name = (user.value?.userInfo as { name?: string } | undefined)?.name
|
||||
return name || (user.value?.userName as string | undefined) || 'operator'
|
||||
})
|
||||
const email = computed(() => (user.value?.userInfo as { email?: string } | undefined)?.email ?? '')
|
||||
|
||||
function toggle() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@dezky/operator",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "Dezky operator portal — internal admin app (Nuxt 3)",
|
||||
"description": "Dezky operator portal — internal admin app (Nuxt 4)",
|
||||
"scripts": {
|
||||
"dev": "nuxt dev --host 0.0.0.0 --port 3000",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ function fmtRelative(iso: string | null | undefined): string {
|
||||
<div class="cap">
|
||||
{{
|
||||
archives?.length
|
||||
? `archived through seq ${archives[0].endSeq} · ${archives.length} batch${archives.length === 1 ? '' : 'es'}`
|
||||
? `archived through seq ${archives[0]!.endSeq} · ${archives.length} batch${archives.length === 1 ? '' : 'es'}`
|
||||
: 'no archives yet · 90-day hot retention'
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -209,7 +209,7 @@ const sortedPrices = computed<PriceRow[]>(() =>
|
||||
<td v-for="c in CURRENCIES" :key="c" class="cell-amount">
|
||||
<template v-if="drafts[row._id]">
|
||||
<input
|
||||
v-model="drafts[row._id][c]"
|
||||
v-model="drafts[row._id]![c]"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
class="amount-input"
|
||||
|
||||
Generated
+407
-331
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,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'))
|
||||
}
|
||||
|
||||
@@ -26,7 +26,11 @@ function originatingIp(event: H3Event): string | undefined {
|
||||
export async function platformApi<T = unknown>(
|
||||
event: H3Event,
|
||||
path: string,
|
||||
init: { method?: string; body?: unknown; query?: Record<string, string | number | undefined> } = {},
|
||||
init: {
|
||||
method?: string
|
||||
body?: BodyInit | Record<string, unknown> | null
|
||||
query?: Record<string, string | number | undefined>
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
const session = await getUserSession(event).catch(() => null)
|
||||
const accessToken = (session as { accessToken?: string } | null)?.accessToken
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
@@ -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(''),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '' },
|
||||
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' },
|
||||
] as const
|
||||
]
|
||||
|
||||
const recent = sampleAudit.slice(0, 6)
|
||||
|
||||
|
||||
@@ -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' :
|
||||
|
||||
@@ -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'] {
|
||||
|
||||
Generated
+407
-331
File diff suppressed because it is too large
Load Diff
@@ -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'))
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -209,9 +209,10 @@ function onKey(e: KeyboardEvent) {
|
||||
activeIndex.value = Math.max(activeIndex.value - 1, 0)
|
||||
scrollActiveIntoView()
|
||||
} else if (e.key === 'Enter') {
|
||||
if (activeIndex.value >= 0 && filtered.value[activeIndex.value]) {
|
||||
const sel = filtered.value[activeIndex.value]
|
||||
if (activeIndex.value >= 0 && sel) {
|
||||
e.preventDefault()
|
||||
selectCountry(filtered.value[activeIndex.value])
|
||||
selectCountry(sel)
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
open.value = false
|
||||
|
||||
Reference in New Issue
Block a user