From 1178e9edfe969b41d750cfff9ba0c29bd93508a9 Mon Sep 17 00:00:00 2001 From: Kundencenter Date: Thu, 1 Oct 2026 11:54:46 +0200 Subject: [PATCH] Show registered domains on hosting resource pages The resource detail now lists the domain records linked to the hosting account (term, registration status), so customers see the domains included in their package. Prices and procurement state are staff-only. Co-Authored-By: Claude Opus 5.5 --- apps/api/src/modules/resources/index.ts | 6 +++++- apps/web/src/app/(app)/ressourcen/[id]/page.tsx | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/api/src/modules/resources/index.ts b/apps/api/src/modules/resources/index.ts index 893eb3a..52fc74e 100644 --- a/apps/api/src/modules/resources/index.ts +++ b/apps/api/src/modules/resources/index.ts @@ -1,6 +1,7 @@ import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { randomUUID, createHash } from 'node:crypto'; +import { domainToUnicode } from 'node:url'; import { ACTION_CAPABILITY, ACTIONS, loadInstance, type ActionName } from '@kc/connectors'; import { ConnectorError, type ChildKind } from '@kc/connector-sdk'; import { decrypt, encrypt, randomToken } from '../../core/crypto.js'; @@ -88,7 +89,10 @@ export const resourcesModule: KcModule = { const staff = can(a.principal, 'resources.read'); const jobs = await query("SELECT id, status, last_error, attempts, created_at, updated_at, JSON_VALUE(payload, '$.action') AS action FROM jobs WHERE type = 'connector.execute' AND JSON_VALUE(payload, '$.resourceId') = ? ORDER BY created_at DESC LIMIT 10", [id]); const data = json<{ limits?: object; usage?: object; details?: object }>(r.data_json, {}); - return { ...view(r, staff), limits: data.limits ?? {}, usage: data.usage ?? {}, details: data.details ?? {}, allowedActions: allowedActions(r, a), canReveal: canReveal(r, a), canResetPassword: canResetPassword(r, a), hasChildren: childCapsTop(r).includes('children.read'), canLogin: canLoginTop(r, a), customerActions: staff ? json(r.customer_actions, []) : undefined, jobs: jobs.map((j) => ({ id: j.id, action: j.action, status: j.status, error: j.last_error, attempts: j.attempts, createdAt: j.created_at, updatedAt: j.updated_at })) }; + // Registrierte Domains aus der Domain-Aufstellung (Laufzeit/Registrierung), nicht die Live-Einträge im Panel; Preise nur für Personal + const domains = (await query('SELECT id, domain, term_months, procurement, sell_gross_cents FROM domain_records WHERE resource_id = ? ORDER BY domain', [id])) + .map((x) => ({ id: x.id, domain: domainToUnicode(x.domain) || x.domain, termMonths: x.term_months, registered: x.procurement !== 'open', ...(staff ? { procurement: x.procurement, sellGrossCents: x.sell_gross_cents } : {}) })); + return { ...view(r, staff), domains, limits: data.limits ?? {}, usage: data.usage ?? {}, details: data.details ?? {}, allowedActions: allowedActions(r, a), canReveal: canReveal(r, a), canResetPassword: canResetPassword(r, a), hasChildren: childCapsTop(r).includes('children.read'), canLogin: canLoginTop(r, a), customerActions: staff ? json(r.customer_actions, []) : undefined, jobs: jobs.map((j) => ({ id: j.id, action: j.action, status: j.status, error: j.last_error, attempts: j.attempts, createdAt: j.created_at, updatedAt: j.updated_at })) }; }); app.post('/resources/:id/actions', async (req, reply) => { diff --git a/apps/web/src/app/(app)/ressourcen/[id]/page.tsx b/apps/web/src/app/(app)/ressourcen/[id]/page.tsx index 08b24f8..de31e86 100644 --- a/apps/web/src/app/(app)/ressourcen/[id]/page.tsx +++ b/apps/web/src/app/(app)/ressourcen/[id]/page.tsx @@ -4,13 +4,15 @@ import Link from 'next/link'; import { useParams } from 'next/navigation'; import { api, errMsg } from '@/lib/api'; import { useSession } from '@/lib/session'; -import { ACTION_LABEL, Alert, Field, JobStatus, ResState, fmt, CUSTOMER_ACTIONS } from '@/components/ui'; +import { ACTION_LABEL, Alert, Field, JobStatus, ResState, fmt, eur, CUSTOMER_ACTIONS } from '@/components/ui'; import { SecretField } from '@/components/SecretField'; import { uuid } from '@/lib/uuid'; import { HostingPanel, UsageBars } from '@/components/HostingPanel'; interface Det { id: string; type: string; name: string; state: string; orgId: string | null; validFrom: string | null; validUntil: string | null; syncedAt: string; stale: boolean; staleReason: string | null; missing: boolean; instance?: string; connector?: string; - canReveal?: boolean; hasChildren?: boolean; canLogin?: boolean; limits: Record; usage: Record; details: Record; allowedActions: string[]; customerActions?: string[]; jobs: { id: string; action: string; status: string; error: string | null; createdAt: string }[] } + canReveal?: boolean; hasChildren?: boolean; canLogin?: boolean; limits: Record; usage: Record; details: Record; allowedActions: string[]; customerActions?: string[]; domains: DomainRec[]; jobs: { id: string; action: string; status: string; error: string | null; createdAt: string }[] } +interface DomainRec { id: string; domain: string; termMonths: number | null; registered: boolean; procurement?: string; sellGrossCents?: number | null } +const PROCUREMENT: Record = { open: 'noch nicht bestellt', ordered: 'beim Registrar bestellt', external: 'extern verwaltet' }; const LIMIT_LABEL: Record = { users: 'Benutzer', storageMb: 'Speicher (MB)' }; interface PanelCreds { canReset: boolean; set: boolean; setAt: string | null } export default function Ressource() { @@ -74,6 +76,12 @@ export default function Ressource() {
Gerät
{d.details.activation.hardwareIdMasked}
: d.type === 'license' ?
Aktivierung
Noch nicht aktiviert
: null}

Stand: {fmt(d.syncedAt)}{staff && d.instance ? ` · Verbindung: ${d.instance}` : ''}

+ {d.domains.length > 0 &&

Domains

+
{staff && } + {d.domains.map((x) => + + {staff && })}
DomainLaufzeitStatusPreis (brutto)
{x.domain}{x.termMonths ? `${x.termMonths} Monate` : '–'}{staff ? PROCUREMENT[x.procurement ?? ''] ?? x.procurement : x.registered ? 'Registriert' : 'In Bearbeitung'}{x.sellGrossCents != null ? eur(x.sellGrossCents) : '–'}
+ {staff &&

Zur Domain-Aufstellung

}
} {d.type === 'hosting_account' && } {(d.hasChildren || d.canLogin) && } {d.canReveal &&

Lizenzschlüssel

}