Add password-protected access flow
This commit is contained in:
92
server.js
92
server.js
@@ -1,5 +1,6 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
@@ -9,11 +10,56 @@ const { sendMail, fetchLatestEmails, fetchMessageDetail } = require('./mailServi
|
||||
|
||||
const app = express();
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
const appPassword = String(process.env.APP_PASSWORD || '').trim();
|
||||
const authCookieName = 'mail_sr_auth';
|
||||
const authCookieValue = appPassword
|
||||
? crypto.createHash('sha256').update(appPassword).digest('hex')
|
||||
: '';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.get('/api/auth/status', (req, res) => {
|
||||
res.json({ ok: true, authenticated: isAuthenticated(req) });
|
||||
});
|
||||
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
if (!appPassword) {
|
||||
return res.json({ ok: true, authenticated: true });
|
||||
}
|
||||
|
||||
const password = String(req.body.password || '');
|
||||
if (password !== appPassword) {
|
||||
return res.status(401).json({ error: '密码错误' });
|
||||
}
|
||||
|
||||
res.setHeader('Set-Cookie', serializeAuthCookie(authCookieValue));
|
||||
res.json({ ok: true, authenticated: true });
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', (_req, res) => {
|
||||
res.setHeader('Set-Cookie', serializeAuthCookie('', { maxAge: 0 }));
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.use('/api', (req, res, next) => {
|
||||
if (!appPassword) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.path === '/auth/status' || req.path === '/auth/login' || req.path === '/health') {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isAuthenticated(req)) {
|
||||
return res.status(401).json({ error: '请先输入访问密码' });
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
const getChannelStmt = db.prepare('SELECT * FROM channels WHERE id = ?');
|
||||
const getAccountStmt = db.prepare('SELECT * FROM accounts WHERE id = ?');
|
||||
const listChannelsStmt = db.prepare(`
|
||||
@@ -367,6 +413,52 @@ function serializeAccount(account) {
|
||||
};
|
||||
}
|
||||
|
||||
function isAuthenticated(req) {
|
||||
if (!appPassword) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cookies = parseCookies(req.headers.cookie || '');
|
||||
return cookies[authCookieName] === authCookieValue;
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader) {
|
||||
return String(cookieHeader || '')
|
||||
.split(';')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.reduce((result, item) => {
|
||||
const separatorIndex = item.indexOf('=');
|
||||
if (separatorIndex === -1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const key = item.slice(0, separatorIndex).trim();
|
||||
const value = decodeURIComponent(item.slice(separatorIndex + 1).trim());
|
||||
result[key] = value;
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function serializeAuthCookie(value, options = {}) {
|
||||
const parts = [
|
||||
`${authCookieName}=${encodeURIComponent(value)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
parts.push('Secure');
|
||||
}
|
||||
|
||||
if (typeof options.maxAge === 'number') {
|
||||
parts.push(`Max-Age=${options.maxAge}`);
|
||||
}
|
||||
|
||||
return parts.join('; ');
|
||||
}
|
||||
|
||||
function sanitizeChannelPayload(body) {
|
||||
const data = {
|
||||
name: String(body.name || '').trim(),
|
||||
|
||||
Reference in New Issue
Block a user