Skip to content

Commit 8a9cc36

Browse files
authored
Merge pull request #1725 from master3395/v2.5.5-dev
V2.5.5 dev
2 parents 55ba384 + 6eb28d6 commit 8a9cc36

File tree

19 files changed

+1121
-535
lines changed

19 files changed

+1121
-535
lines changed

baseTemplate/templates/baseTemplate/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,7 @@ <h3>Notifications</h3>
22212221
<!-- Additional Scripts (data-cfasync=false ensures controllers load before Angular compiles) -->
22222222
<script src="{% static 'packages/packages.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
22232223
<script src="{% static 'websiteFunctions/websiteFunctions.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
2224+
<script src="{% static 'loginSystem/webauthn.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
22242225
<script src="{% static 'userManagment/userManagment.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
22252226
<script src="{% static 'databases/databases.js' %}?v={{ CP_VERSION }}" data-cfasync="false"></script>
22262227
<script src="{% static 'dns/dns.js' %}?v={{ CP_VERSION }}&dns={{ DNS_STATIC_VERSION }}" data-cfasync="false"></script>

deploy-templates.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/bin/bash
2+
# Deploy updated templates (and related static) from this repo to live CyberPanel.
3+
# Use after pulling template changes so the panel at /usr/local/CyberCP shows the new UI.
4+
# Usage: sudo bash deploy-templates.sh
5+
6+
set -e
7+
8+
CYBERCP_ROOT="${CYBERCP_ROOT:-/usr/local/CyberCP}"
9+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10+
11+
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Deploying templates to $CYBERCP_ROOT..."
12+
13+
# userManagment templates
14+
for name in modifyUser createUser; do
15+
SRC="$SCRIPT_DIR/userManagment/templates/userManagment/${name}.html"
16+
DST="$CYBERCP_ROOT/userManagment/templates/userManagment/${name}.html"
17+
if [ -f "$SRC" ]; then
18+
cp -f "$SRC" "$DST"
19+
echo " Copied userManagment ${name}.html"
20+
fi
21+
done
22+
23+
# loginSystem login template
24+
if [ -f "$SCRIPT_DIR/loginSystem/templates/loginSystem/login.html" ]; then
25+
cp -f "$SCRIPT_DIR/loginSystem/templates/loginSystem/login.html" \
26+
"$CYBERCP_ROOT/loginSystem/templates/loginSystem/login.html"
27+
echo " Copied loginSystem login.html"
28+
fi
29+
30+
# Optional: userManagment static (if you change JS)
31+
if [ -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" ]; then
32+
mkdir -p "$CYBERCP_ROOT/userManagment/static/userManagment"
33+
cp -f "$SCRIPT_DIR/userManagment/static/userManagment/userManagment.js" \
34+
"$CYBERCP_ROOT/userManagment/static/userManagment/userManagment.js"
35+
echo " Copied userManagment.js"
36+
fi
37+
if [ -f "$SCRIPT_DIR/loginSystem/static/loginSystem/webauthn.js" ]; then
38+
mkdir -p "$CYBERCP_ROOT/loginSystem/static/loginSystem"
39+
cp -f "$SCRIPT_DIR/loginSystem/static/loginSystem/webauthn.js" \
40+
"$CYBERCP_ROOT/loginSystem/static/loginSystem/webauthn.js"
41+
echo " Copied webauthn.js"
42+
fi
43+
44+
# Run collectstatic if manage.py exists (so static changes are served)
45+
if [ -f "$CYBERCP_ROOT/manage.py" ]; then
46+
PYTHON="${CYBERCP_ROOT}/bin/python"
47+
[ -x "$PYTHON" ] || PYTHON="python3"
48+
echo " Running collectstatic..."
49+
(cd "$CYBERCP_ROOT" && "$PYTHON" manage.py collectstatic --noinput --clear 2>&1) | tail -5
50+
fi
51+
52+
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] Templates deployed. Hard-refresh (Ctrl+F5) on Modify User / Create User / Login if needed."

docs/2FA_AUTHENTICATION_GUIDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ TOTP generates time-based codes that change every 30 seconds. Users scan a QR co
9797
### What is WebAuthn?
9898
WebAuthn is a web standard that enables secure, passwordless authentication using public-key cryptography. It supports biometric authentication, security keys, and device passkeys.
9999

100+
**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://your-server:2087`).
101+
100102
### Setting Up WebAuthn
101103

102104
#### Prerequisites

fix-cyberpanel-500.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
# fix-cyberpanel-500.sh – Apply common fixes for CyberPanel HTTP 500 on login.
3+
# Run on the server: sudo bash fix-cyberpanel-500.sh
4+
# See: to-do/CYBERPANEL-HTTP-500-LOGIN-FIX.md
5+
6+
set -e
7+
LOG="/var/log/cyberpanel_500_fix.log"
8+
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
9+
10+
if [[ $EUID -ne 0 ]]; then
11+
echo "Run as root: sudo bash $0"
12+
exit 1
13+
fi
14+
15+
log "=== CyberPanel 500 fix script started ==="
16+
17+
# 0. Ensure MariaDB is running (common cause: DB down -> 500)
18+
log "Step 0: Ensuring MariaDB is running..."
19+
systemctl start mariadb 2>/dev/null || systemctl start mysql 2>/dev/null || true
20+
systemctl enable mariadb 2>/dev/null || systemctl enable mysql 2>/dev/null || true
21+
log "Step 0 done."
22+
23+
# 1. Remove or neutralize configservercsf (common cause of 500)
24+
log "Step 1: Cleaning configservercsf references..."
25+
rm -rf /usr/local/CyberCP/configservercsf 2>/dev/null || true
26+
rm -f /home/cyberpanel/plugins/configservercsf 2>/dev/null || true
27+
rm -rf /usr/local/CyberCP/public/static/configservercsf 2>/dev/null || true
28+
sed -i '/configservercsf/d' /usr/local/CyberCP/CyberCP/settings.py 2>/dev/null || true
29+
sed -i '/configservercsf/d' /usr/local/CyberCP/CyberCP/urls.py 2>/dev/null || true
30+
log "Step 1 done."
31+
32+
# 2. Clear Python cache
33+
log "Step 2: Clearing __pycache__..."
34+
find /usr/local/CyberCP -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
35+
log "Step 2 done."
36+
37+
# 3. Restart panel and web server
38+
log "Step 3: Restarting lscpd and lsws..."
39+
systemctl restart lscpd
40+
systemctl restart lsws
41+
killall lsphp 2>/dev/null || true
42+
log "Step 3 done."
43+
44+
log "=== Fix script finished. Try https://YOUR_IP:2087 or :8090 ==="
45+
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)."

loginSystem/static/loginSystem/webauthn.js

Lines changed: 104 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class CyberPanelWebAuthn {
1010
this.apiEndpoints = {
1111
registrationStart: '/webauthn/registration/start/',
1212
registrationComplete: '/webauthn/registration/complete/',
13+
authenticationOptions: '/webauthn/authentication/options/',
1314
authenticationStart: '/webauthn/authentication/start/',
1415
authenticationComplete: '/webauthn/authentication/complete/',
1516
credentialsList: '/webauthn/credentials/',
@@ -60,18 +61,10 @@ class CyberPanelWebAuthn {
6061
addLoginButtons() {
6162
const loginForm = document.querySelector('#loginForm');
6263
if (!loginForm) return;
63-
64-
// Add WebAuthn login button
65-
const webauthnButton = document.createElement('button');
66-
webauthnButton.type = 'button';
67-
webauthnButton.className = 'btn btn-primary btn-block';
68-
webauthnButton.innerHTML = '<i class="fas fa-fingerprint"></i> Login with Passkey';
69-
webauthnButton.onclick = () => this.startPasswordlessLogin();
70-
71-
// Insert after password field
72-
const passwordField = loginForm.querySelector('input[type="password"]');
73-
if (passwordField) {
74-
passwordField.parentNode.insertBefore(webauthnButton, passwordField.parentNode.nextSibling);
64+
const existingBtn = document.getElementById('webauthn-login-btn');
65+
if (existingBtn && !existingBtn.dataset.bound) {
66+
existingBtn.dataset.bound = '1';
67+
existingBtn.onclick = () => this.startPasskeyFirstLogin();
7568
}
7669
}
7770

@@ -80,34 +73,90 @@ class CyberPanelWebAuthn {
8073
// Implementation depends on the specific UI structure
8174
}
8275

76+
arrayBufferToBase64url(buffer) {
77+
const bytes = new Uint8Array(buffer);
78+
let binary = '';
79+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
80+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
81+
}
82+
83+
base64urlToArrayBuffer(str) {
84+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
85+
const pad = (4 - (base64.length % 4)) % 4;
86+
for (let i = 0; i < pad; i++) base64 += '=';
87+
const binary = atob(base64);
88+
const bytes = new Uint8Array(binary.length);
89+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
90+
return bytes.buffer;
91+
}
92+
93+
async startPasskeyFirstLogin() {
94+
try {
95+
this.showLoading('Signing in with passkey...');
96+
const optsUrl = this.apiEndpoints.authenticationOptions + '?return=' + encodeURIComponent(window.location.pathname || '/');
97+
const optsResponse = await fetch(optsUrl, { method: 'GET', credentials: 'same-origin' });
98+
const optsData = await optsResponse.json();
99+
if (!optsData.publicKey) {
100+
throw new Error(optsData.error || 'Failed to get options');
101+
}
102+
const publicKey = optsData.publicKey;
103+
publicKey.challenge = this.base64urlToArrayBuffer(publicKey.challenge);
104+
if (publicKey.allowCredentials && publicKey.allowCredentials.length) {
105+
publicKey.allowCredentials = publicKey.allowCredentials.map(function(c) {
106+
return {
107+
type: c.type || 'public-key',
108+
id: typeof c.id === 'string' ? this.base64urlToArrayBuffer(c.id) : c.id,
109+
transports: c.transports
110+
};
111+
}.bind(this));
112+
}
113+
const credential = await navigator.credentials.get({ publicKey });
114+
if (!credential) throw new Error('No credential');
115+
const credentialJson = {
116+
id: credential.id,
117+
rawId: this.arrayBufferToBase64url(credential.rawId),
118+
type: credential.type,
119+
response: {
120+
clientDataJSON: this.arrayBufferToBase64url(credential.response.clientDataJSON),
121+
authenticatorData: this.arrayBufferToBase64url(credential.response.authenticatorData),
122+
signature: this.arrayBufferToBase64url(credential.response.signature),
123+
userHandle: credential.response.userHandle ? this.arrayBufferToBase64url(credential.response.userHandle) : null
124+
}
125+
};
126+
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
127+
credential: credentialJson
128+
});
129+
if (authResponse.success && authResponse.redirect) {
130+
window.location.href = authResponse.redirect;
131+
return;
132+
}
133+
throw new Error(authResponse.error || 'Verification failed');
134+
} catch (error) {
135+
if (error.name === 'NotAllowedError' || (error.message && (error.message.indexOf('cancel') !== -1 || error.message.indexOf('timed out') !== -1))) {
136+
this.hideLoading();
137+
return;
138+
}
139+
console.error('WebAuthn passkey-first error:', error);
140+
this.showError(error.message || 'Passkey sign-in failed');
141+
} finally {
142+
this.hideLoading();
143+
}
144+
}
145+
83146
async startPasswordlessLogin() {
84147
try {
85148
const username = document.querySelector('input[name="username"]').value;
86149
if (!username) {
87150
this.showError('Please enter your username first');
88151
return;
89152
}
90-
91153
this.showLoading('Starting passkey authentication...');
92-
93-
// Get authentication challenge
94-
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, {
95-
username: username
96-
});
97-
154+
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationStart, { username: username });
98155
if (!challengeResponse.success) {
99156
throw new Error(challengeResponse.error || 'Failed to start authentication');
100157
}
101-
102-
// Convert challenge to proper format
103158
const challenge = this.convertChallenge(challengeResponse.challenge);
104-
105-
// Get credential
106-
const credential = await navigator.credentials.get({
107-
publicKey: challenge
108-
});
109-
110-
// Complete authentication
159+
const credential = await navigator.credentials.get({ publicKey: challenge });
111160
const authResponse = await this.makeRequest('POST', this.apiEndpoints.authenticationComplete, {
112161
challenge_id: challengeResponse.challenge_id,
113162
credential: {
@@ -117,19 +166,14 @@ class CyberPanelWebAuthn {
117166
client_data_json: this.arrayBufferToBase64(credential.response.clientDataJSON),
118167
authenticator_data: this.arrayBufferToBase64(credential.response.authenticatorData),
119168
signature: this.arrayBufferToBase64(credential.response.signature),
120-
user_handle: credential.response.userHandle ?
121-
this.arrayBufferToBase64(credential.response.userHandle) : null
169+
user_handle: credential.response.userHandle ? this.arrayBufferToBase64(credential.response.userHandle) : null
122170
});
123-
124171
if (authResponse.success) {
125172
this.showSuccess('Authentication successful! Redirecting...');
126-
setTimeout(() => {
127-
window.location.href = '/';
128-
}, 1000);
173+
setTimeout(() => { window.location.href = authResponse.redirect || '/'; }, 1000);
129174
} else {
130175
throw new Error(authResponse.error || 'Authentication failed');
131176
}
132-
133177
} catch (error) {
134178
console.error('WebAuthn authentication error:', error);
135179
this.showError(error.message || 'Authentication failed');
@@ -138,9 +182,10 @@ class CyberPanelWebAuthn {
138182
}
139183
}
140184

141-
async registerPasskey(username, credentialName = '') {
185+
async registerPasskey(username, credentialName = '', options = {}) {
186+
const silent = options && options.silent === true;
142187
try {
143-
this.showLoading('Starting passkey registration...');
188+
if (!silent) this.showLoading('Starting passkey registration...');
144189

145190
// Get registration challenge
146191
const challengeResponse = await this.makeRequest('POST', this.apiEndpoints.registrationStart, {
@@ -172,18 +217,18 @@ class CyberPanelWebAuthn {
172217
});
173218

174219
if (regResponse.success) {
175-
this.showSuccess('Passkey registered successfully!');
220+
if (!silent) this.showSuccess('Passkey registered successfully!');
176221
return regResponse;
177222
} else {
178223
throw new Error(regResponse.error || 'Registration failed');
179224
}
180225

181226
} catch (error) {
182227
console.error('WebAuthn registration error:', error);
183-
this.showError(error.message || 'Registration failed');
228+
if (!silent) this.showError(error.message || 'Registration failed');
184229
throw error;
185230
} finally {
186-
this.hideLoading();
231+
if (!silent) this.hideLoading();
187232
}
188233
}
189234

@@ -265,23 +310,25 @@ class CyberPanelWebAuthn {
265310
}
266311

267312
convertChallenge(challenge) {
268-
// Convert base64 challenge to ArrayBuffer
269-
const challengeBytes = this.base64ToArrayBuffer(challenge.challenge);
270-
313+
const ch = challenge.challenge;
314+
const challengeBytes = (typeof ch === 'string' && (ch.indexOf('-') !== -1 || ch.indexOf('_') !== -1))
315+
? this.base64urlToArrayBuffer(ch) : this.base64ToArrayBuffer(ch);
316+
const userId = challenge.user && challenge.user.id;
317+
const userIdBuf = !userId ? undefined : (typeof userId === 'string' && (userId.indexOf('-') !== -1 || userId.indexOf('_') !== -1)
318+
? this.base64urlToArrayBuffer(userId) : this.base64ToArrayBuffer(userId));
271319
return {
272320
...challenge,
273321
challenge: challengeBytes,
274-
user: {
275-
...challenge.user,
276-
id: this.base64ToArrayBuffer(challenge.user.id)
277-
},
322+
user: challenge.user ? { ...challenge.user, id: userIdBuf } : undefined,
278323
excludeCredentials: challenge.excludeCredentials?.map(cred => ({
279324
...cred,
280-
id: this.base64ToArrayBuffer(cred.id)
325+
id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1)
326+
? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id)
281327
})) || [],
282328
allowCredentials: challenge.allowCredentials?.map(cred => ({
283329
...cred,
284-
id: this.base64ToArrayBuffer(cred.id)
330+
id: typeof cred.id === 'string' && (cred.id.indexOf('-') !== -1 || cred.id.indexOf('_') !== -1)
331+
? this.base64urlToArrayBuffer(cred.id) : this.base64ToArrayBuffer(cred.id)
285332
})) || []
286333
};
287334
}
@@ -383,12 +430,17 @@ class CyberPanelWebAuthn {
383430
}
384431
}
385432

386-
// Initialize WebAuthn when DOM is loaded
387-
document.addEventListener('DOMContentLoaded', function() {
433+
// Initialize WebAuthn - run now if DOM ready, else on DOMContentLoaded (script often loads after DOM is ready)
434+
function initCyberPanelWebAuthn() {
388435
if (CyberPanelWebAuthn.isSupported()) {
389436
window.cyberPanelWebAuthn = new CyberPanelWebAuthn();
390437
}
391-
});
438+
}
439+
if (document.readyState === 'loading') {
440+
document.addEventListener('DOMContentLoaded', initCyberPanelWebAuthn);
441+
} else {
442+
initCyberPanelWebAuthn();
443+
}
392444

393445
// Export for use in other scripts
394446
if (typeof module !== 'undefined' && module.exports) {

0 commit comments

Comments
 (0)