Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Build stage for frontend
FROM node:20-alpine AS frontend-builder

WORKDIR /app/web

# Copy frontend package files
COPY web/MS_OAuth2API_Next_Web/package*.json ./

# Install frontend dependencies
RUN npm install

# Copy frontend source code
COPY web/MS_OAuth2API_Next_Web/ ./

# Build frontend (skip type-check for faster builds)
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "skip type-check for faster builds" is misleading because npm run build-only is a standard Next.js/Vite command that naturally skips type checking (unlike npm run build). Consider removing or clarifying this comment to avoid confusion.

Suggested change
# Build frontend (skip type-check for faster builds)
# Build frontend using the build-only script

Copilot uses AI. Check for mistakes.
RUN npm run build-only

# Production stage for backend
FROM node:20-alpine

# Install wget for healthcheck
RUN apk add --no-cache wget

WORKDIR /app

# Copy backend package files
COPY package*.json ./

# Install backend dependencies (production only)
RUN npm install --omit=dev

# Copy backend source code
COPY config ./config
COPY controllers ./controllers
COPY middlewares ./middlewares
COPY routes ./routes
COPY services ./services
COPY utils ./utils
COPY main.js ./

# Copy built frontend assets to public directory
RUN mkdir -p public
COPY --from=frontend-builder /app/web/dist ./public

# Create logs directory with proper permissions
RUN mkdir -p logs && chmod 755 logs

# Set environment defaults
ENV NODE_ENV=production
ENV PORT=13000

# Expose port
EXPOSE 13000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:13000/ || exit 1

# Start command
CMD ["node", "main.js"]
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This container runs node main.js as the root user because no non-root user is configured in this Dockerfile. If an attacker ever gains code execution in the Node process (for example via a framework or dependency vulnerability), they will have root privileges inside the container, which increases the risk of container escape and damage to any mounted volumes. Create and switch to a dedicated unprivileged user via USER before starting the app to limit the blast radius of a compromise.

Copilot uses AI. Check for mistakes.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,58 @@
- 支持邮箱验证
- 支持邮箱清空
- 支持邮件查看
- 支持Docker部署(TODO 待实现)
- 支持Docker部署

### Docker 部署

#### 快速开始

1. **构建并启动**
```bash
docker-compose up -d --build
```

2. **查看日志**
```bash
docker-compose logs -f app
```

3. **停止服务**
```bash
docker-compose down
```

4. **重启服务**
```bash
docker-compose restart
```

#### 端口映射

| 服务 | 容器内部端口 | 外部映射端口 |
|------|-------------|-------------|
| App | 13000 | 13000 |
| Redis| 6379 | 16379 |
| MySQL| 3306 | 13306 |

#### 启用 MySQL (可选)

如需使用 MySQL,编辑 `docker-compose.yml`:
1. 取消 `mysql` 服务的注释
2. 取消 `app` 服务中 `DB_*` 环境变量的注释
3. 取消 `depends_on` 中 `mysql` 的注释
4. 取消 `volumes` 中 `mysql_data` 的注释

#### 环境变量配置

参考 `.env.example` 文件了解所有可用配置项。在 Docker 环境中,环境变量通过 `docker-compose.yml` 配置。

#### 注意事项
- 应用服务端口: `13000`
- Redis 外部端口: `16379` (容器内 `6379`)
- MySQL 外部端口: `13306` (容器内 `3306`)
- Redis 数据会持久化到 Docker Volume
- 首次构建需编译前端,可能需要几分钟

### 使用说明
- Redis 配置
Expand Down
78 changes: 78 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
services:
app:
build: .
ports:
- "13000:13000"
environment:
- NODE_ENV=production
- PORT=13000
- USE_REDIS=1
- REDIS_HOST=redis
- REDIS_PORT=6379
## ===== MySQL Config (uncomment below lines to enable) =====
#- DB_HOST=mysql
#- DB_PORT=3306
#- DB_USER=root
#- DB_PASSWORD=ms_oauth2api_root
#- DB_DATABASE=ms_oauth2api
#- DB_WAIT_FOR_CONNECTIONS=true
#- DB_CONNECTION_LIMIT=10
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DB_QUEUE_LIMIT environment variable is commented out in the Docker configuration but is still referenced in the db.js file with a fallback value. Consider adding this environment variable to the commented MySQL configuration section for completeness and consistency.

Suggested change
#- DB_CONNECTION_LIMIT=10
#- DB_CONNECTION_LIMIT=10
#- DB_QUEUE_LIMIT=50

Copilot uses AI. Check for mistakes.
depends_on:
- redis
#- mysql
restart: unless-stopped
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:13000/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

redis:
image: redis:alpine
ports:
- "16379:6379"
restart: unless-stopped
networks:
- app-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- redis_data:/data
command: redis-server --appendonly yes

## ===== MySQL Service (uncomment below to enable) =====
#mysql:
# image: mysql:8.0
# ports:
# - "13306:3306"
# environment:
# - MYSQL_ROOT_PASSWORD=ms_oauth2api_root
# - MYSQL_DATABASE=ms_oauth2api
# - MYSQL_CHARACTER_SET_SERVER=utf8mb4
# - MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci
# restart: unless-stopped
# networks:
# - app-network
# healthcheck:
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pms_oauth2api_root"]
# interval: 10s
# timeout: 5s
# retries: 10
# start_period: 30s
# volumes:
# - mysql_data:/var/lib/mysql
# command: --default-authentication-plugin=mysql_native_password

networks:
app-network:
driver: bridge

volumes:
redis_data:
#mysql_data:
5 changes: 4 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ const config = require('./config')
const logger = require('./utils/logger')
app.use(require('./middlewares/logger'))

// 初始化数据库连接(如果配置了的话)
require('./utils/db')

// 错误处理
const errorHandler = require('./middlewares/error')
app.use(errorHandler)

// 静态资源
app.use(static(path.join(__dirname, '../public')))
app.use(static(path.join(__dirname, './public')))

// 请求体解析
app.use(koaBody({
Expand Down
39 changes: 21 additions & 18 deletions services/BaseService.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class BaseService {
* @returns {Promise<Array>} 查询结果
*/
async query(sql, params = []) {
if (!pool) {
throw new Error('Database not configured. Please set DB_HOST and DB_DATABASE environment variables.');
}
try {
const [rows] = await pool.query(sql, params);
return rows;
Expand All @@ -44,7 +47,7 @@ class BaseService {
*/
async findAll(fields = ['*']) {
if (!validateFieldName(fields) && fields[0] !== '*') {
throw new Error('Invalid field names');
throw new Error('Invalid field names');
}
const fieldList = fields.join(', ');
return this.query(`SELECT ${fieldList} FROM ??`, [this.tableName]);
Expand Down Expand Up @@ -78,15 +81,15 @@ class BaseService {
if (!validateFieldName(fields)) {
throw new Error('Invalid field names');
}

const values = fields.map(field => data[field]);
const placeholders = fields.map(() => '?').join(', ');

const [result] = await pool.query(
`INSERT INTO ?? (??) VALUES (${placeholders})`,
[this.tableName, fields, ...values]
);
Comment on lines 88 to 91
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method directly uses pool.query() without checking if pool is null. Since the database is now optional, this should use this.query() instead to ensure consistent error handling when the database is not configured.

Copilot uses AI. Check for mistakes.

return result.insertId;
}

Expand All @@ -101,15 +104,15 @@ class BaseService {
if (!validateFieldName(fields)) {
throw new Error('Invalid field names');
}

const setClause = fields.map(field => `${field} = ?`).join(', ');
const values = fields.map(field => data[field]);

const [result] = await pool.query(
`UPDATE ?? SET ${setClause} WHERE id = ?`,
[this.tableName, ...values, id]
);
Comment on lines 111 to 114
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method directly uses pool.query() without checking if pool is null. Since the database is now optional, this should use this.query() instead to ensure consistent error handling when the database is not configured.

Copilot uses AI. Check for mistakes.

return result.affectedRows > 0;
}

Expand Down Expand Up @@ -139,40 +142,40 @@ class BaseService {
offset = 0,
orderBy = 'id ASC'
} = options;

if (!validateFieldName(fields) && fields[0] !== '*') {
throw new Error('Invalid field names');
}

const conditionFields = Object.keys(conditions);
if (!validateFieldName(conditionFields)) {
throw new Error('Invalid condition field names');
}

const fieldList = fields.join(', ');
let whereClause = '1 = 1';
const values = [];

if (conditionFields.length > 0) {
whereClause = conditionFields.map(field => {
values.push(conditions[field]);
return `${field} = ?`;
}).join(' AND ');
}

// 验证 orderBy
const orderByParts = orderBy.split(' ');
if (orderByParts.length > 2 || (orderByParts[1] && !['ASC', 'DESC'].includes(orderByParts[1].toUpperCase()))) {
throw new Error('Invalid orderBy parameter');
}

const sql = `
SELECT ${fieldList} FROM ??
WHERE ${whereClause}
ORDER BY ${orderBy}
LIMIT ? OFFSET ?
`;

return this.query(sql, [this.tableName, ...values, limit, offset]);
}

Expand All @@ -186,22 +189,22 @@ class BaseService {
if (conditionFields.length > 0 && !validateFieldName(conditionFields)) {
throw new Error('Invalid condition field names');
}

let whereClause = '1 = 1';
const values = [];

if (conditionFields.length > 0) {
whereClause = conditionFields.map(field => {
values.push(conditions[field]);
return `${field} = ?`;
}).join(' AND ');
}

const [result] = await pool.query(
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method directly uses pool.query() without checking if pool is null. Since the database is now optional, this should use this.query() instead to ensure consistent error handling when the database is not configured.

Suggested change
const [result] = await pool.query(
const [result] = await this.query(

Copilot uses AI. Check for mistakes.
`SELECT COUNT(*) AS count FROM ?? WHERE ${whereClause}`,
[this.tableName, ...values]
);

return result[0].count;
}

Expand Down
28 changes: 18 additions & 10 deletions utils/db.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
waitForConnections: process.env.DB_WAIT_FOR_CONNECTIONS,
connectionLimit: process.env.DB_CONNECTION_LIMIT,
queueLimit: process.env.DB_QUEUE_LIMIT,
});
let pool = null;

// Only create pool if database configuration is provided
if (process.env.DB_HOST && process.env.DB_DATABASE) {
pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT || 3306,
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The port value should be converted to an integer. Currently, if DB_PORT is provided as a string environment variable, it will be passed as a string to mysql.createPool(). Use parseInt(process.env.DB_PORT) || 3306 to ensure proper type conversion.

Suggested change
port: process.env.DB_PORT || 3306,
port: parseInt(process.env.DB_PORT) || 3306,

Copilot uses AI. Check for mistakes.
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_DATABASE,
waitForConnections: process.env.DB_WAIT_FOR_CONNECTIONS !== 'false',
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT) || 10,
queueLimit: parseInt(process.env.DB_QUEUE_LIMIT) || 0,
});
console.log('✅ MySQL pool created');
} else {
console.log('ℹ️ MySQL not configured, database features disabled');
}

module.exports = pool;
4 changes: 2 additions & 2 deletions web/MS_OAuth2API_Next_Web/src/views/email/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ const parseFileContent = (file: UploadRawFile) => {
return
}

const matches = content.split('\n');
const matches = content.split('\n').map(item => item.trim()).filter(item => item !== '');

if (matches) {
if (matches.length > 0) {
emailList.value = matches
} else {
ElMessage.warning('未在文件中找到邮箱地址')
Expand Down