diff --git a/app/public/dashboard-login.html b/app/public/dashboard-login.html index bc9f058..082794b 100644 --- a/app/public/dashboard-login.html +++ b/app/public/dashboard-login.html @@ -30,6 +30,7 @@ button:hover { opacity: 0.88; }

Station Dashboard

Sign in to view your weather data

+
@@ -37,9 +38,11 @@ button:hover { opacity: 0.88; }
+ Forgot password? diff --git a/app/public/dashboard-recover.html b/app/public/dashboard-recover.html new file mode 100644 index 0000000..fa83c80 --- /dev/null +++ b/app/public/dashboard-recover.html @@ -0,0 +1,55 @@ + + + + + +Station Dashboard — Recover Access + + + +
+

Recover Access

+

Reset your dashboard password

+
+ Find your recovery token in the server logs — look for a line that says
+ Dashboard recovery token:
+ In CasaOS, open the app's log from the container details panel. The token changes daily. +
+ +
+ + + + + +
+ ← Back to sign in +
+ + + diff --git a/app/server.js b/app/server.js index 0af9794..419d980 100644 --- a/app/server.js +++ b/app/server.js @@ -62,6 +62,11 @@ function hashPassword(password, salt) { return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex'); } +function getDailyRecoveryToken() { + const day = Math.floor(Date.now() / (24 * 60 * 60 * 1000)); + return crypto.createHmac('sha256', SESSION_SECRET).update('recover:' + day).digest('hex').slice(0, 16); +} + function requireDashboardAuth(req, res, next) { const cookies = parseCookies(req); const user = verifySession(cookies.dsession); @@ -856,6 +861,27 @@ app.get('/dashboard/logout', (_req, res) => { res.redirect('/dashboard/login'); }); +// Password recovery +app.get('/dashboard/recover', (_req, res) => { + if (!dashAuth) return res.redirect('/dashboard/setup'); + res.sendFile(path.join(__dirname, 'public', 'dashboard-recover.html')); +}); + +app.post('/dashboard/recover', (req, res) => { + if (!dashAuth) return res.redirect('/dashboard/setup'); + const { token, password } = req.body || {}; + if (!token || !password || password.length < 6) return res.redirect('/dashboard/recover?error=1'); + const expected = getDailyRecoveryToken(); + let match = false; + try { match = crypto.timingSafeEqual(Buffer.from(token.trim()), Buffer.from(expected)); } catch {} + if (!match) return res.redirect('/dashboard/recover?error=1'); + const salt = crypto.randomBytes(16).toString('hex'); + const hash = hashPassword(password, salt); + dashAuth = { ...dashAuth, salt, hash }; + try { fs.writeFileSync(DASH_AUTH_FILE, JSON.stringify(dashAuth)); } catch {} + res.redirect('/dashboard/login?recovered=1'); +}); + // Main dashboard app.get('/dashboard', requireDashboardAuth, (_req, res) => { if (!dashAuth) return res.redirect('/dashboard/setup'); @@ -1144,4 +1170,10 @@ app.get('/api/conditions/public', (_req, res) => { res.json(cached?.data?.[0]?.lastData || null); }); -app.listen(PORT, () => console.log(`Weather app running on port ${PORT}`)); +app.listen(PORT, () => { + console.log(`Weather app running on port ${PORT}`); + if (dashAuth) { + console.log(`Dashboard recovery token (changes daily): ${getDailyRecoveryToken()}`); + console.log(` Use at http://localhost:${PORT}/dashboard/recover`); + } +});