+ );
+}
diff --git a/src/lib/db.ts b/src/lib/db.ts
new file mode 100644
index 0000000..714729c
--- /dev/null
+++ b/src/lib/db.ts
@@ -0,0 +1,175 @@
+/**
+ * Database client for the portal-db PostgreSQL database.
+ *
+ * Uses the `pg` package directly — no ORM overhead for a single table.
+ * The tenant_requests table acts as the approval gate between customer
+ * registration and actual PiecedTenant CR creation.
+ *
+ * Connection: via DATABASE_URL env var pointing to CloudNativePG cluster.
+ */
+
+import { Pool } from "pg";
+import type { TenantRequest, TenantRequestStatus } from "@/types";
+
+// Lazy-init: pool is created on first use, not at module import time.
+// This avoids "Invalid URL" errors during Next.js build when env vars
+// aren't available yet.
+let _pool: Pool | null = null;
+
+function getPool(): Pool {
+ if (!_pool) {
+ const url = process.env.DATABASE_URL;
+ if (!url) throw new Error("DATABASE_URL is not set");
+ _pool = new Pool({
+ connectionString: url,
+ max: 5,
+ idleTimeoutMillis: 30_000,
+ });
+ }
+ return _pool;
+}
+
+// ---------------------------------------------------------------------------
+// Schema migration (idempotent)
+// ---------------------------------------------------------------------------
+
+const MIGRATION_SQL = `
+CREATE TABLE IF NOT EXISTS tenant_requests (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ zitadel_org_id TEXT NOT NULL UNIQUE,
+ zitadel_user_id TEXT NOT NULL,
+ company_name TEXT NOT NULL,
+ contact_name TEXT NOT NULL,
+ contact_email TEXT NOT NULL,
+ agent_name TEXT NOT NULL DEFAULT 'Assistant',
+ soul_md TEXT,
+ packages TEXT[] DEFAULT '{}',
+ billing_address JSONB DEFAULT '{}',
+ billing_notes TEXT,
+ status TEXT NOT NULL DEFAULT 'pending',
+ admin_notes TEXT,
+ tenant_name TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE INDEX IF NOT EXISTS idx_tenant_requests_status ON tenant_requests(status);
+CREATE INDEX IF NOT EXISTS idx_tenant_requests_org_id ON tenant_requests(zitadel_org_id);
+`;
+
+let migrated = false;
+
+export async function ensureSchema(): Promise {
+ if (migrated) return;
+ await getPool().query(MIGRATION_SQL);
+ migrated = true;
+}
+
+// ---------------------------------------------------------------------------
+// CRUD
+// ---------------------------------------------------------------------------
+
+export async function createTenantRequest(
+ params: Omit
+): Promise {
+ await ensureSchema();
+ const result = await getPool().query(
+ `INSERT INTO tenant_requests
+ (zitadel_org_id, zitadel_user_id, company_name, contact_name,
+ contact_email, agent_name, soul_md, packages, billing_address, billing_notes)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
+ RETURNING *`,
+ [
+ params.zitadelOrgId,
+ params.zitadelUserId,
+ params.companyName,
+ params.contactName,
+ params.contactEmail,
+ params.agentName,
+ params.soulMd,
+ params.packages,
+ JSON.stringify(params.billingAddress),
+ params.billingNotes,
+ ]
+ );
+ return mapRow(result.rows[0]);
+}
+
+export async function getTenantRequestByOrgId(
+ orgId: string
+): Promise {
+ await ensureSchema();
+ const result = await getPool().query(
+ "SELECT * FROM tenant_requests WHERE zitadel_org_id = $1",
+ [orgId]
+ );
+ return result.rows[0] ? mapRow(result.rows[0]) : null;
+}
+
+export async function getTenantRequestById(
+ id: string
+): Promise {
+ await ensureSchema();
+ const result = await getPool().query(
+ "SELECT * FROM tenant_requests WHERE id = $1",
+ [id]
+ );
+ return result.rows[0] ? mapRow(result.rows[0]) : null;
+}
+
+export async function listTenantRequests(
+ status?: TenantRequestStatus
+): Promise {
+ await ensureSchema();
+ const pool = getPool();
+ const query = status
+ ? { text: "SELECT * FROM tenant_requests WHERE status = $1 ORDER BY created_at DESC", values: [status] }
+ : { text: "SELECT * FROM tenant_requests ORDER BY created_at DESC", values: [] };
+ const result = await pool.query(query);
+ return result.rows.map(mapRow);
+}
+
+export async function updateTenantRequestStatus(
+ id: string,
+ status: TenantRequestStatus,
+ extra?: { adminNotes?: string; tenantName?: string }
+): Promise {
+ await ensureSchema();
+ const result = await getPool().query(
+ `UPDATE tenant_requests
+ SET status = $1, admin_notes = COALESCE($2, admin_notes),
+ tenant_name = COALESCE($3, tenant_name), updated_at = now()
+ WHERE id = $4
+ RETURNING *`,
+ [status, extra?.adminNotes ?? null, extra?.tenantName ?? null, id]
+ );
+ if (!result.rows[0]) throw new Error(`TenantRequest ${id} not found`);
+ return mapRow(result.rows[0]);
+}
+
+// ---------------------------------------------------------------------------
+// Row mapping (snake_case → camelCase)
+// ---------------------------------------------------------------------------
+
+function mapRow(row: any): TenantRequest {
+ return {
+ id: row.id,
+ zitadelOrgId: row.zitadel_org_id,
+ zitadelUserId: row.zitadel_user_id,
+ companyName: row.company_name,
+ contactName: row.contact_name,
+ contactEmail: row.contact_email,
+ agentName: row.agent_name,
+ soulMd: row.soul_md,
+ packages: row.packages ?? [],
+ billingAddress: typeof row.billing_address === "string"
+ ? JSON.parse(row.billing_address)
+ : row.billing_address ?? {},
+ billingNotes: row.billing_notes,
+ status: row.status as TenantRequestStatus,
+ adminNotes: row.admin_notes,
+ tenantName: row.tenant_name,
+ createdAt: row.created_at?.toISOString?.() ?? row.created_at,
+ updatedAt: row.updated_at?.toISOString?.() ?? row.updated_at,
+ };
+}
diff --git a/src/lib/zitadel.ts b/src/lib/zitadel.ts
new file mode 100644
index 0000000..a9b35b4
--- /dev/null
+++ b/src/lib/zitadel.ts
@@ -0,0 +1,300 @@
+/**
+ * ZITADEL API client for portal-driven registration (Option B).
+ *
+ * Uses v2 APIs:
+ * - OrganizationService: POST /v2/organizations
+ * - UserService: POST /v2/users/new + POST /v2/users/{id}/invite
+ * - ProjectService: Connect RPC CreateProjectGrant
+ * - AuthorizationService: Connect RPC CreateAuthorization
+ *
+ * Registration flow (invite-based):
+ * 1. Create Org
+ * 2. Create User (no password, email unverified)
+ * 3. Send invite → ZITADEL emails a link to set password + verify email
+ * 4. Create Project Grant
+ * 5. Create Authorization (role assignment)
+ *
+ * Auth: pieced-sa PAT (Personal Access Token) — passed as Bearer token.
+ * The SA must have IAM_OWNER role to create orgs cross-tenant.
+ */
+
+const ZITADEL_URL = process.env.ZITADEL_ISSUER!; // https://auth.pieced.ch
+const ZITADEL_SA_PAT = process.env.ZITADEL_SA_PAT!;
+const ZITADEL_PROJECT_ID = process.env.ZITADEL_PROJECT_ID!; // 367435120493199793
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+async function zitadelFetch(
+ path: string,
+ method: string = "GET",
+ body?: unknown,
+ headers?: Record
+): Promise {
+ const url = `${ZITADEL_URL}${path}`;
+ const res = await fetch(url, {
+ method,
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ Authorization: `Bearer ${ZITADEL_SA_PAT}`,
+ ...headers,
+ },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ const err = new Error(`ZITADEL ${method} ${path}: ${res.status} ${text}`);
+ (err as any).statusCode = res.status;
+ throw err;
+ }
+ return res.json() as Promise;
+}
+
+/**
+ * Connect RPC call — ZITADEL v2 services use Connect protocol.
+ * Same as REST but requires Connect-Protocol-Version header.
+ */
+async function connectRpc(
+ service: string,
+ method: string,
+ body: unknown
+): Promise {
+ return zitadelFetch(
+ `/${service}/${method}`,
+ "POST",
+ body,
+ { "Connect-Protocol-Version": "1" }
+ );
+}
+
+// ---------------------------------------------------------------------------
+// v2 Organization API — REST
+// ---------------------------------------------------------------------------
+
+export interface CreateOrgResult {
+ organizationId: string;
+ creationDate: string;
+}
+
+export async function createOrganization(
+ name: string
+): Promise {
+ return zitadelFetch("/v2/organizations", "POST", {
+ name,
+ });
+}
+
+// ---------------------------------------------------------------------------
+// v2 User API — REST
+// ---------------------------------------------------------------------------
+
+export interface CreateUserResult {
+ id: string;
+ creationDate: string;
+ emailCode?: string;
+}
+
+/**
+ * Create a human user in a specific organization WITHOUT a password.
+ * The user cannot log in until they complete the invite flow
+ * (set password + verify email via the link in the invite email).
+ *
+ * POST /v2/users/new
+ */
+export async function createHumanUser(params: {
+ orgId: string;
+ email: string;
+ givenName: string;
+ familyName: string;
+ preferredLanguage?: string;
+}): Promise {
+ return zitadelFetch("/v2/users/new", "POST", {
+ organizationId: params.orgId,
+ human: {
+ profile: {
+ givenName: params.givenName,
+ familyName: params.familyName,
+ displayName: `${params.givenName} ${params.familyName}`,
+ preferredLanguage: params.preferredLanguage || "en",
+ },
+ email: {
+ email: params.email,
+ // Not verified — invite flow will handle verification
+ },
+ },
+ // No password — user sets it via invite link
+ });
+}
+
+/**
+ * Send an invitation email to the user.
+ * The email contains a link where the user sets their password
+ * (or passkey/IdP) and verifies their email address in one step.
+ *
+ * Requires SMTP to be configured in ZITADEL.
+ * If SMTP is not configured, this call succeeds but no email is sent.
+ *
+ * POST /v2/users/{userId}/invite
+ */
+export async function createInviteCode(userId: string): Promise {
+ await zitadelFetch(`/v2/users/${userId}/invite`, "POST", {
+ sendCode: {},
+ });
+}
+
+// ---------------------------------------------------------------------------
+// v2 Project API — Connect RPC
+// ---------------------------------------------------------------------------
+
+export interface ProjectGrantResult {
+ projectGrantId: string;
+ creationDate: string;
+}
+
+/**
+ * Grant the "OpenClaw Platform" project to a customer organization.
+ * Connect RPC: zitadel.project.v2.ProjectService/CreateProjectGrant
+ */
+export async function createProjectGrant(
+ grantedOrgId: string,
+ roleKeys?: string[]
+): Promise {
+ return connectRpc(
+ "zitadel.project.v2.ProjectService",
+ "CreateProjectGrant",
+ {
+ projectId: ZITADEL_PROJECT_ID,
+ grantedOrganizationId: grantedOrgId,
+ roleKeys: roleKeys || ["owner"],
+ }
+ );
+}
+
+// ---------------------------------------------------------------------------
+// v2 Authorization API — Connect RPC
+// ---------------------------------------------------------------------------
+
+export interface AuthorizationResult {
+ id: string;
+ creationDate: string;
+}
+
+/**
+ * Create a role assignment (authorization) for a user.
+ * This makes the role appear in the JWT claims.
+ * Connect RPC: zitadel.authorization.v2.AuthorizationService/CreateAuthorization
+ */
+export async function createAuthorization(params: {
+ userId: string;
+ projectId?: string;
+ organizationId: string;
+ roleKeys?: string[];
+}): Promise {
+ return connectRpc(
+ "zitadel.authorization.v2.AuthorizationService",
+ "CreateAuthorization",
+ {
+ userId: params.userId,
+ projectId: params.projectId || ZITADEL_PROJECT_ID,
+ organizationId: params.organizationId,
+ roleKeys: params.roleKeys || ["owner"],
+ }
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Delete Organization (for rollback on partial failure)
+// ---------------------------------------------------------------------------
+
+export async function deleteOrganization(orgId: string): Promise {
+ await zitadelFetch(`/v2/organizations/${orgId}`, "DELETE");
+}
+
+// ---------------------------------------------------------------------------
+// Full registration flow
+// ---------------------------------------------------------------------------
+
+export interface RegistrationResult {
+ orgId: string;
+ userId: string;
+ projectGrantId: string;
+}
+
+/**
+ * Complete registration flow:
+ * 1. Create ZITADEL Org
+ * 2. Create Human User (no password, email unverified)
+ * 3. Send invite code (ZITADEL emails link to set password + verify email)
+ * 4. Create Project Grant (link OpenClaw Platform project to new org)
+ * 5. Create Authorization (assign "owner" role to user)
+ *
+ * If any step after org creation fails, the org is deleted (rollback).
+ */
+export async function registerCustomer(params: {
+ companyName: string;
+ email: string;
+ givenName: string;
+ familyName: string;
+ preferredLanguage?: string;
+}): Promise {
+ // 1. Create org
+ const org = await createOrganization(params.companyName);
+
+ try {
+ // 2. Create user in org (no password)
+ const user = await createHumanUser({
+ orgId: org.organizationId,
+ email: params.email,
+ givenName: params.givenName,
+ familyName: params.familyName,
+ preferredLanguage: params.preferredLanguage,
+ });
+
+ // 3. Send invite — user receives email to set password + verify email
+ try {
+ await createInviteCode(user.id);
+ } catch (inviteErr) {
+ // Log but don't fail — SMTP may not be configured yet.
+ // Admin can resend the invite later from ZITADEL console.
+ console.warn(
+ `Invite email could not be sent for user ${user.id} (SMTP may not be configured):`,
+ inviteErr
+ );
+ }
+
+ // 4. Grant project to org
+ const grant = await createProjectGrant(org.organizationId, ["owner"]);
+
+ // 5. Assign "owner" role to user
+ await createAuthorization({
+ userId: user.id,
+ organizationId: org.organizationId,
+ roleKeys: ["owner"],
+ });
+
+ return {
+ orgId: org.organizationId,
+ userId: user.id,
+ projectGrantId: grant.projectGrantId,
+ };
+ } catch (err) {
+ // Rollback: delete the org so the customer can retry
+ console.error(
+ `Registration failed after org creation (${org.organizationId}), rolling back:`,
+ err
+ );
+ try {
+ await deleteOrganization(org.organizationId);
+ console.log(`Rolled back org ${org.organizationId}`);
+ } catch (rollbackErr) {
+ console.error(
+ `Failed to rollback org ${org.organizationId}:`,
+ rollbackErr
+ );
+ }
+ throw err;
+ }
+}
diff --git a/src/messages/de.json b/src/messages/de.json
index 8ebac98..1658693 100644
--- a/src/messages/de.json
+++ b/src/messages/de.json
@@ -10,13 +10,72 @@
"language": "Sprache",
"cancel": "Abbrechen",
"save": "Speichern",
- "error": "Ein Fehler ist aufgetreten"
+ "error": "Ein Fehler ist aufgetreten",
+ "register": "Registrieren"
},
"login": {
"title": "PieCed Portal",
- "subtitle": "Melde dich an, um deinen KI-Assistenten zu verwalten",
+ "subtitle": "Melden Sie sich an, um Ihren KI-Assistenten zu verwalten",
"button": "Weiter mit ZITADEL",
- "footer": "On-Premises gehostet in der Schweiz"
+ "footer": "On-Premises gehostet in der Schweiz",
+ "noAccount": "Noch kein Konto?",
+ "register": "Firma registrieren"
+ },
+ "register": {
+ "title": "Konto erstellen",
+ "subtitle": "Registrieren Sie Ihre Firma für einen in der Schweiz gehosteten KI-Assistenten",
+ "companyName": "Firmenname",
+ "companyNamePlaceholder": "Muster GmbH",
+ "givenName": "Vorname",
+ "familyName": "Nachname",
+ "email": "E-Mail-Adresse",
+ "submit": "Registrieren",
+ "hasAccount": "Bereits ein Konto?",
+ "footer": "Ihre Daten werden ausschliesslich On-Premises in der Schweiz gehostet.",
+ "successTitle": "Registrierung eingegangen",
+ "successDescription": "Sie erhalten eine Einladungs-E-Mail mit einem Link, um Ihr Passwort festzulegen und Ihre E-Mail-Adresse zu bestätigen. Danach können Sie sich anmelden und Ihren KI-Assistenten einrichten.",
+ "goToLogin": "Zur Anmeldung"
+ },
+ "onboarding": {
+ "loading": "Status wird geladen…",
+ "welcomeTitle": "KI-Assistenten einrichten",
+ "welcomeDescription": "In wenigen Schritten erhalten Sie Ihren eigenen KI-Assistenten — ausschliesslich in der Schweiz gehostet, vollständig unter Ihrer Kontrolle.",
+ "welcomeFeature_swissHosted": "On-Premises in der Schweiz gehostet — Ihre Daten verlassen nie das Land",
+ "welcomeFeature_privacy": "Keine Datenweitergabe an Dritte — vollständiger Datenschutz",
+ "welcomeFeature_customizable": "Vollständig anpassbare Persönlichkeit, Pakete und Integrationen",
+ "getStarted": "Loslegen",
+ "configureTitle": "Assistenten konfigurieren",
+ "configureDescription": "Geben Sie Ihrem Assistenten einen Namen und eine Persönlichkeit. Sie können dies jederzeit ändern.",
+ "agentName": "Agent-Name",
+ "soulMd": "Persönlichkeit (SOUL.md)",
+ "soulMdHint": "Definiert das Verhalten Ihres Assistenten. Markdown-Format. Kann später bearbeitet werden.",
+ "packages": "Pakete",
+ "packagesHint": "Optionale Integrationen. Können auch später aktiviert werden.",
+ "billingTitle": "Rechnungsinformationen",
+ "billingDescription": "Wir benötigen Ihre Rechnungsadresse für die Fakturierung. Ein Zahlungsanbieter wird zukünftig integriert.",
+ "billingCompany": "Firma",
+ "billingStreet": "Strasse",
+ "billingPostalCode": "PLZ",
+ "billingCity": "Ort",
+ "billingCountry": "Land",
+ "billingNotes": "Bemerkungen",
+ "billingNotesPlaceholder": "Bemerkungen zur Rechnung (Bestellnummer, MWST-Nr., bevorzugte Zahlungsart usw.)",
+ "confirmTitle": "Überprüfen & absenden",
+ "confirmDescription": "Bitte überprüfen Sie Ihre Einstellungen. Ihr Antrag wird von unserem Team geprüft, bevor die Bereitstellung beginnt.",
+ "confirmNote": "Nach dem Absenden prüft unser Team Ihren Antrag und die Rechnungsangaben. Sie erhalten Zugang nach Genehmigung — normalerweise innerhalb eines Werktages.",
+ "submitRequest": "Antrag absenden",
+ "back": "Zurück",
+ "next": "Weiter",
+ "pendingTitle": "Antrag eingereicht",
+ "pendingDescription": "Ihr Antrag wurde eingereicht und wird von unserem Team geprüft. Sie erhalten Zugang nach Genehmigung — normalerweise innerhalb eines Werktages.",
+ "rejectedTitle": "Antrag nicht genehmigt",
+ "rejectedDescription": "Leider wurde Ihr Antrag nicht genehmigt. Bitte kontaktieren Sie uns für weitere Informationen.",
+ "provisioningTitle": "Instanz wird eingerichtet",
+ "provisioningDescription": "Ihr KI-Assistent wird bereitgestellt. Dies dauert in der Regel wenige Minuten.",
+ "phase": "Phase",
+ "readyTitle": "Ihr Assistent ist bereit!",
+ "readyDescription": "Ihr KI-Assistent wurde bereitgestellt und ist aktiv. Sie können ihn nun über das Dashboard verwalten.",
+ "goToDashboard": "Zum Dashboard"
},
"dashboard": {
"title": "Dashboard",
@@ -40,13 +99,24 @@
"phase": "Phase",
"packages": "Pakete",
"created": "Erstellt",
- "manage": "Verwalten"
+ "manage": "Verwalten",
+ "requests": "Onboarding-Anträge",
+ "pendingRequests": "Offene Anträge",
+ "approve": "Genehmigen",
+ "reject": "Ablehnen",
+ "company": "Firma",
+ "contact": "Kontakt",
+ "status": "Status",
+ "submitted": "Eingereicht",
+ "noRequests": "Keine offenen Anträge.",
+ "approveConfirm": "Diesen Antrag genehmigen und Bereitstellung starten?",
+ "rejectConfirm": "Diesen Antrag ablehnen?"
},
"tenantDetail": {
"agent": "Agent",
"packages": "Pakete",
"workspaceFiles": "Workspace-Dateien",
- "notFound": "Mandant nicht gefunden.",
+ "notFound": "Tenant nicht gefunden.",
"usage": "Nutzung & Kosten"
},
"usage": {
@@ -64,15 +134,15 @@
"workspace": {
"save": "Speichern",
"placeholder": "Inhalt für {file} eingeben…",
- "seedingNote": "Workspace-Dateien werden beim ersten Start eingerichtet. Aktualisierung löst ConfigMap-Update und Pod-Neustart aus."
+ "seedingNote": "Workspace-Dateien werden beim ersten Start geladen. Eine Aktualisierung auf einer bestehenden Instanz löst ein ConfigMap-Update und Pod-Neustart aus."
},
"packages": {
"enable": "Aktivieren",
"disable": "Deaktivieren",
"enableAndSave": "Aktivieren & Speichern",
"configure": "Konfigurieren",
- "requiresApiKey": "API-Schlüssel erforderlich",
- "missingFields": "Bitte füllen Sie alle Pflichtfelder aus.",
+ "requiresApiKey": "Erfordert API-Schlüssel",
+ "missingFields": "Bitte füllen Sie alle erforderlichen Felder aus.",
"status": {
"pending": "Ausstehend",
"active": "Aktiv",
@@ -80,30 +150,30 @@
},
"telegram": {
"description": "Verbinden Sie Ihren KI-Assistenten mit einem Telegram-Bot.",
- "botTokenLabel": "Telegram Bot-Token",
+ "botTokenLabel": "Telegram Bot Token",
"botTokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"instructions": "1. Öffnen Sie @BotFather auf Telegram\n2. Senden Sie /newbot und folgen Sie den Anweisungen\n3. Kopieren Sie den Bot-Token",
- "disclaimer": "Ich bestätige, dass ich Eigentümer dieses Telegram-Bots bin und PieCed IT autorisiere, ihn mit meiner Instanz zu verbinden."
+ "disclaimer": "Ich bestätige, dass ich diesen Telegram-Bot besitze und PieCed IT autorisiere, ihn mit meinem KI-Assistenten zu verbinden."
},
"discord": {
- "description": "Verbinden Sie Ihren KI-Assistenten über einen Bot mit Discord.",
- "botTokenLabel": "Discord Bot-Token",
+ "description": "Verbinden Sie Ihren KI-Assistenten mit einem Discord-Server über einen Bot.",
+ "botTokenLabel": "Discord Bot Token",
"botTokenPlaceholder": "MTAxNjQ0OTk2NjAz...",
- "instructions": "1. Gehen Sie zu discord.com/developers/applications\n2. Erstellen Sie eine Anwendung und fügen Sie einen Bot hinzu\n3. Kopieren Sie den Bot-Token",
- "disclaimer": "Ich bestätige, dass ich Eigentümer dieses Discord-Bots bin und PieCed IT autorisiere, ihn mit meiner Instanz zu verbinden."
+ "instructions": "1. Gehen Sie zu discord.com/developers/applications\n2. Erstellen Sie eine neue Anwendung und fügen Sie einen Bot hinzu\n3. Kopieren Sie den Bot-Token",
+ "disclaimer": "Ich bestätige, dass ich diesen Discord-Bot besitze und PieCed IT autorisiere, ihn mit meinem KI-Assistenten zu verbinden."
},
"email": {
- "description": "Ermöglichen Sie Ihrem KI-Assistenten E-Mails zu senden und empfangen.",
- "smtpHostLabel": "SMTP-Host",
+ "description": "Ermöglichen Sie Ihrem KI-Assistenten, E-Mails zu senden und zu empfangen.",
+ "smtpHostLabel": "SMTP Host",
"smtpHostPlaceholder": "smtp.example.com",
- "smtpUserLabel": "SMTP-Benutzername",
+ "smtpUserLabel": "SMTP Benutzername",
"smtpUserPlaceholder": "user@example.com",
- "smtpPasswordLabel": "SMTP-Passwort",
+ "smtpPasswordLabel": "SMTP Passwort",
"smtpPasswordPlaceholder": "••••••••",
- "imapHostLabel": "IMAP-Host",
+ "imapHostLabel": "IMAP Host",
"imapHostPlaceholder": "imap.example.com",
- "instructions": "Geben Sie SMTP- und IMAP-Zugangsdaten an. Der Assistent nutzt diese zum Senden und Empfangen.",
- "disclaimer": "Ich bestätige, dass ich berechtigt bin, diese Zugangsdaten zu verwenden und PieCed IT auf dieses Postfach zugreifen darf."
+ "instructions": "Geben Sie SMTP- und IMAP-Zugangsdaten an. Der Assistent nutzt diese zum Senden und Empfangen von Nachrichten.",
+ "disclaimer": "Ich bestätige, dass ich berechtigt bin, diese E-Mail-Zugangsdaten zu verwenden und dass PieCed IT auf dieses Postfach zugreifen darf."
},
"webSearch": {
"description": "Geben Sie Ihrem KI-Assistenten die Möglichkeit, im Web zu suchen."
diff --git a/src/messages/en.json b/src/messages/en.json
index 548ec4b..cec0a52 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -10,13 +10,72 @@
"language": "Language",
"cancel": "Cancel",
"save": "Save",
- "error": "An error occurred"
+ "error": "An error occurred",
+ "register": "Register"
},
"login": {
"title": "PieCed Portal",
"subtitle": "Sign in to manage your AI assistant",
"button": "Continue with ZITADEL",
- "footer": "Hosted on-premises in Switzerland"
+ "footer": "Hosted on-premises in Switzerland",
+ "noAccount": "No account yet?",
+ "register": "Register your company"
+ },
+ "register": {
+ "title": "Create your account",
+ "subtitle": "Register your company for a Swiss-hosted AI assistant",
+ "companyName": "Company Name",
+ "companyNamePlaceholder": "Acme GmbH",
+ "givenName": "First Name",
+ "familyName": "Last Name",
+ "email": "Email Address",
+ "submit": "Register",
+ "hasAccount": "Already have an account?",
+ "footer": "Your data is hosted exclusively on-premises in Switzerland.",
+ "successTitle": "Registration received",
+ "successDescription": "You will receive an invitation email with a link to set your password and verify your email address. Once completed, you can sign in to set up your AI assistant.",
+ "goToLogin": "Go to Sign In"
+ },
+ "onboarding": {
+ "loading": "Loading status…",
+ "welcomeTitle": "Set up your AI assistant",
+ "welcomeDescription": "In a few steps, you'll have your own AI assistant — hosted exclusively in Switzerland, fully under your control.",
+ "welcomeFeature_swissHosted": "Hosted on-premises in Switzerland — your data never leaves the country",
+ "welcomeFeature_privacy": "No data shared with third parties — complete privacy",
+ "welcomeFeature_customizable": "Fully customizable personality, packages, and integrations",
+ "getStarted": "Get started",
+ "configureTitle": "Configure your assistant",
+ "configureDescription": "Give your assistant a name and personality. You can always change this later.",
+ "agentName": "Agent Name",
+ "soulMd": "Personality (SOUL.md)",
+ "soulMdHint": "This defines how your assistant behaves. Markdown format. You can edit this later.",
+ "packages": "Packages",
+ "packagesHint": "Optional integrations. You can enable these later too.",
+ "billingTitle": "Billing information",
+ "billingDescription": "We need your billing address to set up invoicing. A payment provider will be integrated in the future.",
+ "billingCompany": "Company",
+ "billingStreet": "Street",
+ "billingPostalCode": "Postal Code",
+ "billingCity": "City",
+ "billingCountry": "Country",
+ "billingNotes": "Notes",
+ "billingNotesPlaceholder": "Any notes about billing (PO number, VAT ID, preferred payment method, etc.)",
+ "confirmTitle": "Review & submit",
+ "confirmDescription": "Please review your setup. Your request will be reviewed by our team before provisioning.",
+ "confirmNote": "After submission, our team will review your request and billing details. You'll receive access once approved — typically within one business day.",
+ "submitRequest": "Submit request",
+ "back": "Back",
+ "next": "Next",
+ "pendingTitle": "Request submitted",
+ "pendingDescription": "Your onboarding request has been submitted and is awaiting review by our team. You'll receive access once approved — typically within one business day.",
+ "rejectedTitle": "Request not approved",
+ "rejectedDescription": "Unfortunately, your onboarding request was not approved. Please contact us for more information.",
+ "provisioningTitle": "Setting up your instance",
+ "provisioningDescription": "Your AI assistant is being provisioned. This usually takes a few minutes.",
+ "phase": "Phase",
+ "readyTitle": "Your assistant is ready!",
+ "readyDescription": "Your AI assistant has been provisioned and is running. You can now manage it from the dashboard.",
+ "goToDashboard": "Go to Dashboard"
},
"dashboard": {
"title": "Dashboard",
@@ -40,7 +99,18 @@
"phase": "Phase",
"packages": "Packages",
"created": "Created",
- "manage": "Manage"
+ "manage": "Manage",
+ "requests": "Onboarding Requests",
+ "pendingRequests": "Pending Requests",
+ "approve": "Approve",
+ "reject": "Reject",
+ "company": "Company",
+ "contact": "Contact",
+ "status": "Status",
+ "submitted": "Submitted",
+ "noRequests": "No pending requests.",
+ "approveConfirm": "Approve this request and start provisioning?",
+ "rejectConfirm": "Reject this request?"
},
"tenantDetail": {
"agent": "Agent",
diff --git a/src/messages/fr.json b/src/messages/fr.json
index 7c5b8a2..164a11e 100644
--- a/src/messages/fr.json
+++ b/src/messages/fr.json
@@ -2,32 +2,91 @@
"common": {
"appName": "PieCed",
"tagline": "Plateforme IA",
- "login": "Connexion",
- "logout": "Déconnexion",
+ "login": "Se connecter",
+ "logout": "Se déconnecter",
"dashboard": "Tableau de bord",
"admin": "Admin",
"loading": "Chargement…",
"language": "Langue",
"cancel": "Annuler",
"save": "Enregistrer",
- "error": "Une erreur est survenue"
+ "error": "Une erreur est survenue",
+ "register": "S'inscrire"
},
"login": {
- "title": "PieCed Portal",
+ "title": "Portail PieCed",
"subtitle": "Connectez-vous pour gérer votre assistant IA",
"button": "Continuer avec ZITADEL",
- "footer": "Hébergé sur site en Suisse"
+ "footer": "Hébergé on-premises en Suisse",
+ "noAccount": "Pas encore de compte ?",
+ "register": "Inscrivez votre entreprise"
+ },
+ "register": {
+ "title": "Créer votre compte",
+ "subtitle": "Inscrivez votre entreprise pour un assistant IA hébergé en Suisse",
+ "companyName": "Nom de l'entreprise",
+ "companyNamePlaceholder": "Acme SA",
+ "givenName": "Prénom",
+ "familyName": "Nom",
+ "email": "Adresse e-mail",
+ "submit": "S'inscrire",
+ "hasAccount": "Vous avez déjà un compte ?",
+ "footer": "Vos données sont hébergées exclusivement on-premises en Suisse.",
+ "successTitle": "Inscription reçue",
+ "successDescription": "Vous recevrez un e-mail d'invitation avec un lien pour définir votre mot de passe et vérifier votre adresse e-mail. Une fois terminé, vous pourrez vous connecter pour configurer votre assistant IA.",
+ "goToLogin": "Aller à la connexion"
+ },
+ "onboarding": {
+ "loading": "Chargement du statut…",
+ "welcomeTitle": "Configurez votre assistant IA",
+ "welcomeDescription": "En quelques étapes, vous aurez votre propre assistant IA — hébergé exclusivement en Suisse, entièrement sous votre contrôle.",
+ "welcomeFeature_swissHosted": "Hébergé on-premises en Suisse — vos données ne quittent jamais le pays",
+ "welcomeFeature_privacy": "Aucune donnée partagée avec des tiers — confidentialité totale",
+ "welcomeFeature_customizable": "Personnalité, paquets et intégrations entièrement personnalisables",
+ "getStarted": "Commencer",
+ "configureTitle": "Configurer votre assistant",
+ "configureDescription": "Donnez un nom et une personnalité à votre assistant. Vous pourrez toujours modifier cela plus tard.",
+ "agentName": "Nom de l'agent",
+ "soulMd": "Personnalité (SOUL.md)",
+ "soulMdHint": "Définit le comportement de votre assistant. Format Markdown. Modifiable ultérieurement.",
+ "packages": "Paquets",
+ "packagesHint": "Intégrations optionnelles. Vous pouvez aussi les activer plus tard.",
+ "billingTitle": "Informations de facturation",
+ "billingDescription": "Nous avons besoin de votre adresse de facturation. Un prestataire de paiement sera intégré à l'avenir.",
+ "billingCompany": "Entreprise",
+ "billingStreet": "Rue",
+ "billingPostalCode": "Code postal",
+ "billingCity": "Ville",
+ "billingCountry": "Pays",
+ "billingNotes": "Remarques",
+ "billingNotesPlaceholder": "Remarques concernant la facturation (numéro de commande, TVA, mode de paiement préféré, etc.)",
+ "confirmTitle": "Vérifier et envoyer",
+ "confirmDescription": "Veuillez vérifier votre configuration. Votre demande sera examinée par notre équipe avant la mise en service.",
+ "confirmNote": "Après l'envoi, notre équipe examinera votre demande et vos informations de facturation. Vous recevrez l'accès après approbation — généralement dans un délai d'un jour ouvrable.",
+ "submitRequest": "Envoyer la demande",
+ "back": "Retour",
+ "next": "Suivant",
+ "pendingTitle": "Demande envoyée",
+ "pendingDescription": "Votre demande d'intégration a été envoyée et est en attente d'examen par notre équipe. Vous recevrez l'accès après approbation — généralement dans un délai d'un jour ouvrable.",
+ "rejectedTitle": "Demande non approuvée",
+ "rejectedDescription": "Malheureusement, votre demande n'a pas été approuvée. Veuillez nous contacter pour plus d'informations.",
+ "provisioningTitle": "Configuration de votre instance",
+ "provisioningDescription": "Votre assistant IA est en cours de mise en service. Cela prend généralement quelques minutes.",
+ "phase": "Phase",
+ "readyTitle": "Votre assistant est prêt !",
+ "readyDescription": "Votre assistant IA a été mis en service et est actif. Vous pouvez maintenant le gérer depuis le tableau de bord.",
+ "goToDashboard": "Aller au tableau de bord"
},
"dashboard": {
"title": "Tableau de bord",
"welcome": "Bienvenue, {name}",
- "instanceStatus": "État de l'instance",
+ "instanceStatus": "Statut de l'instance",
"usage": "Utilisation",
"packages": "Paquets",
"noInstance": "Aucune instance provisionnée.",
"comingSoon": "Vue détaillée à venir dans la Session 6.2",
- "noInstanceDescription": "Configurez votre instance d'assistant IA pour démarrer avec PieCed IT.",
- "manage": "Gérer l'instance & les paquets"
+ "noInstanceDescription": "Configurez votre instance d'assistant IA pour commencer avec PieCed IT.",
+ "manage": "Gérer l'instance et les paquets"
},
"admin": {
"title": "Admin plateforme",
@@ -40,14 +99,25 @@
"phase": "Phase",
"packages": "Paquets",
"created": "Créé",
- "manage": "Gérer"
+ "manage": "Gérer",
+ "requests": "Demandes d'intégration",
+ "pendingRequests": "Demandes en attente",
+ "approve": "Approuver",
+ "reject": "Refuser",
+ "company": "Entreprise",
+ "contact": "Contact",
+ "status": "Statut",
+ "submitted": "Envoyé",
+ "noRequests": "Aucune demande en attente.",
+ "approveConfirm": "Approuver cette demande et lancer la mise en service ?",
+ "rejectConfirm": "Refuser cette demande ?"
},
"tenantDetail": {
"agent": "Agent",
"packages": "Paquets",
- "workspaceFiles": "Fichiers Workspace",
+ "workspaceFiles": "Fichiers workspace",
"notFound": "Tenant introuvable.",
- "usage": "Utilisation & Dépenses"
+ "usage": "Utilisation et dépenses"
},
"usage": {
"inputTokens": "Tokens d'entrée",
@@ -55,21 +125,21 @@
"totalSpend": "Dépenses totales",
"totalCost": "Coût total",
"budget": "Budget",
- "noLimit": "Pas de limite",
+ "noLimit": "Aucune limite",
"last30Days": "30 derniers jours",
- "noData": "Aucune donnée d'utilisation.",
+ "noData": "Aucune donnée d'utilisation disponible.",
"dailyBreakdown": "Détail journalier",
"requests": "requêtes"
},
"workspace": {
"save": "Enregistrer",
- "placeholder": "Saisissez le contenu de {file}…",
- "seedingNote": "Les fichiers workspace sont initialisés au premier démarrage. La mise à jour déclenche un redémarrage du pod."
+ "placeholder": "Saisir le contenu pour {file}…",
+ "seedingNote": "Les fichiers workspace sont initialisés au premier démarrage. Une mise à jour sur une instance existante déclenche une mise à jour du ConfigMap et un redémarrage du pod."
},
"packages": {
"enable": "Activer",
"disable": "Désactiver",
- "enableAndSave": "Activer & Enregistrer",
+ "enableAndSave": "Activer et enregistrer",
"configure": "Configurer",
"requiresApiKey": "Clé API requise",
"missingFields": "Veuillez remplir tous les champs obligatoires.",
@@ -82,31 +152,31 @@
"description": "Connectez votre assistant IA à un bot Telegram.",
"botTokenLabel": "Token du bot Telegram",
"botTokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
- "instructions": "1. Ouvrez @BotFather sur Telegram\n2. Envoyez /newbot et suivez les instructions\n3. Copiez le token fourni",
+ "instructions": "1. Ouvrez @BotFather sur Telegram\n2. Envoyez /newbot et suivez les instructions\n3. Copiez le token du bot fourni",
"disclaimer": "Je confirme être propriétaire de ce bot Telegram et autorise PieCed IT à le connecter à mon assistant IA."
},
"discord": {
- "description": "Connectez votre assistant IA à Discord via un bot.",
+ "description": "Connectez votre assistant IA à un serveur Discord via un bot.",
"botTokenLabel": "Token du bot Discord",
"botTokenPlaceholder": "MTAxNjQ0OTk2NjAz...",
- "instructions": "1. Allez sur discord.com/developers/applications\n2. Créez une application et ajoutez un bot\n3. Copiez le token",
+ "instructions": "1. Allez sur discord.com/developers/applications\n2. Créez une nouvelle application et ajoutez un bot\n3. Copiez le token du bot",
"disclaimer": "Je confirme être propriétaire de ce bot Discord et autorise PieCed IT à le connecter à mon assistant IA."
},
"email": {
- "description": "Permettez à votre assistant IA d'envoyer et recevoir des e-mails.",
+ "description": "Permettez à votre assistant IA d'envoyer et de recevoir des e-mails.",
"smtpHostLabel": "Hôte SMTP",
"smtpHostPlaceholder": "smtp.example.com",
- "smtpUserLabel": "Utilisateur SMTP",
+ "smtpUserLabel": "Nom d'utilisateur SMTP",
"smtpUserPlaceholder": "user@example.com",
"smtpPasswordLabel": "Mot de passe SMTP",
"smtpPasswordPlaceholder": "••••••••",
"imapHostLabel": "Hôte IMAP",
"imapHostPlaceholder": "imap.example.com",
- "instructions": "Fournissez les identifiants SMTP et IMAP pour l'envoi et la réception de messages.",
- "disclaimer": "Je confirme être autorisé(e) à utiliser ces identifiants et que PieCed IT peut accéder à cette boîte."
+ "instructions": "Fournissez les identifiants SMTP et IMAP. L'assistant les utilise pour envoyer et surveiller les messages.",
+ "disclaimer": "Je confirme être autorisé à utiliser ces identifiants e-mail et que PieCed IT peut accéder à cette boîte mail."
},
"webSearch": {
- "description": "Donnez à votre assistant IA la possibilité de rechercher sur le web."
+ "description": "Donnez à votre assistant IA la capacité de rechercher sur le web."
},
"documentProcessing": {
"description": "Activez l'analyse, le résumé et l'extraction de documents."
diff --git a/src/messages/it.json b/src/messages/it.json
index 829e740..5880048 100644
--- a/src/messages/it.json
+++ b/src/messages/it.json
@@ -10,13 +10,72 @@
"language": "Lingua",
"cancel": "Annulla",
"save": "Salva",
- "error": "Si è verificato un errore"
+ "error": "Si è verificato un errore",
+ "register": "Registrati"
},
"login": {
- "title": "PieCed Portal",
+ "title": "Portale PieCed",
"subtitle": "Accedi per gestire il tuo assistente IA",
"button": "Continua con ZITADEL",
- "footer": "Ospitato on-premises in Svizzera"
+ "footer": "Ospitato on-premises in Svizzera",
+ "noAccount": "Non hai ancora un account?",
+ "register": "Registra la tua azienda"
+ },
+ "register": {
+ "title": "Crea il tuo account",
+ "subtitle": "Registra la tua azienda per un assistente IA ospitato in Svizzera",
+ "companyName": "Nome dell'azienda",
+ "companyNamePlaceholder": "Acme SA",
+ "givenName": "Nome",
+ "familyName": "Cognome",
+ "email": "Indirizzo e-mail",
+ "submit": "Registrati",
+ "hasAccount": "Hai già un account?",
+ "footer": "I tuoi dati sono ospitati esclusivamente on-premises in Svizzera.",
+ "successTitle": "Registrazione ricevuta",
+ "successDescription": "Riceverai un'e-mail di invito con un link per impostare la password e verificare il tuo indirizzo e-mail. Una volta completato, potrai accedere per configurare il tuo assistente IA.",
+ "goToLogin": "Vai all'accesso"
+ },
+ "onboarding": {
+ "loading": "Caricamento dello stato…",
+ "welcomeTitle": "Configura il tuo assistente IA",
+ "welcomeDescription": "In pochi passaggi avrai il tuo assistente IA personale — ospitato esclusivamente in Svizzera, completamente sotto il tuo controllo.",
+ "welcomeFeature_swissHosted": "Ospitato on-premises in Svizzera — i tuoi dati non lasciano mai il paese",
+ "welcomeFeature_privacy": "Nessun dato condiviso con terzi — privacy completa",
+ "welcomeFeature_customizable": "Personalità, pacchetti e integrazioni completamente personalizzabili",
+ "getStarted": "Inizia",
+ "configureTitle": "Configura il tuo assistente",
+ "configureDescription": "Dai un nome e una personalità al tuo assistente. Puoi sempre modificarlo in seguito.",
+ "agentName": "Nome dell'agente",
+ "soulMd": "Personalità (SOUL.md)",
+ "soulMdHint": "Definisce il comportamento del tuo assistente. Formato Markdown. Modificabile in seguito.",
+ "packages": "Pacchetti",
+ "packagesHint": "Integrazioni opzionali. Puoi attivarle anche in seguito.",
+ "billingTitle": "Informazioni di fatturazione",
+ "billingDescription": "Abbiamo bisogno del tuo indirizzo di fatturazione. Un fornitore di pagamento verrà integrato in futuro.",
+ "billingCompany": "Azienda",
+ "billingStreet": "Via",
+ "billingPostalCode": "CAP",
+ "billingCity": "Città",
+ "billingCountry": "Paese",
+ "billingNotes": "Note",
+ "billingNotesPlaceholder": "Note sulla fatturazione (numero d'ordine, partita IVA, metodo di pagamento preferito, ecc.)",
+ "confirmTitle": "Verifica e invia",
+ "confirmDescription": "Verifica la tua configurazione. La tua richiesta verrà esaminata dal nostro team prima dell'attivazione.",
+ "confirmNote": "Dopo l'invio, il nostro team esaminerà la tua richiesta e i dati di fatturazione. Riceverai l'accesso dopo l'approvazione — di solito entro un giorno lavorativo.",
+ "submitRequest": "Invia richiesta",
+ "back": "Indietro",
+ "next": "Avanti",
+ "pendingTitle": "Richiesta inviata",
+ "pendingDescription": "La tua richiesta di attivazione è stata inviata ed è in attesa di revisione da parte del nostro team. Riceverai l'accesso dopo l'approvazione — di solito entro un giorno lavorativo.",
+ "rejectedTitle": "Richiesta non approvata",
+ "rejectedDescription": "Purtroppo la tua richiesta non è stata approvata. Contattaci per maggiori informazioni.",
+ "provisioningTitle": "Configurazione dell'istanza",
+ "provisioningDescription": "Il tuo assistente IA è in fase di attivazione. Di solito sono necessari pochi minuti.",
+ "phase": "Fase",
+ "readyTitle": "Il tuo assistente è pronto!",
+ "readyDescription": "Il tuo assistente IA è stato attivato ed è operativo. Puoi ora gestirlo dalla dashboard.",
+ "goToDashboard": "Vai alla dashboard"
},
"dashboard": {
"title": "Dashboard",
@@ -24,7 +83,7 @@
"instanceStatus": "Stato dell'istanza",
"usage": "Utilizzo",
"packages": "Pacchetti",
- "noInstance": "Nessuna istanza ancora provisioned.",
+ "noInstance": "Nessuna istanza ancora attivata.",
"comingSoon": "Vista dettagliata in arrivo nella Sessione 6.2",
"noInstanceDescription": "Configura la tua istanza di assistente IA per iniziare con PieCed IT.",
"manage": "Gestisci istanza e pacchetti"
@@ -33,21 +92,32 @@
"title": "Admin piattaforma",
"subtitle": "Tutti i tenant della piattaforma",
"allTenants": "Tenant",
- "noTenants": "Nessun tenant ancora provisionato.",
+ "noTenants": "Nessun tenant ancora attivato.",
"noAccess": "Permessi insufficienti per questa vista.",
"name": "Nome",
"displayName": "Nome visualizzato",
"phase": "Fase",
"packages": "Pacchetti",
"created": "Creato",
- "manage": "Gestisci"
+ "manage": "Gestisci",
+ "requests": "Richieste di attivazione",
+ "pendingRequests": "Richieste in sospeso",
+ "approve": "Approva",
+ "reject": "Rifiuta",
+ "company": "Azienda",
+ "contact": "Contatto",
+ "status": "Stato",
+ "submitted": "Inviato",
+ "noRequests": "Nessuna richiesta in sospeso.",
+ "approveConfirm": "Approvare questa richiesta e avviare l'attivazione?",
+ "rejectConfirm": "Rifiutare questa richiesta?"
},
"tenantDetail": {
"agent": "Agente",
"packages": "Pacchetti",
- "workspaceFiles": "File Workspace",
+ "workspaceFiles": "File workspace",
"notFound": "Tenant non trovato.",
- "usage": "Utilizzo & Spese"
+ "usage": "Utilizzo e spese"
},
"usage": {
"inputTokens": "Token di input",
@@ -57,24 +127,24 @@
"budget": "Budget",
"noLimit": "Nessun limite",
"last30Days": "Ultimi 30 giorni",
- "noData": "Nessun dato di utilizzo.",
+ "noData": "Nessun dato di utilizzo disponibile.",
"dailyBreakdown": "Dettaglio giornaliero",
"requests": "richieste"
},
"workspace": {
"save": "Salva",
"placeholder": "Inserisci il contenuto per {file}…",
- "seedingNote": "I file workspace vengono inizializzati al primo avvio. L'aggiornamento attiva un riavvio del pod."
+ "seedingNote": "I file workspace vengono inizializzati al primo avvio. Un aggiornamento su un'istanza esistente attiva un aggiornamento del ConfigMap e un riavvio del pod."
},
"packages": {
"enable": "Attiva",
"disable": "Disattiva",
- "enableAndSave": "Attiva & Salva",
+ "enableAndSave": "Attiva e salva",
"configure": "Configura",
"requiresApiKey": "Chiave API richiesta",
- "missingFields": "Compilare tutti i campi obbligatori.",
+ "missingFields": "Compila tutti i campi obbligatori.",
"status": {
- "pending": "In attesa",
+ "pending": "In sospeso",
"active": "Attivo",
"error": "Errore"
},
@@ -82,34 +152,34 @@
"description": "Collega il tuo assistente IA a un bot Telegram.",
"botTokenLabel": "Token del bot Telegram",
"botTokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
- "instructions": "1. Apri @BotFather su Telegram\n2. Invia /newbot e segui le istruzioni\n3. Copia il token del bot",
- "disclaimer": "Confermo di essere il proprietario di questo bot Telegram e autorizzo PieCed IT a collegarlo al mio assistente IA."
+ "instructions": "1. Apri @BotFather su Telegram\n2. Invia /newbot e segui le istruzioni\n3. Copia il token del bot fornito",
+ "disclaimer": "Confermo di essere proprietario di questo bot Telegram e autorizzo PieCed IT a collegarlo al mio assistente IA."
},
"discord": {
- "description": "Collega il tuo assistente IA a Discord tramite un bot.",
+ "description": "Collega il tuo assistente IA a un server Discord tramite un bot.",
"botTokenLabel": "Token del bot Discord",
"botTokenPlaceholder": "MTAxNjQ0OTk2NjAz...",
- "instructions": "1. Vai su discord.com/developers/applications\n2. Crea un'applicazione e aggiungi un bot\n3. Copia il token del bot",
- "disclaimer": "Confermo di essere il proprietario di questo bot Discord e autorizzo PieCed IT a collegarlo al mio assistente IA."
+ "instructions": "1. Vai su discord.com/developers/applications\n2. Crea una nuova applicazione e aggiungi un bot\n3. Copia il token del bot",
+ "disclaimer": "Confermo di essere proprietario di questo bot Discord e autorizzo PieCed IT a collegarlo al mio assistente IA."
},
"email": {
- "description": "Consenti al tuo assistente IA di inviare e ricevere e-mail.",
+ "description": "Permetti al tuo assistente IA di inviare e ricevere e-mail.",
"smtpHostLabel": "Host SMTP",
"smtpHostPlaceholder": "smtp.example.com",
- "smtpUserLabel": "Utente SMTP",
+ "smtpUserLabel": "Nome utente SMTP",
"smtpUserPlaceholder": "user@example.com",
"smtpPasswordLabel": "Password SMTP",
"smtpPasswordPlaceholder": "••••••••",
"imapHostLabel": "Host IMAP",
"imapHostPlaceholder": "imap.example.com",
- "instructions": "Fornisci le credenziali SMTP e IMAP per l'invio e la ricezione dei messaggi.",
- "disclaimer": "Confermo di essere autorizzato/a a usare queste credenziali e che PieCed IT può accedere a questa casella."
+ "instructions": "Fornisci le credenziali SMTP e IMAP. L'assistente le utilizza per inviare e monitorare i messaggi.",
+ "disclaimer": "Confermo di essere autorizzato a utilizzare queste credenziali e-mail e che PieCed IT può accedere a questa casella di posta."
},
"webSearch": {
- "description": "Dai al tuo assistente IA la possibilità di cercare sul web."
+ "description": "Dai al tuo assistente IA la capacità di cercare sul web."
},
"documentProcessing": {
- "description": "Attiva analisi, riassunto ed estrazione dei documenti."
+ "description": "Attiva analisi, riepilogo ed estrazione di documenti."
}
}
}
diff --git a/src/middleware.ts b/src/middleware.ts
index 76ae4b3..2c1447d 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -6,22 +6,23 @@ import { routing } from "@/i18n/routing";
const intlMiddleware = createIntlMiddleware(routing);
-const publicPaths = ["/login", "/api/auth"];
+const publicPaths = ["/login", "/register", "/api/auth", "/api/register"];
function isPublicPath(pathname: string): boolean {
// Strip locale prefix for comparison
const stripped = pathname.replace(/^\/(de|fr|it|en)/, "") || "/";
return (
publicPaths.some((p) => stripped === p || stripped.startsWith(`${p}/`)) ||
- pathname.startsWith("/api/auth")
+ pathname.startsWith("/api/auth") ||
+ pathname.startsWith("/api/register")
);
}
export default async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
- // NextAuth API routes pass through directly
- if (pathname.startsWith("/api/auth")) {
+ // NextAuth API routes and register API pass through directly
+ if (pathname.startsWith("/api/auth") || pathname.startsWith("/api/register")) {
return NextResponse.next();
}
@@ -39,5 +40,5 @@ export default async function middleware(request: NextRequest) {
}
export const config = {
- matcher: ["/((?!_next|favicon.ico|api).*)" ],
+ matcher: ["/((?!_next|favicon.ico|api).*)"],
};
diff --git a/src/types/index.ts b/src/types/index.ts
index a322aa6..1fd7c0f 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -65,3 +65,57 @@ export interface UsageSummary {
totalSpendChf: number;
period: string;
}
+
+// ---------------------------------------------------------------------------
+// Registration & Onboarding
+// ---------------------------------------------------------------------------
+
+export interface RegistrationInput {
+ companyName: string;
+ givenName: string;
+ familyName: string;
+ email: string;
+ preferredLanguage?: string;
+}
+
+export interface BillingAddress {
+ company?: string;
+ street?: string;
+ city?: string;
+ postalCode?: string;
+ country?: string;
+}
+
+export type TenantRequestStatus =
+ | "pending" // Submitted, awaiting admin approval
+ | "approved" // Admin approved, provisioning will start
+ | "provisioning" // PiecedTenant CR created, operator reconciling
+ | "active" // Tenant running
+ | "rejected"; // Admin rejected
+
+export interface TenantRequest {
+ id: string;
+ zitadelOrgId: string;
+ zitadelUserId: string;
+ companyName: string;
+ contactName: string;
+ contactEmail: string;
+ agentName: string;
+ soulMd?: string;
+ packages: string[];
+ billingAddress: BillingAddress;
+ billingNotes?: string;
+ status: TenantRequestStatus;
+ adminNotes?: string;
+ tenantName?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface OnboardingInput {
+ agentName: string;
+ soulMd?: string;
+ packages?: string[];
+ billingAddress: BillingAddress;
+ billingNotes?: string;
+}