diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index b075692b2..a3a2f71b4 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -2221,6 +2221,7 @@ + diff --git a/deploy-templates.sh b/deploy-templates.sh new file mode 100755 index 000000000..544b8d995 --- /dev/null +++ b/deploy-templates.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Deploy updated templates (and related static) from this repo to live CyberPanel. +# Use after pulling template changes so the panel at /usr/local/CyberCP shows the new UI. +# Usage: sudo bash deploy-templates.sh + +set -e + +CYBERCP_ROOT="${CYBERCP_ROOT:-/usr/local/CyberCP}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Deploying templates to $CYBERCP_ROOT..." + +# userManagment templates +for name in modifyUser createUser; do + SRC="$SCRIPT_DIR/userManagment/templates/userManagment/${name}.html" + DST="$CYBERCP_ROOT/userManagment/templates/userManagment/${name}.html" + if [ -f "$SRC" ]; then + cp -f "$SRC" "$DST" + echo " Copied userManagment ${name}.html" + fi +done + +# loginSystem login template +if [ -f "$SCRIPT_DIR/loginSystem/templates/loginSystem/login.html" ]; then + cp -f "$SCRIPT_DIR/loginSystem/templates/loginSystem/login.html" \ + "$CYBERCP_ROOT/loginSystem/templates/loginSystem/login.html" + echo " Copied loginSystem login.html" +fi + +# Optional: userManagment static (if you change JS) +if [ -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" ]; then + mkdir -p "$CYBERCP_ROOT/userManagment/static/userManagment" + cp -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" \ + "$CYBERCP_ROOT/userManagment/static/userManagment/userManagment.js" + echo " Copied userManagment.js" +fi +if [ -f "$SCRIPT_DIR/loginSystem/static/loginSystem/webauthn.js" ]; then + mkdir -p "$CYBERCP_ROOT/loginSystem/static/loginSystem" + cp -f "$SCRIPT_DIR/loginSystem/static/loginSystem/webauthn.js" \ + "$CYBERCP_ROOT/loginSystem/static/loginSystem/webauthn.js" + echo " Copied webauthn.js" +fi + +# Run collectstatic if manage.py exists (so static changes are served) +if [ -f "$CYBERCP_ROOT/manage.py" ]; then + PYTHON="${CYBERCP_ROOT}/bin/python" + [ -x "$PYTHON" ] || PYTHON="python3" + echo " Running collectstatic..." + (cd "$CYBERCP_ROOT" && "$PYTHON" manage.py collectstatic --noinput --clear 2>&1) | tail -5 +fi + +echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Templates deployed. Hard-refresh (Ctrl+F5) on Modify User / Create User / Login if needed." diff --git a/docs/2FA_AUTHENTICATION_GUIDE.md b/docs/2FA_AUTHENTICATION_GUIDE.md index f240c1465..cdf71aaac 100644 --- a/docs/2FA_AUTHENTICATION_GUIDE.md +++ b/docs/2FA_AUTHENTICATION_GUIDE.md @@ -97,6 +97,8 @@ TOTP generates time-based codes that change every 30 seconds. Users scan a QR co ### What is WebAuthn? WebAuthn is a web standard that enables secure, passwordless authentication using public-key cryptography. It supports biometric authentication, security keys, and device passkeys. +**Login behaviour**: The login page supports **passkey-first** sign-in: users can click "Login with Passkey" without entering a username. Passkeys are managed under **User Management → Modify User**. The relying party ID (`rp_id`) and origin are derived from the current request host only (never hardcoded), so WebAuthn works on any domain or IP (e.g. `https://207.180.193.210:2087`). + ### Setting Up WebAuthn #### Prerequisites diff --git a/fix-cyberpanel-500.sh b/fix-cyberpanel-500.sh new file mode 100644 index 000000000..2e9a672d1 --- /dev/null +++ b/fix-cyberpanel-500.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# fix-cyberpanel-500.sh – Apply common fixes for CyberPanel HTTP 500 on login. +# Run on the server: sudo bash fix-cyberpanel-500.sh +# See: to-do/CYBERPANEL-HTTP-500-LOGIN-FIX.md + +set -e +LOG="/var/log/cyberpanel_500_fix.log" +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; } + +if [[ $EUID -ne 0 ]]; then + echo "Run as root: sudo bash $0" + exit 1 +fi + +log "=== CyberPanel 500 fix script started ===" + +# 0. Ensure MariaDB is running (common cause: DB down -> 500) +log "Step 0: Ensuring MariaDB is running..." +systemctl start mariadb 2>/dev/null || systemctl start mysql 2>/dev/null || true +systemctl enable mariadb 2>/dev/null || systemctl enable mysql 2>/dev/null || true +log "Step 0 done." + +# 1. Remove or neutralize configservercsf (common cause of 500) +log "Step 1: Cleaning configservercsf references..." +rm -rf /usr/local/CyberCP/configservercsf 2>/dev/null || true +rm -f /home/cyberpanel/plugins/configservercsf 2>/dev/null || true +rm -rf /usr/local/CyberCP/public/static/configservercsf 2>/dev/null || true +sed -i '/configservercsf/d' /usr/local/CyberCP/CyberCP/settings.py 2>/dev/null || true +sed -i '/configservercsf/d' /usr/local/CyberCP/CyberCP/urls.py 2>/dev/null || true +log "Step 1 done." + +# 2. Clear Python cache +log "Step 2: Clearing __pycache__..." +find /usr/local/CyberCP -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true +log "Step 2 done." + +# 3. Restart panel and web server +log "Step 3: Restarting lscpd and lsws..." +systemctl restart lscpd +systemctl restart lsws +killall lsphp 2>/dev/null || true +log "Step 3 done." + +log "=== Fix script finished. Try https://YOUR_IP:2087 or :8090 ===" +log "If 500 persists, enable DEBUG in /usr/local/CyberCP/CyberCP/settings.py and check logs (see to-do/CYBERPANEL-HTTP-500-LOGIN-FIX.md)." diff --git a/loginSystem/static/loginSystem/webauthn.js b/loginSystem/static/loginSystem/webauthn.js index a9d4eddc5..9cc4ea5a4 100644 --- a/loginSystem/static/loginSystem/webauthn.js +++ b/loginSystem/static/loginSystem/webauthn.js @@ -10,6 +10,7 @@ class CyberPanelWebAuthn { this.apiEndpoints = { registrationStart: '/webauthn/registration/start/', registrationComplete: '/webauthn/registration/complete/', + authenticationOptions: '/webauthn/authentication/options/', authenticationStart: '/webauthn/authentication/start/', authenticationComplete: '/webauthn/authentication/complete/', credentialsList: '/webauthn/credentials/', @@ -60,18 +61,10 @@ class CyberPanelWebAuthn { addLoginButtons() { const loginForm = document.querySelector('#loginForm'); if (!loginForm) return; - - // Add WebAuthn login button - const webauthnButton = document.createElement('button'); - webauthnButton.type = 'button'; - webauthnButton.className = 'btn btn-primary btn-block'; - webauthnButton.innerHTML = ' Login with Passkey'; - webauthnButton.onclick = () => this.startPasswordlessLogin(); - - // Insert after password field - const passwordField = loginForm.querySelector('input[type="password"]'); - if (passwordField) { - passwordField.parentNode.insertBefore(webauthnButton, passwordField.parentNode.nextSibling); + const existingBtn = document.getElementById('webauthn-login-btn'); + if (existingBtn && !existingBtn.dataset.bound) { + existingBtn.dataset.bound = '1'; + existingBtn.onclick = () => this.startPasskeyFirstLogin(); } } @@ -80,6 +73,76 @@ class CyberPanelWebAuthn { // Implementation depends on the specific UI structure } + arrayBufferToBase64url(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + } + + base64urlToArrayBuffer(str) { + let base64 = str.replace(/-/g, '+').replace(/_/g, '/'); + const pad = (4 - (base64.length % 4)) % 4; + for (let i = 0; i < pad; i++) base64 += '='; + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes.buffer; + } + + async startPasskeyFirstLogin() { + try { + this.showLoading('Signing in with passkey...'); + const optsUrl = this.apiEndpoints.authenticationOptions + '?return=' + encodeURIComponent(window.location.pathname || '/'); + const optsResponse = await fetch(optsUrl, { method: 'GET', credentials: 'same-origin' }); + const optsData = await optsResponse.json(); + if (!optsData.publicKey) { + throw new Error(optsData.error || 'Failed to get options'); + } + const publicKey = optsData.publicKey; + publicKey.challenge = this.base64urlToArrayBuffer(publicKey.challenge); + if (publicKey.allowCredentials && publicKey.allowCredentials.length) { + publicKey.allowCredentials = publicKey.allowCredentials.map(function(c) { + return { + type: c.type || 'public-key', + id: typeof c.id === 'string' ? this.base64urlToArrayBuffer(c.id) : c.id, + transports: c.transports + }; + }.bind(this)); + } + const credential = await navigator.credentials.get({ publicKey }); + if (!credential) throw new Error('No credential'); + const credentialJson = { + id: credential.id, + rawId: this.arrayBufferToBase64url(credential.rawId), + type: credential.type, + response: { + clientDataJSON: this.arrayBufferToBase64url(credential.response.clientDataJSON), + authenticatorData: this.arrayBufferToBase64url(credential.response.authenticatorData), + signature: this.arrayBufferToBase64url(credential.response.signature), + userHandle: credential.response.userHandle ? this.arrayBufferToBase64url(credential.response.userHandle) : null + } + }; + const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, { + credential: credentialJson + }); + if (authResponse.success && authResponse.redirect) { + window.location.href = authResponse.redirect; + return; + } + throw new Error(authResponse.error || 'Verification failed'); + } catch (error) { + if (error.name === 'NotAllowedError' || (error.message && (error.message.indexOf('cancel') !== -1 || error.message.indexOf('timed out') !== -1))) { + this.hideLoading(); + return; + } + console.error('WebAuthn passkey-first error:', error); + this.showError(error.message || 'Passkey sign-in failed'); + } finally { + this.hideLoading(); + } + } + async startPasswordlessLogin() { try { const username = document.querySelector('input[name="username"]').value; @@ -87,27 +150,13 @@ class CyberPanelWebAuthn { this.showError('Please enter your username first'); return; } - this.showLoading('Starting passkey authentication...'); - - // Get authentication challenge - const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, { - username: username - }); - + const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, { username: username }); if (!challengeResponse.success) { throw new Error(challengeResponse.error || 'Failed to start authentication'); } - - // Convert challenge to proper format const challenge = this.convertChallenge(challengeResponse.challenge); - - // Get credential - const credential = await navigator.credentials.get({ - publicKey: challenge - }); - - // Complete authentication + const credential = await navigator.credentials.get({ publicKey: challenge }); const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, { challenge_id: challengeResponse.challenge_id, credential: { @@ -117,19 +166,14 @@ class CyberPanelWebAuthn { client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON), authenticator_data: this.arrayBufferToBase64(credential.response.authenticatorData), signature: this.arrayBufferToBase64(credential.response.signature), - user_handle: credential.response.userHandle ? - this.arrayBufferToBase64(credential.response.userHandle) : null + user_handle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null }); - if (authResponse.success) { this.showSuccess('Authentication successful! Redirecting...'); - setTimeout(() => { - window.location.href = '/'; - }, 1000); + setTimeout(() => { window.location.href = authResponse.redirect || '/'; }, 1000); } else { throw new Error(authResponse.error || 'Authentication failed'); } - } catch (error) { console.error('WebAuthn authentication error:', error); this.showError(error.message || 'Authentication failed'); @@ -138,9 +182,10 @@ class CyberPanelWebAuthn { } } - async registerPasskey(username, credentialName = '') { + async registerPasskey(username, credentialName = '', options = {}) { + const silent = options && options.silent === true; try { - this.showLoading('Starting passkey registration...'); + if (!silent) this.showLoading('Starting passkey registration...'); // Get registration challenge const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, { @@ -172,7 +217,7 @@ class CyberPanelWebAuthn { }); if (regResponse.success) { - this.showSuccess('Passkey registered successfully!'); + if (!silent) this.showSuccess('Passkey registered successfully!'); return regResponse; } else { throw new Error(regResponse.error || 'Registration failed'); @@ -180,10 +225,10 @@ class CyberPanelWebAuthn { } catch (error) { console.error('WebAuthn registration error:', error); - this.showError(error.message || 'Registration failed'); + if (!silent) this.showError(error.message || 'Registration failed'); throw error; } finally { - this.hideLoading(); + if (!silent) this.hideLoading(); } } @@ -265,23 +310,25 @@ class CyberPanelWebAuthn { } convertChallenge(challenge) { - // Convert base64 challenge to ArrayBuffer - const challengeBytes = this.base64ToArrayBuffer(challenge.challenge); - + const ch = challenge.challenge; + const challengeBytes = (typeof ch === 'string' && (ch.indexOf('-') !== -1 || ch.indexOf('_') !== -1)) + ? this.base64urlToArrayBuffer(ch) : this.base64ToArrayBuffer(ch); + const userId = challenge.user && challenge.user.id; + const userIdBuf = !userId ? undefined : (typeof userId === 'string' && (userId.indexOf('-') !== -1 || userId.indexOf('_') !== -1) + ? this.base64urlToArrayBuffer(userId) : this.base64ToArrayBuffer(userId)); return { ...challenge, challenge: challengeBytes, - user: { - ...challenge.user, - id: this.base64ToArrayBuffer(challenge.user.id) - }, + user: challenge.user ? { ...challenge.user, id: userIdBuf } : undefined, excludeCredentials: challenge.excludeCredentials?.map(cred => ({ ...cred, - id: this.base64ToArrayBuffer(cred.id) + id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1) + ? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id) })) || [], allowCredentials: challenge.allowCredentials?.map(cred => ({ ...cred, - id: this.base64ToArrayBuffer(cred.id) + id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1) + ? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id) })) || [] }; } @@ -383,12 +430,17 @@ class CyberPanelWebAuthn { } } -// Initialize WebAuthn when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { +// Initialize WebAuthn - run now if DOM ready, else on DOMContentLoaded (script often loads after DOM is ready) +function initCyberPanelWebAuthn() { if (CyberPanelWebAuthn.isSupported()) { window.cyberPanelWebAuthn = new CyberPanelWebAuthn(); } -}); +} +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCyberPanelWebAuthn); +} else { + initCyberPanelWebAuthn(); +} // Export for use in other scripts if (typeof module !== 'undefined' && module.exports) { diff --git a/loginSystem/templates/loginSystem/login.html b/loginSystem/templates/loginSystem/login.html index 14d373aff..39127b45c 100644 --- a/loginSystem/templates/loginSystem/login.html +++ b/loginSystem/templates/loginSystem/login.html @@ -352,7 +352,8 @@ class="btn btn-success btn-block btn-login">Sign In - + {% if passkey_login_available %} +
+ {% endif %} @@ -414,29 +416,17 @@ } {% get_current_language as LANGUAGE_CODE %} @@ -234,14 +235,30 @@