Initial commit
This commit is contained in:
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 環境變數文件
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Python 相關
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# 虛擬環境
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE 相關
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# 作業系統相關
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 日誌文件
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# 測試相關
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# 資料庫相關
|
||||||
|
*.db
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# 敏感資料
|
||||||
|
data/*.xlsx
|
||||||
|
data/*.csv
|
||||||
|
*.bak
|
77
README.md
Normal file
77
README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Panjit 密碼管理系統 API
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
這是一個為 Panjit 公司設計的密碼管理系統 API,提供員工登入驗證和密碼修改功能。
|
||||||
|
|
||||||
|
## 功能特色
|
||||||
|
|
||||||
|
- 🔐 安全的密碼雜湊技術
|
||||||
|
- 👤 支援多種登入方式
|
||||||
|
- 🔄 密碼修改功能
|
||||||
|
- 📚 完整的 API 文件
|
||||||
|
- ⚙️ 環境變數配置
|
||||||
|
- 🛡️ 密碼強度驗證
|
||||||
|
|
||||||
|
## 安裝與設定
|
||||||
|
|
||||||
|
### 1. 安裝依賴套件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 環境變數配置
|
||||||
|
|
||||||
|
複製 `env_example.txt` 為 `.env` 並根據實際環境修改相應的值。
|
||||||
|
|
||||||
|
### 3. 初始化資料庫
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python excel_to_db.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 啟動服務
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 文件
|
||||||
|
|
||||||
|
啟動服務後,可透過以下網址查看完整的 API 文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:12023/api/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 密碼規則
|
||||||
|
|
||||||
|
- 長度:6-50 個字元
|
||||||
|
- 必須包含:至少一個字母和一個數字
|
||||||
|
- 不能與舊密碼相同
|
||||||
|
|
||||||
|
## 安全特性
|
||||||
|
|
||||||
|
- 所有密碼使用安全雜湊技術儲存
|
||||||
|
- 支援多種登入方式
|
||||||
|
- 完整的錯誤處理和狀態碼
|
||||||
|
- 環境變數配置敏感資訊
|
||||||
|
|
||||||
|
## 開發工具
|
||||||
|
|
||||||
|
- **測試腳本**:`test_api.py`、`test_change_password.py`
|
||||||
|
- **資料庫重置**:`reset_database.py`
|
||||||
|
- **資料匯入**:`excel_to_db.py`
|
||||||
|
|
||||||
|
## 技術棧
|
||||||
|
|
||||||
|
- **後端框架**:Flask
|
||||||
|
- **資料庫**:MySQL
|
||||||
|
- **密碼雜湊**:JWT
|
||||||
|
- **API 文件**:Swagger UI
|
||||||
|
- **配置管理**:python-dotenv
|
||||||
|
|
||||||
|
## 授權
|
||||||
|
|
||||||
|
此專案為 Panjit 公司內部使用。
|
50
SECURITY.md
Normal file
50
SECURITY.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 安全說明
|
||||||
|
|
||||||
|
## 重要安全注意事項
|
||||||
|
|
||||||
|
### 環境變數配置
|
||||||
|
|
||||||
|
1. **絕對不要**將 `.env` 文件提交到版本控制系統
|
||||||
|
2. **絕對不要**在公開文件中暴露真實的資料庫連接資訊
|
||||||
|
3. **絕對不要**在 README 或其他文件中包含真實的 API 測試範例
|
||||||
|
|
||||||
|
### 資料庫安全
|
||||||
|
|
||||||
|
- 使用強密碼保護資料庫
|
||||||
|
- 定期更換 JWT 密鑰
|
||||||
|
- 限制資料庫訪問權限
|
||||||
|
- 定期備份資料庫
|
||||||
|
|
||||||
|
### API 安全
|
||||||
|
|
||||||
|
- 所有密碼都經過安全雜湊處理
|
||||||
|
- 使用 HTTPS 進行生產環境部署
|
||||||
|
- 實施適當的速率限制
|
||||||
|
- 記錄所有 API 訪問日誌
|
||||||
|
|
||||||
|
### 部署安全
|
||||||
|
|
||||||
|
- 在生產環境中關閉 DEBUG 模式
|
||||||
|
- 使用環境變數管理敏感配置
|
||||||
|
- 定期更新依賴套件
|
||||||
|
- 實施適當的防火牆規則
|
||||||
|
|
||||||
|
## 配置檢查清單
|
||||||
|
|
||||||
|
在部署前,請確認:
|
||||||
|
|
||||||
|
- [ ] `.env` 文件已正確配置且未提交到版本控制
|
||||||
|
- [ ] 資料庫連接資訊已更新為生產環境
|
||||||
|
- [ ] JWT 密鑰已更改為強密碼
|
||||||
|
- [ ] DEBUG 模式已關閉
|
||||||
|
- [ ] 所有測試資料已清除
|
||||||
|
- [ ] API 文件中的範例已移除敏感資訊
|
||||||
|
|
||||||
|
## 緊急處理
|
||||||
|
|
||||||
|
如果發現安全問題:
|
||||||
|
|
||||||
|
1. 立即更改所有密碼和密鑰
|
||||||
|
2. 檢查系統日誌
|
||||||
|
3. 通知相關人員
|
||||||
|
4. 實施必要的安全措施
|
683
app.py
Normal file
683
app.py
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_swagger_ui import get_swaggerui_blueprint
|
||||||
|
from flask_cors import CORS
|
||||||
|
import pymysql
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import base64
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# 啟用 CORS 支援
|
||||||
|
CORS(app, resources={
|
||||||
|
r"/api/*": {
|
||||||
|
"origins": "*",
|
||||||
|
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
"allow_headers": ["Content-Type", "Authorization"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 使用配置類別
|
||||||
|
DB_CONFIG = Config.get_db_config()
|
||||||
|
JWT_SECRET = Config.JWT_SECRET
|
||||||
|
|
||||||
|
# Swagger UI 配置
|
||||||
|
SWAGGER_URL = '/api/docs' # Swagger UI 的 URL
|
||||||
|
API_URL = '/api/swagger.json' # API 文件的 JSON 位置
|
||||||
|
|
||||||
|
# 建立 Swagger UI Blueprint
|
||||||
|
swaggerui_blueprint = get_swaggerui_blueprint(
|
||||||
|
SWAGGER_URL,
|
||||||
|
API_URL,
|
||||||
|
config={
|
||||||
|
'app_name': Config.APP_NAME,
|
||||||
|
'app_version': Config.APP_VERSION,
|
||||||
|
'description': Config.APP_DESCRIPTION
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 註冊 Swagger UI Blueprint
|
||||||
|
app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)
|
||||||
|
|
||||||
|
@app.route('/api/swagger.json')
|
||||||
|
def swagger():
|
||||||
|
"""
|
||||||
|
Swagger API 文件 JSON
|
||||||
|
"""
|
||||||
|
return jsonify({
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"title": Config.APP_NAME,
|
||||||
|
"version": Config.APP_VERSION,
|
||||||
|
"description": Config.APP_DESCRIPTION,
|
||||||
|
"contact": {
|
||||||
|
"name": "Panjit IT Department",
|
||||||
|
"email": "it@panjit.com.tw"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"host": f"{Config.API_HOST}:{Config.API_PORT}",
|
||||||
|
"basePath": "/api",
|
||||||
|
"schemes": ["http", "https"],
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"paths": {
|
||||||
|
"/login": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Authentication"],
|
||||||
|
"summary": "用戶登入驗證",
|
||||||
|
"description": "使用帳號和密碼進行登入驗證",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": True,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"account": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "用戶帳號(loginid 或 email)",
|
||||||
|
"example": "92460"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "用戶密碼",
|
||||||
|
"example": "Panjit92460"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["account", "password"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "登入成功",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "integer"},
|
||||||
|
"loginid": {"type": "string"},
|
||||||
|
"username": {"type": "string"},
|
||||||
|
"email": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "登入失敗",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/change-password": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["Password Management"],
|
||||||
|
"summary": "修改用戶密碼",
|
||||||
|
"description": "修改用戶的登入密碼",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": True,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"account": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "用戶帳號(loginid 或 email)",
|
||||||
|
"example": "92460"
|
||||||
|
},
|
||||||
|
"old_password": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "舊密碼",
|
||||||
|
"example": "Panjit92460"
|
||||||
|
},
|
||||||
|
"new_password": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "新密碼(6-50字元,需包含字母和數字)",
|
||||||
|
"example": "MyNewPassword123"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["account", "old_password", "new_password"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "密碼修改成功",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "請求錯誤",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "舊密碼不正確",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["System"],
|
||||||
|
"summary": "系統健康檢查",
|
||||||
|
"description": "檢查 API 服務和資料庫連線狀態",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "系統正常",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"database": {"type": "string"},
|
||||||
|
"timestamp": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/{account}": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["User Management"],
|
||||||
|
"summary": "查詢用戶資訊",
|
||||||
|
"description": "根據帳號查詢用戶基本資訊",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "account",
|
||||||
|
"in": "path",
|
||||||
|
"required": True,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用戶帳號(loginid 或 email)",
|
||||||
|
"example": "92460"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "查詢成功",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "integer"},
|
||||||
|
"loginid": {"type": "string"},
|
||||||
|
"username": {"type": "string"},
|
||||||
|
"email": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "用戶不存在",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean"},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"Error": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {"type": "boolean", "example": False},
|
||||||
|
"message": {"type": "string"},
|
||||||
|
"error_code": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"User": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "integer", "example": 1},
|
||||||
|
"loginid": {"type": "string", "example": "92460"},
|
||||||
|
"username": {"type": "string", "example": "丁于軒"},
|
||||||
|
"email": {"type": "string", "example": "sharnading@panjit.com.tw"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def verify_password(input_password, hashed_password):
|
||||||
|
"""
|
||||||
|
驗證密碼是否正確
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 解碼 JWT token
|
||||||
|
decoded = jwt.decode(hashed_password, JWT_SECRET, algorithms=['HS256'])
|
||||||
|
|
||||||
|
# 檢查 token 類型
|
||||||
|
if decoded.get('type') != 'password_hash':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 比較密碼
|
||||||
|
stored_password = decoded.get('password', '')
|
||||||
|
return input_password == stored_password
|
||||||
|
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
print("JWT token 已過期")
|
||||||
|
return False
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
print("無效的 JWT token")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"密碼驗證錯誤: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def hash_password_with_jwt(password):
|
||||||
|
"""
|
||||||
|
使用 JWT 將密碼進行雜湊處理
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 建立 JWT payload,包含密碼和時間戳
|
||||||
|
payload = {
|
||||||
|
'password': password,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'type': 'password_hash'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 使用 JWT 編碼密碼
|
||||||
|
token = jwt.encode(payload, JWT_SECRET, algorithm='HS256')
|
||||||
|
return token
|
||||||
|
except Exception as e:
|
||||||
|
print(f"密碼雜湊錯誤: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_password_strength(password):
|
||||||
|
"""
|
||||||
|
驗證密碼強度
|
||||||
|
"""
|
||||||
|
if len(password) < 6:
|
||||||
|
return False, "密碼長度至少需要 6 個字元"
|
||||||
|
|
||||||
|
if len(password) > 50:
|
||||||
|
return False, "密碼長度不能超過 50 個字元"
|
||||||
|
|
||||||
|
# 檢查是否包含至少一個字母和一個數字
|
||||||
|
has_letter = any(c.isalpha() for c in password)
|
||||||
|
has_digit = any(c.isdigit() for c in password)
|
||||||
|
|
||||||
|
if not has_letter:
|
||||||
|
return False, "密碼必須包含至少一個字母"
|
||||||
|
|
||||||
|
if not has_digit:
|
||||||
|
return False, "密碼必須包含至少一個數字"
|
||||||
|
|
||||||
|
return True, "密碼強度符合要求"
|
||||||
|
|
||||||
|
def update_user_password(loginid_or_email, old_password, new_password):
|
||||||
|
"""
|
||||||
|
更新用戶密碼
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
connection = pymysql.connect(**DB_CONFIG)
|
||||||
|
cursor = connection.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
|
# 先查詢用戶資料
|
||||||
|
query = """
|
||||||
|
SELECT id, loginid, username, email, hashed_password
|
||||||
|
FROM user_accounts
|
||||||
|
WHERE loginid = %s OR email = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query, (loginid_or_email, loginid_or_email))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return False, "用戶不存在"
|
||||||
|
|
||||||
|
# 驗證舊密碼
|
||||||
|
if not verify_password(old_password, user['hashed_password']):
|
||||||
|
return False, "舊密碼不正確"
|
||||||
|
|
||||||
|
# 驗證新密碼強度
|
||||||
|
is_valid, message = validate_password_strength(new_password)
|
||||||
|
if not is_valid:
|
||||||
|
return False, message
|
||||||
|
|
||||||
|
# 生成新密碼的雜湊
|
||||||
|
new_hashed_password = hash_password_with_jwt(new_password)
|
||||||
|
if not new_hashed_password:
|
||||||
|
return False, "密碼雜湊失敗"
|
||||||
|
|
||||||
|
# 更新密碼
|
||||||
|
update_query = """
|
||||||
|
UPDATE user_accounts
|
||||||
|
SET hashed_password = %s, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(update_query, (new_hashed_password, user['id']))
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return True, "密碼修改成功"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"更新密碼錯誤: {e}")
|
||||||
|
return False, f"系統錯誤: {str(e)}"
|
||||||
|
finally:
|
||||||
|
if 'connection' in locals():
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
def get_user_by_loginid_or_email(loginid_or_email):
|
||||||
|
"""
|
||||||
|
根據 loginid 或 email 查詢用戶資料
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
connection = pymysql.connect(**DB_CONFIG)
|
||||||
|
cursor = connection.cursor(pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
|
# 查詢用戶資料
|
||||||
|
query = """
|
||||||
|
SELECT id, loginid, username, email, hashed_password
|
||||||
|
FROM user_accounts
|
||||||
|
WHERE loginid = %s OR email = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query, (loginid_or_email, loginid_or_email))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"資料庫查詢錯誤: {e}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
if 'connection' in locals():
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
@app.route('/api/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
"""
|
||||||
|
登入 API
|
||||||
|
接收 JSON 格式: {"account": "帳號或email", "password": "密碼"}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 取得請求資料
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '請提供 JSON 格式的資料',
|
||||||
|
'error_code': 'INVALID_REQUEST'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
account = data.get('account', '').strip()
|
||||||
|
password = data.get('password', '').strip()
|
||||||
|
|
||||||
|
# 檢查必要欄位
|
||||||
|
if not account or not password:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '請提供帳號和密碼',
|
||||||
|
'error_code': 'MISSING_FIELDS'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 查詢用戶資料
|
||||||
|
user = get_user_by_loginid_or_email(account)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '無權限,請詢問資訊部同仁',
|
||||||
|
'error_code': 'USER_NOT_FOUND'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
# 驗證密碼
|
||||||
|
if not verify_password(password, user['hashed_password']):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '無權限,請詢問資訊部同仁',
|
||||||
|
'error_code': 'INVALID_PASSWORD'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
# 登入成功
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '登入成功',
|
||||||
|
'user': {
|
||||||
|
'id': user['id'],
|
||||||
|
'loginid': user['loginid'],
|
||||||
|
'username': user['username'],
|
||||||
|
'email': user['email']
|
||||||
|
}
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"登入 API 錯誤: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '系統錯誤,請稍後再試',
|
||||||
|
'error_code': 'SYSTEM_ERROR'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/health', methods=['GET'])
|
||||||
|
def health_check():
|
||||||
|
"""
|
||||||
|
健康檢查 API
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 測試資料庫連線
|
||||||
|
connection = pymysql.connect(**DB_CONFIG)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT 1")
|
||||||
|
cursor.fetchone()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'API 服務正常',
|
||||||
|
'database': '連線正常',
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'API 服務異常',
|
||||||
|
'database': '連線失敗',
|
||||||
|
'error': str(e),
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/user/<account>', methods=['GET'])
|
||||||
|
def get_user_info(account):
|
||||||
|
"""
|
||||||
|
查詢用戶資訊 API(不包含密碼)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user = get_user_by_loginid_or_email(account)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用戶不存在',
|
||||||
|
'error_code': 'USER_NOT_FOUND'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# 回傳用戶資訊(不包含密碼)
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'user': {
|
||||||
|
'id': user['id'],
|
||||||
|
'loginid': user['loginid'],
|
||||||
|
'username': user['username'],
|
||||||
|
'email': user['email']
|
||||||
|
}
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"查詢用戶資訊錯誤: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '系統錯誤,請稍後再試',
|
||||||
|
'error_code': 'SYSTEM_ERROR'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/change-password', methods=['POST'])
|
||||||
|
def change_password():
|
||||||
|
"""
|
||||||
|
修改密碼 API
|
||||||
|
接收 JSON 格式: {"account": "帳號或email", "old_password": "舊密碼", "new_password": "新密碼"}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 取得請求資料
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '請提供 JSON 格式的資料',
|
||||||
|
'error_code': 'INVALID_REQUEST'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
account = data.get('account', '').strip()
|
||||||
|
old_password = data.get('old_password', '').strip()
|
||||||
|
new_password = data.get('new_password', '').strip()
|
||||||
|
|
||||||
|
# 檢查必要欄位
|
||||||
|
if not account or not old_password or not new_password:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '請提供帳號、舊密碼和新密碼',
|
||||||
|
'error_code': 'MISSING_FIELDS'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 檢查新密碼不能與舊密碼相同
|
||||||
|
if old_password == new_password:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '新密碼不能與舊密碼相同',
|
||||||
|
'error_code': 'SAME_PASSWORD'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 更新密碼
|
||||||
|
success, message = update_user_password(account, old_password, new_password)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': message
|
||||||
|
}), 200
|
||||||
|
else:
|
||||||
|
# 根據錯誤類型返回不同的狀態碼
|
||||||
|
if "用戶不存在" in message:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': message,
|
||||||
|
'error_code': 'USER_NOT_FOUND'
|
||||||
|
}), 404
|
||||||
|
elif "舊密碼不正確" in message:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': message,
|
||||||
|
'error_code': 'INVALID_OLD_PASSWORD'
|
||||||
|
}), 401
|
||||||
|
elif "密碼長度" in message or "密碼必須" in message:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': message,
|
||||||
|
'error_code': 'WEAK_PASSWORD'
|
||||||
|
}), 400
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': message,
|
||||||
|
'error_code': 'UPDATE_FAILED'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"修改密碼 API 錯誤: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '系統錯誤,請稍後再試',
|
||||||
|
'error_code': 'SYSTEM_ERROR'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(error):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'API 端點不存在',
|
||||||
|
'error_code': 'NOT_FOUND'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
@app.errorhandler(405)
|
||||||
|
def method_not_allowed(error):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '不支援的 HTTP 方法',
|
||||||
|
'error_code': 'METHOD_NOT_ALLOWED'
|
||||||
|
}), 405
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(f"啟動 {Config.APP_NAME} v{Config.APP_VERSION}...")
|
||||||
|
print("API 端點:")
|
||||||
|
print(" POST /api/login - 登入驗證")
|
||||||
|
print(" POST /api/change-password - 修改密碼")
|
||||||
|
print(" GET /api/health - 健康檢查")
|
||||||
|
print(" GET /api/user/<account> - 查詢用戶資訊")
|
||||||
|
print(f" GET {SWAGGER_URL} - Swagger API 文件")
|
||||||
|
print(f"\n服務地址: http://{Config.API_HOST}:{Config.API_PORT}")
|
||||||
|
print(f"Swagger 文件: http://{Config.API_HOST}:{Config.API_PORT}{SWAGGER_URL}")
|
||||||
|
print("\n範例請求:")
|
||||||
|
print("登入驗證:")
|
||||||
|
print(f'curl -X POST http://{Config.API_HOST}:{Config.API_PORT}/api/login \\')
|
||||||
|
print(' -H "Content-Type: application/json" \\')
|
||||||
|
print(' -d \'{"account": "92460", "password": "Panjit92460"}\'')
|
||||||
|
print("\n修改密碼:")
|
||||||
|
print(f'curl -X POST http://{Config.API_HOST}:{Config.API_PORT}/api/change-password \\')
|
||||||
|
print(' -H "Content-Type: application/json" \\')
|
||||||
|
print(' -d \'{"account": "92460", "old_password": "Panjit92460", "new_password": "MyNewPassword123"}\'')
|
||||||
|
print("\n服務啟動中...")
|
||||||
|
|
||||||
|
app.run(host=Config.API_HOST, port=Config.API_PORT, debug=Config.API_DEBUG)
|
40
config.py
Normal file
40
config.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 載入環境變數
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""應用程式配置類別"""
|
||||||
|
|
||||||
|
# 資料庫配置
|
||||||
|
DB_HOST = os.getenv('DB_HOST', 'mysql.theaken.com')
|
||||||
|
DB_PORT = int(os.getenv('DB_PORT', 33306))
|
||||||
|
DB_NAME = os.getenv('DB_NAME', 'db_panjit_password')
|
||||||
|
DB_USER = os.getenv('DB_USER', 'root')
|
||||||
|
DB_PASSWORD = os.getenv('DB_PASSWORD', 'zh6161168')
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET = os.getenv('JWT_SECRET', 'panjit666')
|
||||||
|
|
||||||
|
# API 配置
|
||||||
|
API_HOST = os.getenv('API_HOST', '0.0.0.0')
|
||||||
|
API_PORT = int(os.getenv('API_PORT', 5000))
|
||||||
|
API_DEBUG = os.getenv('API_DEBUG', 'True').lower() == 'true'
|
||||||
|
|
||||||
|
# 應用程式配置
|
||||||
|
APP_NAME = os.getenv('APP_NAME', 'Panjit Password Management API')
|
||||||
|
APP_VERSION = os.getenv('APP_VERSION', '1.0.0')
|
||||||
|
APP_DESCRIPTION = os.getenv('APP_DESCRIPTION', 'Panjit 密碼管理系統 API')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_db_config(cls):
|
||||||
|
"""取得資料庫配置字典"""
|
||||||
|
return {
|
||||||
|
'host': cls.DB_HOST,
|
||||||
|
'port': cls.DB_PORT,
|
||||||
|
'user': cls.DB_USER,
|
||||||
|
'password': cls.DB_PASSWORD,
|
||||||
|
'database': cls.DB_NAME,
|
||||||
|
'charset': 'utf8mb4'
|
||||||
|
}
|
22
env_example.txt
Normal file
22
env_example.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 環境變數配置範例
|
||||||
|
# 請複製此文件為 .env 並修改相應的值
|
||||||
|
|
||||||
|
# 資料庫配置
|
||||||
|
DB_HOST=your_database_host
|
||||||
|
DB_PORT=your_database_port
|
||||||
|
DB_NAME=your_database_name
|
||||||
|
DB_USER=your_database_user
|
||||||
|
DB_PASSWORD=your_database_password
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET=your_jwt_secret_key
|
||||||
|
|
||||||
|
# API 配置
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=12023
|
||||||
|
API_DEBUG=True
|
||||||
|
|
||||||
|
# 應用程式配置
|
||||||
|
APP_NAME=Panjit Password Management API
|
||||||
|
APP_VERSION=1.0.0
|
||||||
|
APP_DESCRIPTION=Panjit 密碼管理系統 API
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pandas==2.0.3
|
||||||
|
PyJWT==2.8.0
|
||||||
|
PyMySQL==1.1.0
|
||||||
|
openpyxl==3.1.2
|
||||||
|
Flask==2.3.3
|
||||||
|
flask-swagger-ui==4.11.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
flask-cors==4.0.0
|
Reference in New Issue
Block a user