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