Add API token authentication support
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
PORT=3000
|
||||
DB_PATH=./mail.db
|
||||
APP_PASSWORD=change-this-password
|
||||
API_TOKEN=change-this-token
|
||||
|
||||
51
README.md
51
README.md
@@ -12,6 +12,7 @@
|
||||
- 支持按需补拉单封邮件正文,避免全量同步过慢
|
||||
- 支持批量导入导出邮箱信息
|
||||
- 支持访问密码保护,未输入密码无法进入功能页面
|
||||
- 支持 API Token 模式,方便纯接口调用
|
||||
- 提供 API 用于发邮件、获取邮件列表、获取最新一封邮件
|
||||
|
||||
## 技术栈
|
||||
@@ -59,6 +60,7 @@ cp .env.example .env
|
||||
PORT=3000
|
||||
DB_PATH=./mail.db
|
||||
APP_PASSWORD=change-this-password
|
||||
API_TOKEN=change-this-token
|
||||
```
|
||||
|
||||
说明:
|
||||
@@ -66,6 +68,9 @@ APP_PASSWORD=change-this-password
|
||||
- `APP_PASSWORD` 用于控制页面和 API 访问
|
||||
- 设置后,必须先在登录页输入正确密码才能进入系统
|
||||
- 如果留空,则不启用密码保护
|
||||
- `API_TOKEN` 用于纯 API 调用认证
|
||||
- 设置后,可以通过请求头直接访问业务 API,无需网页登录
|
||||
- 如果 `APP_PASSWORD` 和 `API_TOKEN` 同时存在,则满足任意一种认证方式即可访问 API
|
||||
|
||||
### 3. 启动服务
|
||||
|
||||
@@ -93,6 +98,12 @@ cp .env.example .env
|
||||
APP_PASSWORD=your-strong-password
|
||||
```
|
||||
|
||||
如果启用 API Token,请在 `.env` 中设置:
|
||||
|
||||
```env
|
||||
API_TOKEN=your-strong-api-token
|
||||
```
|
||||
|
||||
### 2. 构建并启动
|
||||
|
||||
```bash
|
||||
@@ -207,7 +218,19 @@ QQ邮箱----demo@qq.com----abcd1234
|
||||
|
||||
## API 文档
|
||||
|
||||
除 `健康检查`、`登录状态`、`登录接口` 外,其余 API 在启用 `APP_PASSWORD` 时都需要先登录。
|
||||
除 `健康检查`、`登录状态`、`登录接口` 外,其余 API 在启用鉴权时都需要先登录,或提供有效的 API Token。
|
||||
|
||||
API Token 支持两种传法:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <API_TOKEN>
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```text
|
||||
X-API-Token: <API_TOKEN>
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
@@ -221,6 +244,8 @@ curl http://localhost:3000/api/health
|
||||
curl http://localhost:3000/api/auth/status
|
||||
```
|
||||
|
||||
返回结果中会包含 `tokenEnabled` 字段,用于表示是否启用了 API Token。
|
||||
|
||||
### 登录
|
||||
|
||||
```bash
|
||||
@@ -243,6 +268,20 @@ curl -X POST http://localhost:3000/api/auth/logout
|
||||
curl http://localhost:3000/api/channels
|
||||
```
|
||||
|
||||
使用 Bearer Token:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/channels \
|
||||
-H 'Authorization: Bearer your-strong-api-token'
|
||||
```
|
||||
|
||||
使用 `X-API-Token`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/channels \
|
||||
-H 'X-API-Token: your-strong-api-token'
|
||||
```
|
||||
|
||||
### 新增渠道
|
||||
|
||||
```bash
|
||||
@@ -305,6 +344,7 @@ curl -X POST http://localhost:3000/api/accounts/import \
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/send \
|
||||
-H 'Authorization: Bearer your-strong-api-token' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"accountId": 1,
|
||||
@@ -319,19 +359,22 @@ curl -X POST http://localhost:3000/api/send \
|
||||
首次建议带 `refresh=true`,从远程 IMAP 同步最新邮件。
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3000/api/messages?accountId=1&limit=20&refresh=true"
|
||||
curl "http://localhost:3000/api/messages?accountId=1&limit=20&refresh=true" \
|
||||
-H 'Authorization: Bearer your-strong-api-token'
|
||||
```
|
||||
|
||||
### 获取最新一封邮件
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3000/api/messages/latest?accountId=1&refresh=true"
|
||||
curl "http://localhost:3000/api/messages/latest?accountId=1&refresh=true" \
|
||||
-H 'Authorization: Bearer your-strong-api-token'
|
||||
```
|
||||
|
||||
### 获取单封邮件详情
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3000/api/accounts/1/messages/INBOX%3A123?refresh=true"
|
||||
curl "http://localhost:3000/api/accounts/1/messages/INBOX%3A123?refresh=true" \
|
||||
-H 'Authorization: Bearer your-strong-api-token'
|
||||
```
|
||||
|
||||
说明:`uid` 需要使用 URL 编码。
|
||||
|
||||
@@ -13,6 +13,7 @@ services:
|
||||
PORT: 3000
|
||||
DB_PATH: /data/mail.db
|
||||
APP_PASSWORD: ${APP_PASSWORD}
|
||||
API_TOKEN: ${API_TOKEN}
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
|
||||
30
server.js
30
server.js
@@ -11,6 +11,7 @@ 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 apiToken = String(process.env.API_TOKEN || '').trim();
|
||||
const authCookieName = 'mail_sr_auth';
|
||||
const authCookieValue = appPassword
|
||||
? crypto.createHash('sha256').update(appPassword).digest('hex')
|
||||
@@ -22,7 +23,7 @@ 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) });
|
||||
res.json({ ok: true, authenticated: isAuthenticated(req), tokenEnabled: Boolean(apiToken) });
|
||||
});
|
||||
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
@@ -45,7 +46,7 @@ app.post('/api/auth/logout', (_req, res) => {
|
||||
});
|
||||
|
||||
app.use('/api', (req, res, next) => {
|
||||
if (!appPassword) {
|
||||
if (!appPassword && !apiToken) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -53,11 +54,11 @@ app.use('/api', (req, res, next) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!isAuthenticated(req)) {
|
||||
return res.status(401).json({ error: '请先输入访问密码' });
|
||||
if (isApiTokenAuthenticated(req) || isAuthenticated(req)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return next();
|
||||
return res.status(401).json({ error: '请先登录或提供有效的 API Token' });
|
||||
});
|
||||
|
||||
const getChannelStmt = db.prepare('SELECT * FROM channels WHERE id = ?');
|
||||
@@ -422,6 +423,25 @@ function isAuthenticated(req) {
|
||||
return cookies[authCookieName] === authCookieValue;
|
||||
}
|
||||
|
||||
function isApiTokenAuthenticated(req) {
|
||||
if (!apiToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bearerToken = getBearerToken(req.headers.authorization || '');
|
||||
const headerToken = String(req.headers['x-api-token'] || '').trim();
|
||||
return bearerToken === apiToken || headerToken === apiToken;
|
||||
}
|
||||
|
||||
function getBearerToken(authorizationHeader) {
|
||||
const value = String(authorizationHeader || '').trim();
|
||||
if (!value.toLowerCase().startsWith('bearer ')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.slice(7).trim();
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader) {
|
||||
return String(cookieHeader || '')
|
||||
.split(';')
|
||||
|
||||
Reference in New Issue
Block a user