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

}