Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
658
src/Application/Controller/permissions.view.php
Normal file
658
src/Application/Controller/permissions.view.php
Normal file
@@ -0,0 +1,658 @@
|
||||
<main class="demo-container">
|
||||
<section class="hero-section">
|
||||
<h1>🔐 Permission Management & Biometric Authentication</h1>
|
||||
<p class="hero-description">Test und Demo des Permission Management Systems und WebAuthn Biometric Authentication</p>
|
||||
</section>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- API Support Status -->
|
||||
<section class="demo-card">
|
||||
<h2>📋 API Support Status</h2>
|
||||
<button class="btn btn-primary" onclick="checkAPISupport()">Check API Support</button>
|
||||
<div id="api-support-results" class="results-box"></div>
|
||||
</section>
|
||||
|
||||
<!-- Permission Management -->
|
||||
<section class="demo-card">
|
||||
<h2>🔔 Permission Management</h2>
|
||||
|
||||
<div class="button-group">
|
||||
<h3>Individual Permissions</h3>
|
||||
<button class="btn btn-secondary" onclick="checkPermission('camera')">Check Camera</button>
|
||||
<button class="btn btn-secondary" onclick="checkPermission('microphone')">Check Microphone</button>
|
||||
<button class="btn btn-secondary" onclick="checkPermission('geolocation')">Check Location</button>
|
||||
<button class="btn btn-secondary" onclick="checkPermission('notifications')">Check Notifications</button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<h3>Request Permissions</h3>
|
||||
<button class="btn btn-primary" onclick="requestPermission('camera')">Request Camera</button>
|
||||
<button class="btn btn-primary" onclick="requestPermission('microphone')">Request Microphone</button>
|
||||
<button class="btn btn-primary" onclick="requestPermission('geolocation')">Request Location</button>
|
||||
<button class="btn btn-primary" onclick="requestPermission('notifications')">Request Notifications</button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<h3>Batch Operations</h3>
|
||||
<button class="btn btn-success" onclick="requestMultiplePermissions()">Request Multiple</button>
|
||||
<button class="btn btn-success" onclick="startOnboardingFlow()">Start Onboarding</button>
|
||||
<button class="btn btn-outline" onclick="getPermissionReport()">Get Report</button>
|
||||
</div>
|
||||
|
||||
<div id="permission-results" class="results-box"></div>
|
||||
</section>
|
||||
|
||||
<!-- Biometric Authentication -->
|
||||
<section class="demo-card">
|
||||
<h2>👆 Biometric Authentication</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<h3>User Information</h3>
|
||||
<div class="input-group">
|
||||
<label for="username">Username/Email:</label>
|
||||
<input type="text" id="username" placeholder="user@example.com" value="testuser@example.com">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="displayName">Display Name:</label>
|
||||
<input type="text" id="displayName" placeholder="Test User" value="Test User">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<h3>Authentication Actions</h3>
|
||||
<button class="btn btn-info" onclick="checkBiometricSupport()">Check Support</button>
|
||||
<button class="btn btn-success" onclick="registerBiometric()">Register Biometric</button>
|
||||
<button class="btn btn-primary" onclick="authenticateBiometric()">Authenticate</button>
|
||||
<button class="btn btn-secondary" onclick="setupConditionalUI()">Setup Conditional UI</button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<h3>Management</h3>
|
||||
<button class="btn btn-outline" onclick="getBiometricStatus()">Get Status</button>
|
||||
<button class="btn btn-outline" onclick="listCredentials()">List Credentials</button>
|
||||
<button class="btn btn-danger" onclick="clearCredentials()">Clear All</button>
|
||||
</div>
|
||||
|
||||
<div id="biometric-results" class="results-box"></div>
|
||||
|
||||
<div class="credentials-section">
|
||||
<h3>Registered Credentials</h3>
|
||||
<div id="credentials-list" class="credentials-list">
|
||||
<p>No credentials registered</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Combined Workflows -->
|
||||
<section class="demo-card full-width">
|
||||
<h2>🔄 Combined Workflows</h2>
|
||||
<div class="button-group">
|
||||
<button class="btn btn-success" onclick="completeSetupFlow()">Complete Setup Flow</button>
|
||||
<button class="btn btn-primary" onclick="createSecureLoginFlow()">Create Secure Login Flow</button>
|
||||
</div>
|
||||
<div id="workflow-results" class="results-box"></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
border: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.demo-card.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.demo-card h2 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #f0f2f5;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-card h3 {
|
||||
margin: 1.5rem 0 1rem 0;
|
||||
color: #555;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.button-group:last-of-type {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
margin: 0.25rem 0.5rem 0.25rem 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #1e7e34;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #117a8b;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
color: #6c757d;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc !important;
|
||||
color: #666 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
|
||||
}
|
||||
|
||||
.results-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
white-space: pre-wrap;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.results-box:empty::after {
|
||||
content: "No results yet...";
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.credentials-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.credentials-list {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.credential-item {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.credential-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.credential-item strong {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.credential-item button {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.credential-item button:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Wait for API to be available
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
// Wait for modules to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
if (!window.API) {
|
||||
console.error('API Manager not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if required managers are available
|
||||
if (!window.API.permissions || !window.API.biometric) {
|
||||
console.error('Permission or Biometric managers not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔐 Permission Management & Biometric Auth Demo ready');
|
||||
console.log('Available API Managers:', Object.keys(window.API));
|
||||
});
|
||||
|
||||
// API Support Check
|
||||
async function checkAPISupport() {
|
||||
const results = document.getElementById('api-support-results');
|
||||
|
||||
if (!window.API) {
|
||||
results.textContent = 'ERROR: API Manager not available!';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const permissionSupport = window.API.permissions ? await window.API.permissions.getPermissionReport() : null;
|
||||
const biometricSupport = window.API.biometric ? await window.API.biometric.isAvailable() : null;
|
||||
|
||||
results.textContent = JSON.stringify({
|
||||
apiManagers: {
|
||||
permissions: !!window.API.permissions,
|
||||
biometric: !!window.API.biometric,
|
||||
media: !!window.API.media,
|
||||
device: !!window.API.device
|
||||
},
|
||||
permissionSupport,
|
||||
biometricSupport
|
||||
}, null, 2);
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR: ${error.message}`;
|
||||
console.error('API Support Check Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Permission Functions
|
||||
async function checkPermission(permission) {
|
||||
const results = document.getElementById('permission-results');
|
||||
|
||||
try {
|
||||
const status = await window.API.permissions.check(permission);
|
||||
results.textContent = `${permission} permission status:\n${JSON.stringify(status, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR checking ${permission}: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestPermission(permission) {
|
||||
const results = document.getElementById('permission-results');
|
||||
|
||||
try {
|
||||
const result = await window.API.permissions.request(permission, {
|
||||
showRationale: true,
|
||||
timeout: 30000
|
||||
});
|
||||
results.textContent = `${permission} permission request result:\n${JSON.stringify(result, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR requesting ${permission}: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestMultiplePermissions() {
|
||||
const results = document.getElementById('permission-results');
|
||||
|
||||
try {
|
||||
const permissions = ['camera', 'microphone', 'geolocation', 'notifications'];
|
||||
const result = await window.API.permissions.requestMultiple(permissions, {
|
||||
sequential: false,
|
||||
requireAll: false
|
||||
});
|
||||
results.textContent = `Multiple permissions result:\n${JSON.stringify(result, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR requesting multiple permissions: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function startOnboardingFlow() {
|
||||
const results = document.getElementById('permission-results');
|
||||
|
||||
try {
|
||||
const permissions = ['camera', 'microphone', 'geolocation'];
|
||||
const onboardingFlow = window.API.permissions.createOnboardingFlow(permissions, {
|
||||
title: 'App Permissions Setup',
|
||||
descriptions: {
|
||||
camera: 'We need camera access to take photos and scan QR codes',
|
||||
microphone: 'We need microphone access for voice messages',
|
||||
geolocation: 'We need location access to show nearby places'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await onboardingFlow.start();
|
||||
results.textContent = `Onboarding flow result:\n${JSON.stringify(result, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR in onboarding flow: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getPermissionReport() {
|
||||
const results = document.getElementById('permission-results');
|
||||
|
||||
try {
|
||||
const report = await window.API.permissions.getPermissionReport();
|
||||
results.textContent = `Permission Report:\n${JSON.stringify(report, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR getting permission report: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Biometric Functions
|
||||
async function checkBiometricSupport() {
|
||||
const results = document.getElementById('biometric-results');
|
||||
|
||||
try {
|
||||
const availability = await window.API.biometric.isAvailable();
|
||||
results.textContent = `Biometric Support:\n${JSON.stringify(availability, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR checking biometric support: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerBiometric() {
|
||||
const results = document.getElementById('biometric-results');
|
||||
const username = document.getElementById('username').value;
|
||||
const displayName = document.getElementById('displayName').value;
|
||||
|
||||
if (!username) {
|
||||
results.textContent = 'ERROR: Please enter a username';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = {
|
||||
id: username,
|
||||
username: username,
|
||||
displayName: displayName || username
|
||||
};
|
||||
|
||||
const result = await window.API.biometric.register(userInfo);
|
||||
results.textContent = `Registration Result:\n${JSON.stringify(result, null, 2)}`;
|
||||
|
||||
if (result.success) {
|
||||
updateCredentialsList();
|
||||
}
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR during registration: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function authenticateBiometric() {
|
||||
const results = document.getElementById('biometric-results');
|
||||
|
||||
try {
|
||||
const result = await window.API.biometric.authenticate();
|
||||
results.textContent = `Authentication Result:\n${JSON.stringify(result, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR during authentication: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupConditionalUI() {
|
||||
const results = document.getElementById('biometric-results');
|
||||
|
||||
try {
|
||||
const result = await window.API.biometric.setupConditionalUI();
|
||||
results.textContent = `Conditional UI Setup:\n${JSON.stringify(result, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR setting up conditional UI: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getBiometricStatus() {
|
||||
const results = document.getElementById('biometric-results');
|
||||
|
||||
try {
|
||||
const status = await window.API.biometric.getStatusReport();
|
||||
results.textContent = `Biometric Status:\n${JSON.stringify(status, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR getting biometric status: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function listCredentials() {
|
||||
updateCredentialsList();
|
||||
}
|
||||
|
||||
function clearCredentials() {
|
||||
if (confirm('Are you sure you want to clear all biometric credentials?')) {
|
||||
try {
|
||||
const credentials = window.API.biometric.getCredentials();
|
||||
credentials.forEach(cred => {
|
||||
window.API.biometric.revokeCredential(cred.id);
|
||||
});
|
||||
|
||||
updateCredentialsList();
|
||||
document.getElementById('biometric-results').textContent = 'All credentials cleared';
|
||||
} catch (error) {
|
||||
document.getElementById('biometric-results').textContent = `ERROR clearing credentials: ${error.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workflow Functions
|
||||
async function completeSetupFlow() {
|
||||
const results = document.getElementById('workflow-results');
|
||||
|
||||
try {
|
||||
// Step 1: Request required permissions
|
||||
const permissionResult = await window.API.permissions.requestMultiple(
|
||||
['camera', 'microphone', 'notifications'],
|
||||
{ sequential: true }
|
||||
);
|
||||
|
||||
// Step 2: If permissions granted, setup biometric auth
|
||||
if (permissionResult.granted > 0) {
|
||||
const userInfo = {
|
||||
id: 'workflow-user',
|
||||
username: 'workflow-user',
|
||||
displayName: 'Workflow Test User'
|
||||
};
|
||||
|
||||
const biometricResult = await window.API.biometric.register(userInfo);
|
||||
|
||||
results.textContent = `Complete Setup Flow:\nPermissions: ${permissionResult.granted}/${permissionResult.total} granted\nBiometric: ${biometricResult.success ? 'Registered' : 'Failed'}\n\n` +
|
||||
JSON.stringify({ permissions: permissionResult, biometric: biometricResult }, null, 2);
|
||||
|
||||
if (biometricResult.success) {
|
||||
updateCredentialsList();
|
||||
}
|
||||
} else {
|
||||
results.textContent = `Setup failed - no permissions granted:\n${JSON.stringify(permissionResult, null, 2)}`;
|
||||
}
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR in setup flow: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSecureLoginFlow() {
|
||||
const results = document.getElementById('workflow-results');
|
||||
|
||||
try {
|
||||
const loginFlow = window.API.biometric.createLoginFlow({
|
||||
onRegister: (result) => {
|
||||
console.log('Biometric registered:', result);
|
||||
updateCredentialsList();
|
||||
},
|
||||
onLogin: (result) => {
|
||||
console.log('Biometric login successful:', result);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Login flow error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const availability = await loginFlow.init();
|
||||
results.textContent = `Secure Login Flow Created:\n${JSON.stringify(availability, null, 2)}`;
|
||||
} catch (error) {
|
||||
results.textContent = `ERROR creating secure login flow: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
function updateCredentialsList() {
|
||||
const credentialsList = document.getElementById('credentials-list');
|
||||
|
||||
try {
|
||||
const credentials = window.API.biometric.getCredentials();
|
||||
|
||||
if (credentials.length === 0) {
|
||||
credentialsList.innerHTML = '<p style="margin: 0; color: #666; font-style: italic;">No credentials registered</p>';
|
||||
} else {
|
||||
const credentialsHTML = credentials.map(cred => `
|
||||
<div class="credential-item">
|
||||
<strong>ID:</strong> ${cred.id.substring(0, 20)}...<br>
|
||||
<strong>User:</strong> ${cred.userDisplayName}<br>
|
||||
<strong>Created:</strong> ${new Date(cred.createdAt).toLocaleString()}<br>
|
||||
<strong>Last Used:</strong> ${cred.lastUsed ? new Date(cred.lastUsed).toLocaleString() : 'Never'}<br>
|
||||
<strong>Transports:</strong> ${cred.transports.join(', ')}<br>
|
||||
<button onclick="window.API.biometric.revokeCredential('${cred.id}'); updateCredentialsList();">Revoke</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
credentialsList.innerHTML = credentialsHTML;
|
||||
}
|
||||
} catch (error) {
|
||||
credentialsList.innerHTML = `<p style="color: #dc3545;">Error loading credentials: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
setTimeout(updateCredentialsList, 2000);
|
||||
</script>
|
||||
Reference in New Issue
Block a user