Skip to content

Commit 2c40157

Browse files
authored
Merge pull request #48
add auth
2 parents 6ce1926 + 72c01e4 commit 2c40157

File tree

6 files changed

+783
-7
lines changed

6 files changed

+783
-7
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,8 @@ DAYS=20
3535

3636
# Duration in seconds to show activity star after new trades (default: 10)
3737
NOTIFY=10
38+
39+
# Password protection for dashboard access (optional)
40+
# If set, users must enter this password to view the dashboard
41+
# Leave commented out or empty to disable password protection
42+
#PASSWORD=your_password

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ SUMMARY=OFF # Show summary stats bar at top (ON/OFF, default: OFF)
7070
SOUND=ON # Play chime sound for new trade notifications (ON/OFF, default: ON)
7171
DAYS=20 # Number of days to show in charts (default: 20)
7272
NOTIFY=10 # Duration in seconds to show activity star after new trades (default: 10)
73+
74+
# Security
75+
PASSWORD=your_password # Password protection for dashboard access (optional, leave empty to disable)
7376
```
7477

7578
### 4. FreqTrade API Configuration
@@ -189,7 +192,7 @@ freqmon/
189192
2. Use a VPN or SSH tunnel for remote access
190193
3. Enable HTTPS if accessible outside localhost
191194
4. Use strong, unique passwords for each FreqTrade instance
192-
5. Consider adding authentication to the dashboard itself
195+
5. Enable dashboard password protection by setting `PASSWORD` in `.env`
193196

194197
## License
195198

api.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,32 @@
2424
$config = Config::getInstance();
2525
date_default_timezone_set($config->getTimezone());
2626

27+
$action = isset($_GET['action']) ? $_GET['action'] : '';
28+
29+
if ($action === 'check_auth') {
30+
$password = $config->getPassword();
31+
echo json_encode([
32+
'success' => true,
33+
'auth_required' => $password !== null,
34+
]);
35+
exit;
36+
}
37+
38+
if ($action === 'verify_password') {
39+
$input = json_decode(file_get_contents('php://input'), true);
40+
$providedPassword = $input['password'] ?? '';
41+
$configPassword = $config->getPassword();
42+
43+
if ($configPassword === null || $providedPassword === $configPassword) {
44+
echo json_encode(['success' => true]);
45+
} else {
46+
echo json_encode(['success' => false, 'error' => 'Invalid password']);
47+
}
48+
exit;
49+
}
50+
2751
// Handle pair_candles request
28-
if (isset($_GET['action']) && $_GET['action'] === 'pair_candles') {
52+
if ($action === 'pair_candles') {
2953
ob_clean(); // Clear any previous output
3054

3155
$serverNum = isset($_GET['server']) ? intval($_GET['server']) : 1;

index.php

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,68 @@
558558
</div>
559559
</div>
560560
561+
<!-- Password Modal -->
562+
<div class="modal-overlay" id="passwordModal">
563+
<div class="password-modal-content">
564+
<div class="password-title">Authentication Required</div>
565+
<form id="passwordForm" onsubmit="return submitPassword(event)">
566+
<input type="password" id="passwordInput" class="password-input" placeholder="Enter password" autocomplete="current-password">
567+
<div id="passwordError" class="password-error"></div>
568+
<button type="submit" class="password-submit">Unlock</button>
569+
</form>
570+
</div>
571+
</div>
572+
573+
<style>
574+
.password-modal-content {
575+
background: var(--bg-secondary);
576+
border: 1px solid var(--border-color);
577+
border-radius: 8px;
578+
padding: 1.5rem;
579+
width: 280px;
580+
text-align: center;
581+
}
582+
.password-title {
583+
font-size: 0.9rem;
584+
font-weight: 600;
585+
margin-bottom: 1rem;
586+
color: #fff;
587+
}
588+
.password-input {
589+
width: 100%;
590+
padding: 0.5rem;
591+
font-size: 0.8rem;
592+
border: 1px solid var(--border-color);
593+
border-radius: 4px;
594+
background: var(--bg-tertiary);
595+
color: var(--text-primary);
596+
margin-bottom: 0.5rem;
597+
}
598+
.password-input:focus {
599+
outline: none;
600+
border-color: var(--accent-blue);
601+
}
602+
.password-error {
603+
color: var(--accent-red);
604+
font-size: 0.7rem;
605+
min-height: 1rem;
606+
margin-bottom: 0.5rem;
607+
}
608+
.password-submit {
609+
width: 100%;
610+
padding: 0.5rem;
611+
font-size: 0.8rem;
612+
border: none;
613+
border-radius: 4px;
614+
background: var(--accent-blue);
615+
color: #fff;
616+
cursor: pointer;
617+
}
618+
.password-submit:hover {
619+
opacity: 0.9;
620+
}
621+
</style>
622+
561623
<!-- Loading Modal -->
562624
<div class="modal-overlay" id="loadingModal">
563625
<div class="loading-modal-content">
@@ -2108,11 +2170,78 @@ function createBalanceChart(serverId, trades, currentBalance, dailyData) {
21082170
}
21092171
}
21102172
2111-
// Initial load
2112-
loadDashboard();
2113-
2114-
// Auto refresh
2115-
refreshInterval = setInterval(loadDashboard, REFRESH_SECONDS * 1000);
2173+
function getCookie(name) {
2174+
const value = `; ${document.cookie}`;
2175+
const parts = value.split(`; ${name}=`);
2176+
if (parts.length === 2) return parts.pop().split(';').shift();
2177+
return null;
2178+
}
2179+
2180+
function setCookie(name, value, days) {
2181+
const expires = new Date(Date.now() + days * 864e5).toUTCString();
2182+
document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Strict`;
2183+
}
2184+
2185+
async function checkAuth() {
2186+
const response = await fetch('api.php?action=check_auth');
2187+
const result = await response.json();
2188+
return result.auth_required;
2189+
}
2190+
2191+
async function submitPassword(e) {
2192+
e.preventDefault();
2193+
const password = document.getElementById('passwordInput').value;
2194+
const errorEl = document.getElementById('passwordError');
2195+
2196+
const response = await fetch('api.php?action=verify_password', {
2197+
method: 'POST',
2198+
headers: { 'Content-Type': 'application/json' },
2199+
body: JSON.stringify({ password })
2200+
});
2201+
const result = await response.json();
2202+
2203+
if (result.success) {
2204+
setCookie('freqmon_auth', btoa(password), 30);
2205+
document.getElementById('passwordModal').classList.remove('show');
2206+
startDashboard();
2207+
} else {
2208+
errorEl.textContent = 'Invalid password';
2209+
document.getElementById('passwordInput').value = '';
2210+
document.getElementById('passwordInput').focus();
2211+
}
2212+
return false;
2213+
}
2214+
2215+
function startDashboard() {
2216+
loadDashboard();
2217+
refreshInterval = setInterval(loadDashboard, REFRESH_SECONDS * 1000);
2218+
}
2219+
2220+
async function initApp() {
2221+
const authRequired = await checkAuth();
2222+
2223+
if (authRequired) {
2224+
const savedAuth = getCookie('freqmon_auth');
2225+
if (savedAuth) {
2226+
const response = await fetch('api.php?action=verify_password', {
2227+
method: 'POST',
2228+
headers: { 'Content-Type': 'application/json' },
2229+
body: JSON.stringify({ password: atob(savedAuth) })
2230+
});
2231+
const result = await response.json();
2232+
if (result.success) {
2233+
startDashboard();
2234+
return;
2235+
}
2236+
}
2237+
document.getElementById('passwordModal').classList.add('show');
2238+
document.getElementById('passwordInput').focus();
2239+
} else {
2240+
startDashboard();
2241+
}
2242+
}
2243+
2244+
initApp();
21162245
</script>
21172246
21182247
<footer class="site-footer">

0 commit comments

Comments
 (0)