Files

719 lines
33 KiB
PHP

<?php
declare(strict_types=1);
style('qortal_integration', 'admin');
script('qortal_integration', 'admin');
$settings = $_['settings'];
$notices = [];
$brokerBaseUrl = trim((string)($settings['brokerBaseUrl'] ?? ''));
$brokerInternalApiToken = trim((string)($settings['brokerInternalApiToken'] ?? ''));
$brokerInternalApiTokenEnv = trim((string)(getenv('QORTAL_BROKER_INTERNAL_API_TOKEN') ?: ''));
$externalAuthBaseUrl = trim((string)($settings['externalAuthBaseUrl'] ?? ''));
$externalAuthAppId = trim((string)($settings['externalAuthAppId'] ?? ''));
$externalAuthAppSecret = trim((string)($settings['externalAuthAppSecret'] ?? ''));
$externalAuthDocsUrl = trim((string)($settings['externalAuthDocsUrl'] ?? ''));
$externalAuthNodeUrl = trim((string)($settings['externalAuthNodeUrl'] ?? ''));
$externalAuthNodeApiKey = trim((string)($settings['externalAuthNodeApiKey'] ?? ''));
$externalAuthNodeApiKeyMode = trim((string)($settings['externalAuthNodeApiKeyMode'] ?? ''));
if ($externalAuthNodeApiKeyMode !== 'paths') {
$externalAuthNodeApiKeyMode = 'paths';
}
$externalAuthNodeApiKeyPaths = trim((string)($settings['externalAuthNodeApiKeyPaths'] ?? ''));
$oidcIssuerUrl = trim((string)($settings['oidcIssuerUrl'] ?? ''));
$oidcClientId = trim((string)($settings['oidcClientId'] ?? ''));
$oidcClientSecret = trim((string)($settings['oidcClientSecret'] ?? ''));
$oidcPolicyModeOverride = trim((string)($settings['oidcPolicyMode'] ?? ''));
if ($oidcPolicyModeOverride !== 'link_only' && $oidcPolicyModeOverride !== 'auto_provision') {
$oidcPolicyModeOverride = '';
}
$oidcGuardOverride = trim((string)($settings['oidcAutoProvisionGuard'] ?? ''));
if ($oidcGuardOverride !== 'invite_or_allowlist' && $oidcGuardOverride !== 'off') {
$oidcGuardOverride = '';
}
$oidcRequireEmailOverride = trim((string)($settings['oidcRequireEmailForNewAccount'] ?? ''));
if ($oidcRequireEmailOverride !== 'required' && $oidcRequireEmailOverride !== 'off') {
$oidcRequireEmailOverride = '';
}
$nextcloudPublicUrl = trim((string)($settings['nextcloudPublicUrl'] ?? ''));
$qortalNodeUrl = trim((string)($settings['qortalNodeUrl'] ?? ''));
$qortalNodeApiKey = trim((string)($settings['qortalNodeApiKey'] ?? ''));
$qortalGatewayUrl = trim((string)($settings['qortalGatewayUrl'] ?? ''));
$qortalGatewayAllowInsecure = !empty($settings['qortalGatewayAllowInsecure']);
if ($brokerBaseUrl === '') {
$notices[] = ['type' => 'error', 'message' => 'Broker Base URL is required. Set it to http://broker:3000 (Docker) or your broker public URL.'];
}
if ($brokerInternalApiToken === '' && $brokerInternalApiTokenEnv === '') {
$notices[] = ['type' => 'error', 'message' => 'Broker Internal API Token is not configured. Set BROKER_INTERNAL_API_TOKEN in your env file and/or set a matching token below.'];
}
if ($externalAuthDocsUrl === '') {
$notices[] = ['type' => 'warning', 'message' => 'External Auth Docs URL is empty. Set it to your External Auth docs page for quick access.'];
}
if ($externalAuthBaseUrl === '') {
$notices[] = ['type' => 'warning', 'message' => 'External Auth Base URL is empty. Broker wallet operations will fail until it is configured.'];
}
if ($externalAuthAppId === '' || $externalAuthAppSecret === '') {
$notices[] = ['type' => 'warning', 'message' => 'External Auth App ID/Secret are missing. Broker wallet operations require these credentials.'];
}
if ($externalAuthNodeUrl === '') {
$notices[] = ['type' => 'warning', 'message' => 'External Auth Qortal Node URL is empty. Ensure the External Auth container has a Qortal node configured.'];
}
if ($externalAuthNodeUrl !== '' && $externalAuthNodeApiKey === '' && $qortalNodeApiKey === '') {
$notices[] = ['type' => 'warning', 'message' => 'External Auth node API key is empty. Restricted Qortal endpoints may fail if the node requires an API key. In containerized setups, set QORTAL_AUTH_NODE_API_KEY in .env.devprod and recreate external_auth.'];
} elseif ($externalAuthNodeUrl !== '' && $externalAuthNodeApiKey === '' && $qortalNodeApiKey !== '') {
$notices[] = ['type' => 'warning', 'message' => 'External Auth node API key is empty. Runtime sync will fall back to the Qortal Node API key value.'];
}
if ($oidcIssuerUrl === '' && $brokerBaseUrl === '') {
$notices[] = ['type' => 'error', 'message' => 'OIDC Issuer URL cannot be resolved. Set Broker Base URL or provide an explicit issuer URL.'];
}
if ($oidcClientId === '') {
$notices[] = ['type' => 'warning', 'message' => 'OIDC Client ID is empty. Default will fall back to nextcloud-local.'];
}
if ($oidcClientSecret === '') {
$notices[] = ['type' => 'error', 'message' => 'OIDC Client Secret is required. Set it in the OIDC Provider Settings section.'];
} elseif ($oidcClientSecret === 'dev-secret') {
$notices[] = ['type' => 'warning', 'message' => 'OIDC Client Secret is set to dev-secret. Replace it before production.'];
}
if ($nextcloudPublicUrl === '') {
$notices[] = ['type' => 'warning', 'message' => 'Nextcloud Public URL is empty. Trusted domains and overwrite settings will not be updated by setup actions.'];
}
?>
<div
id="qortal-admin-root"
class="section"
data-status-url="<?php p($_['statusUrl']); ?>"
data-setup-url="<?php p($_['setupUrl']); ?>"
data-setup-plan-url="<?php p($_['setupPlanUrl']); ?>"
data-setup-occ-url="<?php p($_['setupOccUrl']); ?>"
data-notify-url="<?php p($_['notifyUrl']); ?>"
data-search-users-url="<?php p($_['searchUsersUrl']); ?>"
data-search-groups-url="<?php p($_['searchGroupsUrl']); ?>"
data-settings-url="<?php p($_['settingsUrl']); ?>"
data-wallets-url="<?php p($_['walletsUrl']); ?>"
data-create-wallet-url="<?php p($_['createWalletUrl']); ?>"
data-register-external-auth-url="<?php p($_['registerExternalAuthUrl']); ?>"
data-mappings-url="<?php p($_['mappingsUrl']); ?>"
data-link-mapping-url="<?php p($_['linkMappingUrl']); ?>"
data-unlink-mapping-url="<?php p($_['unlinkMappingUrl']); ?>"
data-allowlist-url="<?php p($_['allowlistUrl']); ?>"
data-add-allowlist-url="<?php p($_['addAllowlistUrl']); ?>"
data-remove-allowlist-url="<?php p($_['removeAllowlistUrl']); ?>"
data-invites-url="<?php p($_['invitesUrl']); ?>"
data-create-invite-url="<?php p($_['createInviteUrl']); ?>"
data-revoke-invite-url="<?php p($_['revokeInviteUrl']); ?>"
data-qapps-json="<?php p(json_encode($settings['qappsList'] ?? [], JSON_UNESCAPED_SLASHES)); ?>"
data-qapps-enabled="<?php p(!empty($settings['qappsEnabled']) ? '1' : '0'); ?>"
data-qapps-browser-enabled="<?php p(!empty($settings['qappsFullBrowserEnabled']) ? '1' : '0'); ?>"
data-qapps-browser-address="<?php p((string)($settings['qappsFullBrowserAddress'] ?? '')); ?>"
data-qapps-debug-enabled="<?php p(!empty($settings['qappsDebugEnabled']) ? '1' : '0'); ?>"
data-nextcloud-public-url="<?php p($nextcloudPublicUrl); ?>"
>
<h2><?php p($_['title']); ?></h2>
<p class="qortal-help">
Configure broker connectivity, setup wallets, and manage pre-linked identities for Qortal OIDC link mode.
</p>
<?php if (!empty($notices)) { ?>
<div class="qortal-card qortal-notices">
<h3>Setup Notices</h3>
<ul>
<?php foreach ($notices as $notice) { ?>
<li class="qortal-notice <?php p($notice['type']); ?>"><?php p($notice['message']); ?></li>
<?php } ?>
</ul>
</div>
<?php } ?>
<div class="qortal-grid">
<label for="qortal-broker-base-url">Broker Base URL</label>
<div class="qortal-input-group">
<textarea id="qortal-broker-base-url" rows="2" spellcheck="false" placeholder="http://broker:3000"><?php p((string)$settings['brokerBaseUrl']); ?></textarea>
<p class="qortal-note">Required broker endpoints: <code>/api/health</code> and <code>/api/qortal/health</code></p>
</div>
<label for="qortal-broker-internal-api-token">Broker Internal API Token</label>
<div class="qortal-input-group">
<input id="qortal-broker-internal-api-token" type="password" value="<?php p($brokerInternalApiToken); ?>" placeholder="Must match BROKER_INTERNAL_API_TOKEN">
<p class="qortal-note">
Used by Nextcloud when calling broker internal APIs. Must match broker env <code>BROKER_INTERNAL_API_TOKEN</code>.
For containerized setup, prefer setting the env value in <code>.env.devprod</code>.
</p>
</div>
<label for="qortal-external-auth-docs-url">External Auth Docs URL</label>
<div class="qortal-input-group">
<textarea id="qortal-external-auth-docs-url" rows="2" spellcheck="false" placeholder="http://localhost:3191/docs/static/index.html"><?php p((string)$settings['externalAuthDocsUrl']); ?></textarea>
</div>
</div>
<div class="qortal-toggles">
<label>
<input id="qortal-feature-qdn-backups" type="checkbox" <?php if (!empty($settings['featureQdnBackups'])) { print_unescaped('checked'); } ?>>
Enable QDN backups workflow
</label>
<label>
<input id="qortal-feature-qmail" type="checkbox" <?php if (!empty($settings['featureQmail'])) { print_unescaped('checked'); } ?>>
Enable Q-Mail workflow
</label>
</div>
<div class="qortal-actions">
<button id="qortal-save-settings" class="button button-primary">Save Settings</button>
<button id="qortal-refresh-setup" class="button">Refresh Setup Data</button>
<button id="qortal-test-connection" class="button">Test Broker Connection</button>
</div>
<p class="qortal-note">
Save Settings updates Nextcloud app settings and attempts live broker/external-auth runtime sync.
Container env files are not changed by Save Settings.
</p>
<p class="qortal-note">
Broker internal APIs require <code>BROKER_INTERNAL_API_TOKEN</code> on the broker service.
If this token changes in env, update the matching token here (or via app env <code>QORTAL_BROKER_INTERNAL_API_TOKEN</code>).
</p>
<div class="qortal-card" id="qortal-setup-overview">
<h3>Setup Overview</h3>
<ul id="qortal-setup-overview-list" class="qortal-status-list"></ul>
<p id="qortal-setup-overview-note" class="qortal-note"></p>
</div>
<div class="qortal-card">
<h3>OIDC Provider Settings</h3>
<p class="qortal-note">
These values are used when generating or running the <code>user_oidc</code> provider setup only.
They do not update broker runtime env values.
</p>
<div class="qortal-grid">
<label for="qortal-oidc-issuer-url">OIDC Issuer URL</label>
<div class="qortal-input-group">
<textarea id="qortal-oidc-issuer-url" rows="2" spellcheck="false" placeholder="http://broker:3000"><?php p((string)$settings['oidcIssuerUrl']); ?></textarea>
<p class="qortal-note">Defaults to broker base URL if left empty.</p>
</div>
<label for="qortal-oidc-client-id">OIDC Client ID</label>
<input id="qortal-oidc-client-id" type="text" value="<?php p((string)$settings['oidcClientId']); ?>" placeholder="nextcloud-local">
<label for="qortal-oidc-client-secret">OIDC Client Secret</label>
<input id="qortal-oidc-client-secret" type="password" value="<?php p((string)$settings['oidcClientSecret']); ?>" placeholder="dev-secret">
<label for="qortal-nextcloud-public-url">Nextcloud Public URL</label>
<div class="qortal-input-group">
<input id="qortal-nextcloud-public-url" type="text" value="<?php p((string)$settings['nextcloudPublicUrl']); ?>" placeholder="https://cloud.example.com">
<p class="qortal-note">Used to update trusted domains and overwrite settings when running setup.</p>
</div>
</div>
</div>
<div class="qortal-card">
<h3>External Auth Configuration</h3>
<p class="qortal-note">
Store External Auth connection details here for runtime sync and env generation.
Save Settings stores these in Nextcloud and attempts live runtime sync through the broker.
If your daemon does not expose runtime settings endpoints, update env files and recreate/restart containers.
</p>
<p class="qortal-note">
Important: for bundled/containerized External Auth, set <code>QORTAL_AUTH_NODE_API_KEY</code> in
<code>.env.devprod</code> and recreate <code>external_auth</code>. The admin field below is a best-effort
runtime override and may not persist across container restarts.
</p>
<div class="qortal-grid">
<label for="qortal-external-auth-base-url">External Auth Base URL</label>
<div class="qortal-input-group">
<input id="qortal-external-auth-base-url" type="text" value="<?php p((string)$settings['externalAuthBaseUrl']); ?>" placeholder="http://external_auth:3191">
<p class="qortal-note">Used by broker as <code>QORTAL_EXTERNAL_AUTH_BASE_URL</code>.</p>
</div>
<label for="qortal-external-auth-app-id">External Auth App ID</label>
<input id="qortal-external-auth-app-id" type="text" value="<?php p((string)$settings['externalAuthAppId']); ?>" placeholder="UUID">
<label for="qortal-external-auth-app-secret">External Auth App Secret</label>
<input id="qortal-external-auth-app-secret" type="password" value="<?php p((string)$settings['externalAuthAppSecret']); ?>" placeholder="Secret">
<label for="qortal-external-auth-app-name">External Auth App Name</label>
<input id="qortal-external-auth-app-name" type="text" value="qortal-nextcloud-integration" placeholder="qortal-nextcloud-integration">
<label></label>
<div class="qortal-input-group">
<button id="qortal-external-auth-register" class="button">Register External Auth App</button>
<p class="qortal-note">
Warning: registering a new app will replace existing credentials. If External Auth is already configured
via <code>.env</code>, this will generate a new App ID/Secret and you may lose access to existing wallets.
Backup your <code>.env</code> or <code>.env.devprod</code> first.
</p>
</div>
<label for="qortal-external-auth-node-url">Qortal Node URL (External Auth)</label>
<input id="qortal-external-auth-node-url" type="text" value="<?php p((string)$settings['externalAuthNodeUrl']); ?>" placeholder="http://qortal:12391">
<label for="qortal-external-auth-node-api-key">Qortal Node API Key (External Auth)</label>
<input id="qortal-external-auth-node-api-key" type="password" value="<?php p((string)$settings['externalAuthNodeApiKey']); ?>" placeholder="API key (optional)">
<p class="qortal-note">If left empty, runtime sync falls back to the key in “Qortal Node + Gateway”. For containerized setup, still set <code>QORTAL_AUTH_NODE_API_KEY</code> in <code>.env.devprod</code>.</p>
<label for="qortal-external-auth-node-api-key-mode">Node API Key Mode</label>
<select id="qortal-external-auth-node-api-key-mode">
<option value="paths" <?php if ($externalAuthNodeApiKeyMode === 'paths') { print_unescaped('selected'); } ?>>paths (recommended)</option>
</select>
<label for="qortal-external-auth-node-api-key-paths">Node API Key Paths</label>
<input id="qortal-external-auth-node-api-key-paths" type="text" value="<?php p((string)$settings['externalAuthNodeApiKeyPaths']); ?>" placeholder="/">
<p class="qortal-note">Only used when mode is set to <code>paths</code>. Use <code>/</code> to send <code>X-API-KEY</code> on all node API calls.</p>
</div>
<div class="qortal-actions">
<button id="qortal-external-auth-env-generate" class="button">Generate External Auth Env Snippet</button>
</div>
<pre id="qortal-external-auth-env-output" class="qortal-status qortal-compact-status"></pre>
<p class="qortal-note">
Save Settings attempts live runtime sync through the broker. If your External Auth daemon
does not expose runtime settings endpoints, apply env files and restart with
<code>./recreate-devprod.sh --extauth</code> or
<code>docker compose up -d --build broker external_auth</code>.
</p>
</div>
<div class="qortal-card">
<h3>Qortal Node + Gateway</h3>
<p class="qortal-note">
Configure the node used for Q-App rendering and signed requests. Gateway nodes expose a separate gateway port
and do not require an API key.
</p>
<div class="qortal-grid">
<label for="qortal-node-url">Qortal Node URL</label>
<input id="qortal-node-url" type="text" value="<?php p((string)$settings['qortalNodeUrl']); ?>" placeholder="http://qortal:12391">
<label for="qortal-node-api-key">Qortal Node API Key</label>
<input id="qortal-node-api-key" type="password" value="<?php p((string)$settings['qortalNodeApiKey']); ?>" placeholder="API key (if required)">
<label for="qortal-gateway-url">Gateway Node URL</label>
<div class="qortal-input-group">
<input id="qortal-gateway-url" type="text" value="<?php p((string)$settings['qortalGatewayUrl']); ?>" placeholder="http://gateway:12393">
<p class="qortal-note">Use a public gateway (e.g. https://qortal.link) or your own gateway node URL.</p>
</div>
<label for="qortal-gateway-insecure">Allow insecure gateway TLS</label>
<div class="qortal-input-group">
<label>
<input id="qortal-gateway-insecure" type="checkbox" <?php if ($qortalGatewayAllowInsecure) { print_unescaped('checked'); } ?>>
Disable TLS verification for gateway proxy requests
</label>
<p class="qortal-note">
Use only if your gateway uses a self-signed certificate or the container lacks CA roots. Recommended to keep off for production.
</p>
</div>
</div>
<div class="qortal-actions">
<button id="qortal-node-env-generate" class="button">Generate Node Env Snippet</button>
</div>
<pre id="qortal-node-env-output" class="qortal-status qortal-compact-status"></pre>
<p class="qortal-note">
When running a local node container, ensure gateway mode is enabled and expose the gateway port.
</p>
</div>
<div class="qortal-card qortal-hidden" id="qortal-setup-components">
<h3>Setup Components</h3>
<p class="qortal-note">
Generate the setup commands or run them automatically (requires <code>occ</code> access inside the Nextcloud container).
</p>
<div class="qortal-actions">
<button id="qortal-setup-plan" class="button">Generate Setup Commands</button>
<button id="qortal-setup-occ" class="button">Run Setup (occ)</button>
</div>
<pre id="qortal-setup-result" class="qortal-status qortal-compact-status"></pre>
</div>
<div class="qortal-card" id="qortal-oidc-policy-card">
<h3>Auto-Provision Policy (Broker)</h3>
<p class="qortal-note">
Read-only effective broker policy values (from env defaults plus optional admin overrides).
</p>
<div class="qortal-grid qortal-policy-grid">
<label>Policy Mode</label>
<div><code id="qortal-oidc-policy-mode">unknown</code></div>
<label>Auto-Provision Guard</label>
<div><code id="qortal-oidc-guard">unknown</code></div>
<label>Invite TTL (seconds)</label>
<div><code id="qortal-oidc-invite-ttl">-</code></div>
<label>Require Email For New Account</label>
<div><code id="qortal-oidc-require-email">unknown</code></div>
<label>Redirect Allowlist</label>
<div><code id="qortal-oidc-redirects">-</code></div>
</div>
<p id="qortal-oidc-policy-note" class="qortal-note"></p>
<div class="qortal-grid qortal-policy-grid">
<label for="qortal-oidc-policy-select">Policy Mode Override</label>
<select id="qortal-oidc-policy-select">
<option value="" <?php if ($oidcPolicyModeOverride === '') { ?>selected<?php } ?>>(use env default)</option>
<option value="link_only" <?php if ($oidcPolicyModeOverride === 'link_only') { ?>selected<?php } ?>>link_only (existing users only)</option>
<option value="auto_provision" <?php if ($oidcPolicyModeOverride === 'auto_provision') { ?>selected<?php } ?>>auto_provision (new users allowed)</option>
</select>
<label for="qortal-oidc-guard-toggle">Guard Override</label>
<select id="qortal-oidc-guard-toggle">
<option value="" <?php if ($oidcGuardOverride === '') { ?>selected<?php } ?>>(use env default)</option>
<option value="invite_or_allowlist" <?php if ($oidcGuardOverride === 'invite_or_allowlist') { ?>selected<?php } ?>>invite_or_allowlist</option>
<option value="off" <?php if ($oidcGuardOverride === 'off') { ?>selected<?php } ?>>off</option>
</select>
<label for="qortal-oidc-invite-ttl-input">Invite TTL Override (seconds)</label>
<input id="qortal-oidc-invite-ttl-input" type="number" min="60" placeholder="(env default)">
<label for="qortal-oidc-require-email-toggle">Require Email For New Account</label>
<select id="qortal-oidc-require-email-toggle">
<option value="" <?php if ($oidcRequireEmailOverride === '') { ?>selected<?php } ?>>(use env default)</option>
<option value="required" <?php if ($oidcRequireEmailOverride === 'required') { ?>selected<?php } ?>>required</option>
<option value="off" <?php if ($oidcRequireEmailOverride === 'off') { ?>selected<?php } ?>>off</option>
</select>
<label for="qortal-oidc-redirect-allowlist-input">Redirect Allowlist Override</label>
<input id="qortal-oidc-redirect-allowlist-input" type="text" placeholder="(env default)">
</div>
<div class="qortal-actions">
<button id="qortal-oidc-env-generate" class="button">Generate Broker Env Snippet</button>
</div>
<p class="qortal-note">
Save Settings now syncs these overrides to broker runtime. Leave a field blank to keep using env defaults.
</p>
<pre id="qortal-oidc-env-output" class="qortal-status qortal-compact-status"></pre>
</div>
<div class="qortal-card">
<h3>Full Setup Options</h3>
<p class="qortal-note">
Use one of the supported setup entry points depending on your environment.
</p>
<div class="qortal-setup-options">
<div class="qortal-setup-option">
<strong>Local Docker Dev</strong>
<pre><code>./start-dev.sh</code></pre>
</div>
<div class="qortal-setup-option">
<strong>Dev-Prod (Caddy SSL)</strong>
<pre><code>./start-devprod.sh</code></pre>
</div>
<div class="qortal-setup-option">
<strong>Dev-Prod (No SSL / External Proxy)</strong>
<pre><code>./start-devprod.sh</code></pre>
<p class="qortal-note">Choose "no" when prompted for Caddy SSL.</p>
</div>
<div class="qortal-setup-option">
<strong>VM Install (Nextcloud VM + broker containers)</strong>
<pre><code>sudo bash scripts/nextcloud-vm-install.sh</code></pre>
</div>
<div class="qortal-setup-option">
<strong>Recreate Containers (apply new env)</strong>
<pre><code>./recreate-devprod.sh</code></pre>
</div>
</div>
</div>
<div class="qortal-card">
<h3>Q-Apps Access</h3>
<p class="qortal-note">
Enable Q-Apps access in Nextcloud and define allowed <code>qortal://</code> app addresses.
</p>
<div class="qortal-toggles">
<label>
<input id="qortal-qapps-enabled" type="checkbox" <?php if (!empty($settings['qappsEnabled'])) { print_unescaped('checked'); } ?>>
Enable Q-Apps menu
</label>
<label>
<input id="qortal-qapps-browser-enabled" type="checkbox" <?php if (!empty($settings['qappsFullBrowserEnabled'])) { print_unescaped('checked'); } ?>>
Enable full Qortal browser
</label>
<label>
<input id="qortal-qapps-debug-enabled" type="checkbox" <?php if (!empty($settings['qappsDebugEnabled'])) { print_unescaped('checked'); } ?>>
Enable Q-Apps debug panel
</label>
</div>
<div class="qortal-grid">
<label for="qortal-qapps-browser-address">Full Browser Address</label>
<input id="qortal-qapps-browser-address" type="text" placeholder="qortal://apps" value="<?php p((string)($settings['qappsFullBrowserAddress'] ?? '')); ?>">
</div>
<div class="qortal-inline-form qortal-qapps-form">
<label for="qortal-qapps-name">App Name</label>
<input id="qortal-qapps-name" type="text" placeholder="Qortal Notes">
<label for="qortal-qapps-address">Qortal Address</label>
<input id="qortal-qapps-address" type="text" placeholder="qortal://APP/NAME">
<label for="qortal-qapps-icon-mode">Icon Mode</label>
<select id="qortal-qapps-icon-mode">
<option value="auto" selected>Auto (gateway thumbnail)</option>
<option value="custom">Custom URL</option>
</select>
<label for="qortal-qapps-icon-url">Icon URL (optional)</label>
<input id="qortal-qapps-icon-url" type="text" placeholder="https://gateway/arbitrary/THUMBNAIL/APP/qortal_avatar">
<label for="qortal-qapps-description">Description (optional)</label>
<input id="qortal-qapps-description" type="text" placeholder="Short description">
<button id="qortal-add-qapp" class="button">Add Q-App</button>
<button id="qortal-clear-qapps" class="button">Clear List</button>
</div>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>Name</th>
<th>Qortal Address</th>
<th>Icon</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qortal-qapps-body"></tbody>
</table>
</div>
</div>
<div class="qortal-card">
<h3>Initial Setup Checklist</h3>
<ol>
<li>Configure broker URL and save settings.</li>
<li>Use <strong>Refresh Setup Data</strong> and confirm broker and External Auth are healthy.</li>
<li>Create or import wallet(s) visible to broker app credentials.</li>
<li>Link each Qortal address to a Nextcloud user (required for <code>link_only</code> mode).</li>
<li>Verify OIDC provider in Nextcloud login page.</li>
</ol>
</div>
<div class="qortal-card">
<h3>Wallet Operations</h3>
<p class="qortal-note">
Create wallets through the broker using configured External Auth app credentials.
If you link a wallet to a user, share the password securely with that user.
</p>
<div class="qortal-inline-form">
<label for="qortal-wallet-password">Wallet Password</label>
<input id="qortal-wallet-password" type="password" placeholder="Required for wallet creation">
<label for="qortal-wallet-kdf-threads">KDF Threads</label>
<input id="qortal-wallet-kdf-threads" type="number" min="1" placeholder="Optional">
<label for="qortal-wallet-user-id">Nextcloud User ID (optional)</label>
<input id="qortal-wallet-user-id" type="text" placeholder="admin">
<button id="qortal-create-wallet" class="button">Create Wallet</button>
<button id="qortal-create-wallet-link" class="button">Create &amp; Link User</button>
<button id="qortal-refresh-wallets" class="button">Refresh Wallets</button>
</div>
<pre id="qortal-wallet-create-result" class="qortal-status qortal-compact-status"></pre>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>Wallet ID</th>
<th>Address</th>
<th>Created</th>
</tr>
</thead>
<tbody id="qortal-wallets-body"></tbody>
</table>
</div>
</div>
<div class="qortal-card">
<h3>Identity Mapping Operations</h3>
<p class="qortal-note">
Link or unlink Qortal addresses so OIDC login can resolve identities in <code>link_only</code> mode.
</p>
<div class="qortal-inline-form qortal-link-form">
<label for="qortal-link-address">Qortal Address</label>
<input id="qortal-link-address" type="text" placeholder="Q...">
<label for="qortal-link-wallet-id">Wallet ID (optional)</label>
<input id="qortal-link-wallet-id" type="text" placeholder="UUID">
<label for="qortal-link-user-id">Nextcloud User ID</label>
<input id="qortal-link-user-id" type="text" placeholder="admin">
<button id="qortal-link-mapping" class="button">Link Mapping</button>
<button id="qortal-refresh-mappings" class="button">Refresh Mappings</button>
</div>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>Qortal Address</th>
<th>Nextcloud User</th>
<th>Wallet ID</th>
<th>Status</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qortal-mappings-body"></tbody>
</table>
</div>
</div>
<div class="qortal-card" id="qortal-allowlist-card">
<h3>Allowlisted Qortal Addresses</h3>
<p class="qortal-note">
These addresses can be preloaded for future use. Enforcement only applies when
<code>OIDC_POLICY_MODE=auto_provision</code> and the guard is enabled.
</p>
<div class="qortal-inline-form">
<label for="qortal-allowlist-address">Allowlist Qortal Address</label>
<input id="qortal-allowlist-address" type="text" placeholder="Q...">
<button id="qortal-add-allowlist" class="button">Add To Allowlist</button>
<button id="qortal-refresh-allowlist" class="button">Refresh Allowlist</button>
</div>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>Qortal Address</th>
<th>Added By</th>
<th>Added</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qortal-allowlist-body"></tbody>
</table>
</div>
</div>
<div class="qortal-card qortal-hidden" id="qortal-invites-card">
<h3>Invite Tokens</h3>
<p class="qortal-note">
Generate invite tokens for users to paste into the Qortal login form when auto-provisioning.
</p>
<div class="qortal-inline-form">
<label for="qortal-invite-expiry-hours">Invite Expiry (hours)</label>
<input id="qortal-invite-expiry-hours" type="number" min="1" placeholder="168">
<button id="qortal-create-invite" class="button">Create Invite</button>
<button id="qortal-refresh-invites" class="button">Refresh Invites</button>
</div>
<pre id="qortal-invite-create-result" class="qortal-status qortal-compact-status"></pre>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>Token</th>
<th>Status</th>
<th>Expires</th>
<th>Used By</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qortal-invites-body"></tbody>
</table>
</div>
</div>
<div class="qortal-card" id="qortal-invite-qortal-card">
<h3>Invite Existing Qortal Users To Cloud</h3>
<p class="qortal-note">
Generate a message for existing Qortal users. When auto-provisioning is enabled, an invite token is included.
In link-only mode, this message prompts users to link their Qortal account to an existing Nextcloud login.
</p>
<div class="qortal-actions">
<button id="qortal-generate-invite-message" class="button">Generate Invite Message</button>
<button id="qortal-copy-invite-message" class="button">Copy Message</button>
</div>
<textarea id="qortal-invite-message" rows="6" spellcheck="false" placeholder="Invite message will appear here."></textarea>
</div>
<div class="qortal-card" id="qortal-onboard-cloud-card">
<h3>Onboard Cloud Users</h3>
<p class="qortal-note">
Send onboarding prompts to existing Nextcloud users. Invite tokens are not required for existing users.
</p>
<div class="qortal-grid">
<label for="qortal-notify-email-subject">Email Subject</label>
<input id="qortal-notify-email-subject" type="text" value="<?php p((string)($settings['notifyEmailSubject'] ?? '')); ?>">
<label for="qortal-notify-email-body">Email Body</label>
<div class="qortal-input-group">
<textarea id="qortal-notify-email-body" rows="6" spellcheck="false"><?php p((string)($settings['notifyEmailBody'] ?? '')); ?></textarea>
<p class="qortal-note">Placeholders: <code>{link}</code>, <code>{invite}</code>, <code>{user}</code>, <code>{displayName}</code></p>
</div>
</div>
<div class="qortal-grid">
<label for="qortal-notify-user-ids">Target Users (one per line)</label>
<textarea id="qortal-notify-user-ids" rows="4" spellcheck="false" placeholder="admin&#10;alice&#10;bob"></textarea>
<label for="qortal-notify-group-ids">Target Groups (optional)</label>
<textarea id="qortal-notify-group-ids" rows="2" spellcheck="false" placeholder="admins&#10;staff"></textarea>
</div>
<div class="qortal-inline-form qortal-user-search">
<label for="qortal-user-search-query">Search Users</label>
<input id="qortal-user-search-query" type="text" placeholder="Type a username">
<button id="qortal-user-search-button" class="button">Search</button>
</div>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>User ID</th>
<th>Display Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qortal-user-search-results"></tbody>
</table>
</div>
<div class="qortal-inline-form qortal-user-search">
<label for="qortal-group-search-query">Search Groups</label>
<input id="qortal-group-search-query" type="text" placeholder="Type a group name">
<button id="qortal-group-search-button" class="button">Search</button>
</div>
<div class="qortal-table-wrap">
<table class="qortal-table">
<thead>
<tr>
<th>Group ID</th>
<th>Display Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="qortal-group-search-results"></tbody>
</table>
</div>
<div class="qortal-toggles">
<label>
<input id="qortal-notify-email" type="checkbox" checked>
Send Email
</label>
<label>
<input id="qortal-notify-inapp" type="checkbox" checked>
Send In-App Notification
</label>
<label>
<input id="qortal-notify-queue" type="checkbox">
Queue notifications (background job)
</label>
<label>
<input id="qortal-notify-include-invite" type="checkbox">
Include invite token (auto-provision guard)
</label>
</div>
<p class="qortal-note">Invite tokens are only required when auto-provision is enabled. Existing users do not need them.</p>
<div class="qortal-actions">
<button id="qortal-preview-email" class="button">Preview Email</button>
</div>
<div class="qortal-grid">
<label for="qortal-email-preview-subject">Email Preview Subject</label>
<input id="qortal-email-preview-subject" type="text" readonly>
<label for="qortal-email-preview-body">Email Preview Body</label>
<textarea id="qortal-email-preview-body" rows="6" readonly></textarea>
</div>
<div class="qortal-actions">
<button id="qortal-send-notifications" class="button">Send Notifications</button>
</div>
<pre id="qortal-notify-result" class="qortal-status qortal-compact-status"></pre>
</div>
<div id="qortal-admin-feedback" class="qortal-feedback" role="status"></div>
<pre id="qortal-admin-status" class="qortal-status"></pre>
</div>