2ND
This commit is contained in:
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pip install:*)",
|
||||
"Bash(python:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
72
.dockerignore
Normal file
72
.dockerignore
Normal file
@@ -0,0 +1,72 @@
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python cache
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
pip-log.txt
|
||||
|
||||
# Virtual environment
|
||||
venv/
|
||||
env/
|
||||
.venv/
|
||||
.env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
static/generated/*
|
||||
!static/generated/.gitkeep
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Test files
|
||||
tests/
|
||||
test_*
|
||||
*_test.py
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
36
.env.example
Normal file
36
.env.example
Normal file
@@ -0,0 +1,36 @@
|
||||
# Flask 應用程式設定
|
||||
SECRET_KEY=your-super-secret-and-random-string-here
|
||||
FLASK_ENV=production
|
||||
APP_PORT=5000
|
||||
UPLOAD_FOLDER=uploads
|
||||
|
||||
# 資料庫設定
|
||||
DB_ROOT_PASSWORD=tempspec123
|
||||
DB_NAME=tempspec_db
|
||||
DB_USER=tempspec_user
|
||||
DB_PASSWORD=tempspec_pass
|
||||
DB_PORT=3306
|
||||
|
||||
# LDAP/Active Directory 設定
|
||||
LDAP_SERVER=ldap://your-dc.company.com
|
||||
LDAP_PORT=389
|
||||
LDAP_USE_SSL=False
|
||||
LDAP_SEARCH_BASE=DC=company,DC=com
|
||||
LDAP_BIND_USER_DN=CN=service,DC=company,DC=com
|
||||
LDAP_BIND_USER_PASSWORD=service_password
|
||||
LDAP_USER_LOGIN_ATTR=userPrincipalName
|
||||
|
||||
# SMTP 郵件設定
|
||||
SMTP_SERVER=smtp.company.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USE_TLS=True
|
||||
SMTP_SENDER_EMAIL=noreply@company.com
|
||||
SMTP_SENDER_PASSWORD=smtp_password
|
||||
|
||||
# ONLYOFFICE Document Server 設定
|
||||
ONLYOFFICE_PORT=8080
|
||||
ONLYOFFICE_JWT_SECRET=your-onlyoffice-jwt-secret-string
|
||||
|
||||
# Nginx 設定 (可選)
|
||||
NGINX_PORT=80
|
||||
NGINX_SSL_PORT=443
|
250
DEMO_GUIDE.md
Normal file
250
DEMO_GUIDE.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 暫時規範管理系統 V3 - 測試/Demo 啟動指南
|
||||
|
||||
本指南專門用於快速啟動系統進行測試或演示。
|
||||
|
||||
## 🚀 快速啟動 (建議用於Demo)
|
||||
|
||||
### 選項1:Docker 一鍵啟動 (最簡單)
|
||||
|
||||
```bash
|
||||
# 1. 複製並設定環境變數
|
||||
cp .env.example .env
|
||||
|
||||
# 2. 編輯 .env 檔案 (重要!)
|
||||
# 設定以下關鍵參數:
|
||||
# - LDAP_SERVER=ldap://your-dc.panjit.com.tw
|
||||
# - LDAP_BIND_USER_DN=CN=service,DC=panjit,DC=com,DC=tw
|
||||
# - LDAP_BIND_USER_PASSWORD=your_service_password
|
||||
|
||||
# 3. 啟動所有服務
|
||||
docker-compose up -d
|
||||
|
||||
# 4. 等待服務啟動 (約30-60秒)
|
||||
docker-compose logs -f
|
||||
|
||||
# 5. 初始化資料庫
|
||||
docker-compose exec app python init_db.py
|
||||
|
||||
# 6. 訪問系統
|
||||
# http://localhost:5000
|
||||
```
|
||||
|
||||
### 選項2:本機開發模式
|
||||
|
||||
```bash
|
||||
# Windows
|
||||
start-windows.bat
|
||||
|
||||
# Linux
|
||||
./start-linux.sh
|
||||
```
|
||||
|
||||
## 📋 Demo 前檢查清單
|
||||
|
||||
### 必要服務檢查
|
||||
- [ ] **MySQL**: `docker ps` 確認 tempspec-mysql 運行中
|
||||
- [ ] **ONLYOFFICE**: 訪問 http://localhost:8080 確認運行正常
|
||||
- [ ] **Flask App**: 訪問 http://localhost:5000 確認可登入
|
||||
- [ ] **LDAP連線**: 使用AD帳號測試登入
|
||||
|
||||
### 權限設定確認
|
||||
- [ ] **ymirliu@panjit.com.tw**: 已設定為管理員權限
|
||||
- [ ] **權限管理頁面**: 可透過選單訪問
|
||||
- [ ] **角色功能**: admin/editor/viewer 功能正常
|
||||
|
||||
## 🔧 資料庫架構變更說明
|
||||
|
||||
### ⚠️ 重要:資料表名稱變更
|
||||
|
||||
如果您有現有的資料庫,請注意以下變更:
|
||||
|
||||
**舊版本 → V3 變更**:
|
||||
```
|
||||
user → ts_user
|
||||
temp_spec → ts_temp_spec
|
||||
upload → ts_upload
|
||||
spec_history → ts_spec_history
|
||||
```
|
||||
|
||||
### 資料移轉腳本 (如果需要)
|
||||
|
||||
如果需要從舊版本遷移資料:
|
||||
|
||||
```sql
|
||||
-- 備份現有資料
|
||||
CREATE TABLE user_backup AS SELECT * FROM user;
|
||||
CREATE TABLE temp_spec_backup AS SELECT * FROM temp_spec;
|
||||
-- 其他表的備份...
|
||||
|
||||
-- 重命名資料表 (謹慎操作!)
|
||||
RENAME TABLE user TO ts_user;
|
||||
RENAME TABLE temp_spec TO ts_temp_spec;
|
||||
RENAME TABLE upload TO ts_upload;
|
||||
RENAME TABLE spec_history TO ts_spec_history;
|
||||
```
|
||||
|
||||
**建議**: 如果是演示環境,直接重新初始化資料庫即可:
|
||||
|
||||
```bash
|
||||
# 清除舊資料並重新初始化
|
||||
docker-compose down -v # 刪除所有卷
|
||||
docker-compose up -d
|
||||
docker-compose exec app python init_db.py
|
||||
```
|
||||
|
||||
## 🧪 Demo 測試流程
|
||||
|
||||
### 基本功能測試
|
||||
|
||||
1. **登入測試**
|
||||
```bash
|
||||
# 測試AD帳號登入
|
||||
用戶: ymirliu@panjit.com.tw (或 ymirliu)
|
||||
密碼: [AD密碼]
|
||||
```
|
||||
|
||||
2. **權限管理測試**
|
||||
- 登入後訪問 "權限管理" 選單
|
||||
- 確認可以看到使用者列表
|
||||
- 測試權限變更功能
|
||||
|
||||
3. **規範建立流程**
|
||||
- 建立新的暫時規範
|
||||
- 測試 ONLYOFFICE 編輯功能
|
||||
- 下載 Word 文件
|
||||
|
||||
4. **智慧通知測試**
|
||||
- 測試啟用規範時的收件人選擇
|
||||
- 確認 LDAP 搜尋功能運作
|
||||
- 驗證郵件發送 (如果SMTP已設定)
|
||||
|
||||
5. **排程任務測試**
|
||||
- 檢查排程任務是否已註冊
|
||||
- 模擬到期提醒功能
|
||||
|
||||
### Demo 劇本建議
|
||||
|
||||
```
|
||||
1. 開場 - 系統介紹 (2分鐘)
|
||||
"這是企業級暫時規範管理系統V3,集成了AD認證和智慧通知"
|
||||
|
||||
2. 登入演示 (1分鐘)
|
||||
展示AD單一登入功能
|
||||
|
||||
3. 權限管理 (2分鐘)
|
||||
展示LDAP整合的權限管理界面
|
||||
|
||||
4. 規範建立 (3分鐘)
|
||||
展示從建立到線上編輯的完整流程
|
||||
|
||||
5. 智慧通知 (2分鐘)
|
||||
展示動態收件人選擇和自動提醒功能
|
||||
|
||||
6. 總結 (1分鐘)
|
||||
強調V3的主要改進和企業級特性
|
||||
```
|
||||
|
||||
## 🛠️ 疑難排解
|
||||
|
||||
### 常見問題
|
||||
|
||||
1. **LDAP連線失敗**
|
||||
```bash
|
||||
# 檢查LDAP設定
|
||||
docker-compose exec app python -c "
|
||||
from ldap_utils import authenticate_ldap_user
|
||||
print(authenticate_ldap_user('testuser', 'testpass'))
|
||||
"
|
||||
```
|
||||
|
||||
2. **ONLYOFFICE無法載入**
|
||||
```bash
|
||||
# 檢查Document Server狀態
|
||||
curl http://localhost:8080/healthcheck
|
||||
docker logs tempspec-onlyoffice
|
||||
```
|
||||
|
||||
3. **資料庫連線問題**
|
||||
```bash
|
||||
# 檢查MySQL狀態
|
||||
docker-compose exec mysql mysql -u root -p -e "SHOW DATABASES;"
|
||||
```
|
||||
|
||||
4. **權限設定問題**
|
||||
```sql
|
||||
-- 直接在資料庫中設定管理員
|
||||
USE tempspec_db;
|
||||
UPDATE ts_user SET role='admin' WHERE username='ymirliu@panjit.com.tw';
|
||||
```
|
||||
|
||||
### 快速重置
|
||||
|
||||
如果需要完全重置系統:
|
||||
|
||||
```bash
|
||||
# 停止並刪除所有容器和資料
|
||||
docker-compose down -v --remove-orphans
|
||||
|
||||
# 清理Docker資源
|
||||
docker system prune -f
|
||||
|
||||
# 重新啟動
|
||||
docker-compose up -d
|
||||
sleep 30
|
||||
docker-compose exec app python init_db.py
|
||||
|
||||
echo "系統已完全重置!"
|
||||
```
|
||||
|
||||
## 📊 系統狀態監控
|
||||
|
||||
### 檢查系統健康狀態
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# health-check.sh
|
||||
|
||||
echo "=== 系統狀態檢查 ==="
|
||||
|
||||
echo "1. Docker 服務狀態:"
|
||||
docker-compose ps
|
||||
|
||||
echo -e "\n2. 應用程式可訪問性:"
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:5000 && echo " ✓ Flask App: 正常" || echo " ✗ Flask App: 異常"
|
||||
|
||||
echo -e "\n3. ONLYOFFICE 狀態:"
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 && echo " ✓ ONLYOFFICE: 正常" || echo " ✗ ONLYOFFICE: 異常"
|
||||
|
||||
echo -e "\n4. MySQL 狀態:"
|
||||
docker-compose exec mysql mysqladmin ping -h localhost -u root -p$DB_ROOT_PASSWORD --silent && echo " ✓ MySQL: 正常" || echo " ✗ MySQL: 異常"
|
||||
|
||||
echo -e "\n5. 磁碟空間:"
|
||||
df -h | grep -E "(Filesystem|/dev/)"
|
||||
|
||||
echo -e "\n6. 記憶體使用:"
|
||||
free -h
|
||||
|
||||
echo -e "\n=== 檢查完成 ==="
|
||||
```
|
||||
|
||||
## 📝 Demo 注意事項
|
||||
|
||||
1. **網路環境**: 確保Demo環境可以連接到公司的LDAP伺服器
|
||||
2. **防火牆**: 開放必要的端口 (389/636 for LDAP, 25/587 for SMTP)
|
||||
3. **測試資料**: 準備一些測試用的暫時規範範例
|
||||
4. **備案計畫**: 準備離線Demo資料以防網路問題
|
||||
5. **效能考量**: Demo環境建議至少4GB RAM,確保ONLYOFFICE穩定運行
|
||||
|
||||
## 🎯 成功標準
|
||||
|
||||
Demo成功的標誌:
|
||||
- ✅ AD帳號可以正常登入
|
||||
- ✅ 權限管理功能運作正常
|
||||
- ✅ ONLYOFFICE編輯器可以載入和編輯
|
||||
- ✅ 智慧通知功能可以搜尋LDAP用戶
|
||||
- ✅ 排程任務已正確註冊
|
||||
- ✅ 所有核心功能響應時間 < 3秒
|
||||
|
||||
---
|
||||
|
||||
**祝您Demo順利!** 如有任何問題,請參考系統日誌或聯繫技術支援團隊。
|
690
DEPLOYMENT.md
Normal file
690
DEPLOYMENT.md
Normal file
@@ -0,0 +1,690 @@
|
||||
# 部署指南 - 暫時規範管理系統 V3
|
||||
|
||||
本文件提供詳細的部署指導,涵蓋不同平台和環境的部署方式。
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
1. [快速開始](#1-快速開始)
|
||||
2. [平台特定部署](#2-平台特定部署)
|
||||
3. [Docker 部署](#3-docker-部署)
|
||||
4. [生產環境部署](#4-生產環境部署)
|
||||
5. [疑難排解](#5-疑難排解)
|
||||
|
||||
## 1. 快速開始
|
||||
|
||||
### 前置需求檢查清單
|
||||
|
||||
- [ ] Python 3.8+ 已安裝
|
||||
- [ ] Docker 已安裝且運行中 (如使用 Docker 部署)
|
||||
- [ ] MySQL 8.0+ 或相容的資料庫
|
||||
- [ ] ONLYOFFICE Document Server
|
||||
- [ ] LDAP/Active Directory 伺服器 (企業環境)
|
||||
- [ ] SMTP 郵件伺服器
|
||||
|
||||
### 一鍵啟動
|
||||
|
||||
#### Windows 環境
|
||||
```cmd
|
||||
# 使用自動化腳本啟動
|
||||
start-windows.bat
|
||||
|
||||
# 或手動執行
|
||||
docker-compose up -d
|
||||
docker-compose exec app python init_db.py
|
||||
```
|
||||
|
||||
#### Linux 環境
|
||||
```bash
|
||||
# 使用自動化腳本啟動
|
||||
chmod +x start-linux.sh
|
||||
./start-linux.sh
|
||||
|
||||
# 或手動執行
|
||||
docker-compose up -d
|
||||
docker-compose exec app python init_db.py
|
||||
```
|
||||
|
||||
## 2. 平台特定部署
|
||||
|
||||
### 2.1 Windows 部署
|
||||
|
||||
#### 開發環境
|
||||
|
||||
1. **準備環境**
|
||||
```cmd
|
||||
# 建立虛擬環境
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **設定環境變數**
|
||||
```cmd
|
||||
copy .env.example .env
|
||||
# 編輯 .env 檔案設定參數
|
||||
```
|
||||
|
||||
3. **啟動外部服務**
|
||||
```cmd
|
||||
# 啟動 MySQL (使用 Docker)
|
||||
docker run -d --name tempspec-mysql ^
|
||||
-e MYSQL_ROOT_PASSWORD=tempspec123 ^
|
||||
-e MYSQL_DATABASE=tempspec_db ^
|
||||
-e MYSQL_USER=tempspec_user ^
|
||||
-e MYSQL_PASSWORD=tempspec_pass ^
|
||||
-p 3306:3306 mysql:8.0
|
||||
|
||||
# 啟動 ONLYOFFICE
|
||||
docker run -d --name tempspec-onlyoffice ^
|
||||
-e JWT_ENABLED=true ^
|
||||
-e JWT_SECRET=your_jwt_secret ^
|
||||
-p 8080:80 onlyoffice/documentserver
|
||||
```
|
||||
|
||||
4. **初始化並啟動應用**
|
||||
```cmd
|
||||
python init_db.py
|
||||
python app.py
|
||||
```
|
||||
|
||||
#### 生產環境 (Windows Server + IIS)
|
||||
|
||||
1. **安裝 IIS 和 HttpPlatformHandler**
|
||||
- 啟用 IIS 功能
|
||||
- 安裝 HttpPlatformHandler 模組
|
||||
|
||||
2. **建立應用程式目錄**
|
||||
```cmd
|
||||
mkdir C:\inetpub\wwwroot\tempspec
|
||||
xcopy /E /I . C:\inetpub\wwwroot\tempspec
|
||||
```
|
||||
|
||||
3. **建立 Web.config**
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="PythonHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
|
||||
</handlers>
|
||||
<httpPlatform processPath="C:\Python310\python.exe"
|
||||
arguments="app.py"
|
||||
startupTimeLimit="60"
|
||||
startupRetryCount="3"
|
||||
stdoutLogEnabled="true"
|
||||
stdoutLogFile="logs\python.log"
|
||||
environmentVariables="PYTHONPATH=C:\inetpub\wwwroot\tempspec">
|
||||
<environmentVariables>
|
||||
<environmentVariable name="PYTHONPATH" value="C:\inetpub\wwwroot\tempspec" />
|
||||
</environmentVariables>
|
||||
</httpPlatform>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
4. **使用 Waitress (建議方式)**
|
||||
```cmd
|
||||
pip install waitress
|
||||
waitress-serve --host=0.0.0.0 --port=5000 app:app
|
||||
```
|
||||
|
||||
#### Windows 服務安裝
|
||||
|
||||
使用 NSSM (Non-Sucking Service Manager):
|
||||
|
||||
```cmd
|
||||
# 下載 NSSM
|
||||
# https://nssm.cc/download
|
||||
|
||||
# 安裝服務
|
||||
nssm install TempSpecSystem
|
||||
|
||||
# 設定參數
|
||||
nssm set TempSpecSystem Application "C:\path\to\python.exe"
|
||||
nssm set TempSpecSystem AppParameters "app.py"
|
||||
nssm set TempSpecSystem AppDirectory "C:\path\to\your\app"
|
||||
nssm set TempSpecSystem DisplayName "Temp Spec System V3"
|
||||
nssm set TempSpecSystem Description "企業暫時規範管理系統"
|
||||
|
||||
# 啟動服務
|
||||
nssm start TempSpecSystem
|
||||
```
|
||||
|
||||
### 2.2 Linux 部署
|
||||
|
||||
#### 開發環境
|
||||
|
||||
1. **準備環境 (Ubuntu/Debian)**
|
||||
```bash
|
||||
# 更新系統
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 安裝必要套件
|
||||
sudo apt install python3 python3-pip python3-venv git curl -y
|
||||
|
||||
# 建立虛擬環境
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **CentOS/RHEL 環境**
|
||||
```bash
|
||||
# 安裝 EPEL repository
|
||||
sudo yum install epel-release -y
|
||||
|
||||
# 安裝必要套件
|
||||
sudo yum install python3 python3-pip git curl -y
|
||||
|
||||
# 建立虛擬環境
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 生產環境 (Nginx + Gunicorn)
|
||||
|
||||
1. **安裝 Gunicorn**
|
||||
```bash
|
||||
pip install gunicorn gevent
|
||||
```
|
||||
|
||||
2. **建立 Gunicorn 設定檔**
|
||||
```python
|
||||
# gunicorn.conf.py
|
||||
bind = "127.0.0.1:5000"
|
||||
workers = 4
|
||||
worker_class = "gevent"
|
||||
worker_connections = 1000
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 50
|
||||
timeout = 30
|
||||
keepalive = 2
|
||||
preload_app = True
|
||||
```
|
||||
|
||||
3. **建立 systemd 服務檔案**
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/tempspec.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Temp Spec System V3
|
||||
After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/path/to/your/app
|
||||
Environment="PATH=/path/to/your/app/venv/bin"
|
||||
ExecStart=/path/to/your/app/venv/bin/gunicorn -c gunicorn.conf.py app:app
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
4. **啟動服務**
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable tempspec
|
||||
sudo systemctl start tempspec
|
||||
sudo systemctl status tempspec
|
||||
```
|
||||
|
||||
5. **設定 Nginx**
|
||||
```bash
|
||||
sudo nano /etc/nginx/sites-available/tempspec
|
||||
```
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL 重定向 (生產環境建議)
|
||||
# return 301 https://$server_name$request_uri;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 支援
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# 靜態檔案
|
||||
location /static/ {
|
||||
alias /path/to/your/app/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 大檔案上傳支援
|
||||
client_max_body_size 100M;
|
||||
}
|
||||
```
|
||||
|
||||
6. **啟用站點**
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/tempspec /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 3. Docker 部署
|
||||
|
||||
### 3.1 基本 Docker 部署
|
||||
|
||||
```bash
|
||||
# 1. 複製環境設定檔
|
||||
cp .env.example .env
|
||||
# 編輯 .env 設定參數
|
||||
|
||||
# 2. 啟動所有服務
|
||||
docker-compose up -d
|
||||
|
||||
# 3. 初始化資料庫
|
||||
docker-compose exec app python init_db.py
|
||||
|
||||
# 4. 檢查服務狀態
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 3.2 開發環境部署
|
||||
|
||||
```bash
|
||||
# 使用 override 檔案啟動開發環境
|
||||
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
||||
|
||||
# 查看即時日誌
|
||||
docker-compose logs -f app
|
||||
```
|
||||
|
||||
### 3.3 生產環境部署
|
||||
|
||||
```bash
|
||||
# 啟動包含 Nginx 的完整生產環境
|
||||
docker-compose --profile production up -d
|
||||
|
||||
# 檢查所有服務
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 3.4 Docker Swarm 部署 (高可用性)
|
||||
|
||||
1. **初始化 Swarm**
|
||||
```bash
|
||||
docker swarm init
|
||||
```
|
||||
|
||||
2. **建立 Docker Stack 檔案**
|
||||
```yaml
|
||||
# docker-stack.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: tempspec:latest
|
||||
deploy:
|
||||
replicas: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
networks:
|
||||
- tempspec-network
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
deploy:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == manager
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
networks:
|
||||
- tempspec-network
|
||||
|
||||
networks:
|
||||
tempspec-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
```
|
||||
|
||||
3. **部署 Stack**
|
||||
```bash
|
||||
docker stack deploy -c docker-stack.yml tempspec
|
||||
```
|
||||
|
||||
### 3.5 Docker 管理指令
|
||||
|
||||
```bash
|
||||
# 查看服務狀態
|
||||
docker-compose ps
|
||||
|
||||
# 查看日誌
|
||||
docker-compose logs -f [service_name]
|
||||
|
||||
# 重啟服務
|
||||
docker-compose restart [service_name]
|
||||
|
||||
# 進入容器
|
||||
docker-compose exec app bash
|
||||
|
||||
# 備份資料庫
|
||||
docker-compose exec mysql mysqldump -u root -p tempspec_db > backup.sql
|
||||
|
||||
# 清理未使用的資源
|
||||
docker system prune -f
|
||||
docker volume prune -f
|
||||
```
|
||||
|
||||
## 4. 生產環境部署
|
||||
|
||||
### 4.1 負載均衡部署
|
||||
|
||||
使用多個應用程式實例提高可用性:
|
||||
|
||||
```yaml
|
||||
# docker-compose.prod.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app1:
|
||||
build: .
|
||||
environment:
|
||||
- INSTANCE_ID=1
|
||||
# 其他設定...
|
||||
|
||||
app2:
|
||||
build: .
|
||||
environment:
|
||||
- INSTANCE_ID=2
|
||||
# 其他設定...
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./nginx/nginx-lb.conf:/etc/nginx/nginx.conf
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- app1
|
||||
- app2
|
||||
```
|
||||
|
||||
### 4.2 SSL/HTTPS 設定
|
||||
|
||||
1. **獲取 SSL 證書 (Let's Encrypt)**
|
||||
```bash
|
||||
# 安裝 Certbot
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
|
||||
# 獲取證書
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
2. **Docker 環境 SSL 設定**
|
||||
```yaml
|
||||
# docker-compose.ssl.yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
nginx:
|
||||
volumes:
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
- ./nginx/ssl.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- "443:443"
|
||||
- "80:80"
|
||||
```
|
||||
|
||||
### 4.3 監控和日誌
|
||||
|
||||
1. **集成監控 (可選)**
|
||||
```yaml
|
||||
# 添加到 docker-compose.yml
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
```
|
||||
|
||||
2. **集中化日誌收集**
|
||||
```yaml
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.0
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
|
||||
kibana:
|
||||
image: docker.elastic.co/kibana/kibana:7.14.0
|
||||
ports:
|
||||
- "5601:5601"
|
||||
```
|
||||
|
||||
### 4.4 備份策略
|
||||
|
||||
1. **資料庫備份腳本**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup-db.sh
|
||||
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/backup/mysql"
|
||||
CONTAINER_NAME="tempspec-mysql"
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# 備份資料庫
|
||||
docker exec $CONTAINER_NAME mysqldump \
|
||||
-u root -p$MYSQL_ROOT_PASSWORD \
|
||||
tempspec_db > $BACKUP_DIR/tempspec_$DATE.sql
|
||||
|
||||
# 壓縮備份檔案
|
||||
gzip $BACKUP_DIR/tempspec_$DATE.sql
|
||||
|
||||
# 刪除 30 天前的備份
|
||||
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete
|
||||
|
||||
echo "Database backup completed: tempspec_$DATE.sql.gz"
|
||||
```
|
||||
|
||||
2. **設定 cron 自動備份**
|
||||
```bash
|
||||
# 編輯 crontab
|
||||
crontab -e
|
||||
|
||||
# 每天凌晨 2:30 執行備份
|
||||
30 2 * * * /path/to/backup-db.sh >> /var/log/tempspec-backup.log 2>&1
|
||||
```
|
||||
|
||||
## 5. 疑難排解
|
||||
|
||||
### 5.1 常見問題
|
||||
|
||||
#### 問題:Docker 容器無法啟動
|
||||
|
||||
**可能原因**:
|
||||
- 端口被占用
|
||||
- 環境變數設定錯誤
|
||||
- 磁碟空間不足
|
||||
|
||||
**解決方案**:
|
||||
```bash
|
||||
# 檢查端口占用
|
||||
netstat -tulpn | grep :5000
|
||||
|
||||
# 檢查容器日誌
|
||||
docker-compose logs app
|
||||
|
||||
# 檢查磁碟空間
|
||||
df -h
|
||||
|
||||
# 清理 Docker 資源
|
||||
docker system prune -a
|
||||
```
|
||||
|
||||
#### 問題:LDAP 連線失敗
|
||||
|
||||
**檢查清單**:
|
||||
- [ ] LDAP 伺服器地址正確
|
||||
- [ ] 防火牆開放 389/636 端口
|
||||
- [ ] 服務帳號權限足夠
|
||||
- [ ] 搜尋基底設定正確
|
||||
|
||||
**測試 LDAP 連線**:
|
||||
```bash
|
||||
# 使用 ldapsearch 測試
|
||||
ldapsearch -H ldap://your-dc.company.com -D "CN=service,DC=company,DC=com" -W -b "DC=company,DC=com"
|
||||
|
||||
# 或在 Python 中測試
|
||||
python3 -c "
|
||||
from ldap_utils import authenticate_ldap_user
|
||||
result = authenticate_ldap_user('testuser', 'testpass')
|
||||
print('LDAP Test Result:', result)
|
||||
"
|
||||
```
|
||||
|
||||
#### 問題:ONLYOFFICE 編輯器載入失敗
|
||||
|
||||
**檢查項目**:
|
||||
- [ ] Document Server 容器運行正常
|
||||
- [ ] JWT Secret 設定一致
|
||||
- [ ] 網路連線可達
|
||||
- [ ] 瀏覽器支援 WebSocket
|
||||
|
||||
**測試方法**:
|
||||
```bash
|
||||
# 檢查 ONLYOFFICE 健康狀態
|
||||
curl http://localhost:8080/healthcheck
|
||||
|
||||
# 檢查容器狀態
|
||||
docker logs tempspec-onlyoffice
|
||||
|
||||
# 測試 JWT 設定
|
||||
docker exec tempspec-onlyoffice cat /etc/onlyoffice/documentserver/default.json | grep -i jwt
|
||||
```
|
||||
|
||||
#### 問題:排程任務未執行
|
||||
|
||||
**檢查步驟**:
|
||||
1. 確認 APScheduler 已初始化
|
||||
2. 檢查應用程式日誌
|
||||
3. 驗證任務註冊
|
||||
|
||||
```python
|
||||
# 在 Flask shell 中檢查任務
|
||||
from app import scheduler
|
||||
print(scheduler.get_jobs())
|
||||
```
|
||||
|
||||
### 5.2 效能調校
|
||||
|
||||
#### 資料庫最佳化
|
||||
|
||||
```sql
|
||||
-- 檢查慢查詢
|
||||
SHOW VARIABLES LIKE 'slow_query_log';
|
||||
SHOW VARIABLES LIKE 'long_query_time';
|
||||
|
||||
-- 分析資料表
|
||||
ANALYZE TABLE ts_temp_spec;
|
||||
ANALYZE TABLE ts_user;
|
||||
ANALYZE TABLE ts_upload;
|
||||
|
||||
-- 建立必要索引
|
||||
CREATE INDEX idx_spec_status ON ts_temp_spec(status);
|
||||
CREATE INDEX idx_spec_end_date ON ts_temp_spec(end_date);
|
||||
CREATE INDEX idx_history_spec_id ON ts_spec_history(spec_id);
|
||||
```
|
||||
|
||||
#### 應用程式最佳化
|
||||
|
||||
```python
|
||||
# config.py 調整
|
||||
class ProductionConfig(Config):
|
||||
# 資料庫連線池設定
|
||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_size': 20,
|
||||
'pool_recycle': 300,
|
||||
'pool_pre_ping': True
|
||||
}
|
||||
|
||||
# Redis 快取 (可選)
|
||||
CACHE_TYPE = 'redis'
|
||||
CACHE_REDIS_URL = 'redis://redis:6379/0'
|
||||
```
|
||||
|
||||
### 5.3 監控指標
|
||||
|
||||
重要的監控指標:
|
||||
|
||||
- **應用程式指標**
|
||||
- 回應時間
|
||||
- 錯誤率
|
||||
- 記憶體使用率
|
||||
- CPU 使用率
|
||||
|
||||
- **資料庫指標**
|
||||
- 連線數
|
||||
- 查詢執行時間
|
||||
- 緩衝池命中率
|
||||
|
||||
- **系統指標**
|
||||
- 磁碟使用率
|
||||
- 網路流量
|
||||
- 檔案描述符使用率
|
||||
|
||||
```bash
|
||||
# 系統監控腳本範例
|
||||
#!/bin/bash
|
||||
# monitor.sh
|
||||
|
||||
echo "=== System Status ==="
|
||||
echo "CPU Usage: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)"
|
||||
echo "Memory Usage: $(free | grep Mem | awk '{printf("%.1f%%", $3/$2 * 100.0)}')"
|
||||
echo "Disk Usage: $(df -h | grep '/$' | awk '{print $5}')"
|
||||
|
||||
echo "=== Docker Status ==="
|
||||
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
|
||||
|
||||
echo "=== Application Status ==="
|
||||
curl -s http://localhost:5000/health | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
本部署指南涵蓋了大部分常見的部署場景。如果遇到特殊情況或需要客製化部署,請參考系統文檔或聯繫技術支援團隊。
|
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# 使用官方 Python 3.10 運行時作為基礎映像
|
||||
FROM python:3.10-slim
|
||||
|
||||
# 設定環境變數
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 設定工作目錄
|
||||
WORKDIR /app
|
||||
|
||||
# 更新系統套件並安裝必要的系統依賴
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 複製依賴檔案
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安裝 Python 依賴
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 複製應用程式代碼
|
||||
COPY . .
|
||||
|
||||
# 建立必要的目錄
|
||||
RUN mkdir -p uploads static/generated logs
|
||||
|
||||
# 設定權限
|
||||
RUN chmod +x app.py
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5000
|
||||
|
||||
# 健康檢查
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/ || exit 1
|
||||
|
||||
# 啟動命令
|
||||
CMD ["python", "app.py"]
|
584
README.md
584
README.md
@@ -1,133 +1,513 @@
|
||||
# TEMP Spec System - 暫時規範管理系統 (ONLYOFFICE-Edition)
|
||||
# 暫時規範管理系統 V3
|
||||
|
||||
這是一個使用 Flask 開發的 Web 應用程式,旨在管理、追蹤和存檔暫時性的工程規範。此版本已整合 ONLYOFFICE Document Server,提供強大的所見即所得(WYSIWYG)線上文件編輯體驗。
|
||||
企業級暫時規範生命週期管理系統,提供完整的文件管理、LDAP整合驗證、智慧通知系統及排程提醒功能。
|
||||
|
||||
## 核心功能
|
||||
## 🚀 系統特色
|
||||
|
||||
- **使用者權限管理**: 內建三種角色 (`viewer`, `editor`, `admin`),各角色擁有不同操作權限。
|
||||
- **規範生命週期**: 支援暫時規範的建立、啟用、展延、終止與刪除。
|
||||
- **線上文件編輯**: 整合 ONLYOFFICE Document Server,支援多人協作與專業級的線上 Word 文件編輯。
|
||||
- **範本自動填入**: 建立規範時,可將表單資料自動填入 Word 範本中。
|
||||
- **檔案管理**: 支援上傳簽核後的文件,並與對應的規範進行關聯。
|
||||
- **歷史紀錄**: 詳細記錄每一份規範的所有變更歷史,方便追蹤與稽核。
|
||||
- **LDAP/AD 整合驗證**:支援企業Active Directory單一登入
|
||||
- **ONLYOFFICE 線上編輯**:即時協作文件編輯功能
|
||||
- **智慧通知系統**:動態收件人選擇與自動提醒
|
||||
- **文件生命週期管理**:完整的建立、啟用、展延、終止流程
|
||||
- **多平台支援**:支援 Windows/Linux 環境部署
|
||||
- **Docker 容器化**:一鍵部署環境
|
||||
|
||||
---
|
||||
## 📋 功能模組
|
||||
|
||||
## 環境要求
|
||||
### 核心功能
|
||||
- **文件管理**:Word範本自動化生成與PDF轉換
|
||||
- **權限控制**:三級權限管理 (Viewer/Editor/Admin)
|
||||
- **歷史追蹤**:完整的操作記錄與版本控制
|
||||
- **檔案上傳**:支援多種格式的佐證文件上傳
|
||||
|
||||
在部署此應用程式之前,請確保您的系統已安裝以下軟體:
|
||||
### 智慧通知系統
|
||||
- **動態收件人選擇**:整合LDAP的即時用戶搜尋
|
||||
- **全流程通知**:啟用、展延、終止操作的自動郵件通知
|
||||
- **自動提醒**:3天與7天到期前的主動提醒郵件
|
||||
- **排程系統**:每日自動檢查即將到期的規範
|
||||
|
||||
1. **Python**: 建議使用 `Python 3.10` 或更高版本。
|
||||
2. **MySQL**: 需要一個 MySQL 資料庫來儲存所有應用程式資料。
|
||||
3. **Docker**: **[重要]** 本專案依賴 ONLYOFFICE Document Server,推薦使用 Docker 進行部署和管理。請確保您的伺服器已安裝並運行 Docker。
|
||||
4. **Git**: 用於從版本控制系統下載程式碼。
|
||||
### 編輯器整合
|
||||
- **ONLYOFFICE整合**:支援Word文件的線上即時編輯
|
||||
- **Toast UI Editor**:Markdown格式的內容編輯器
|
||||
- **圖片支援**:內嵌圖片顯示與編輯功能
|
||||
|
||||
---
|
||||
## 🏗️ 系統架構
|
||||
|
||||
## 安裝與設定步驟
|
||||
|
||||
請依照以下步驟來設定您的開發或生產環境:
|
||||
|
||||
### 1. 下載程式碼
|
||||
|
||||
```bash
|
||||
git clone <your-repository-url>
|
||||
cd TEMP_spec_system_V2
|
||||
```
|
||||
暫時規範系統 V3
|
||||
├── 前端介面 (Flask + Bootstrap 5)
|
||||
├── 後端邏輯 (Python Flask)
|
||||
├── 資料庫 (MySQL/SQLite)
|
||||
├── LDAP整合 (Active Directory)
|
||||
├── 文件引擎 (ONLYOFFICE)
|
||||
├── 排程服務 (APScheduler)
|
||||
└── 郵件系統 (SMTP)
|
||||
```
|
||||
|
||||
### 2. 建立並啟用虛擬環境
|
||||
## 🛠️ 技術棧
|
||||
|
||||
- **後端框架**:Python Flask 3.x
|
||||
- **資料庫ORM**:SQLAlchemy
|
||||
- **前端UI**:Bootstrap 5 + Tom Select
|
||||
- **文件處理**:python-docx, docx2pdf
|
||||
- **認證系統**:Flask-Login + LDAP3
|
||||
- **排程系統**:Flask-APScheduler
|
||||
- **容器化**:Docker + Docker Compose
|
||||
|
||||
## 📦 安裝部署
|
||||
|
||||
### 前置需求
|
||||
|
||||
- Python 3.8+
|
||||
- MySQL 8.0+ 或 SQLite
|
||||
- ONLYOFFICE Document Server
|
||||
- LDAP/Active Directory 伺服器
|
||||
- SMTP 郵件伺服器
|
||||
|
||||
### 快速開始 (Docker)
|
||||
|
||||
1. **克隆專案**
|
||||
```bash
|
||||
# Windows
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\activate
|
||||
|
||||
# macOS / Linux
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
git clone <repository-url>
|
||||
cd TEMP_spec_system_V3
|
||||
```
|
||||
|
||||
### 3. 安裝相依套件
|
||||
2. **設定環境變數**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 編輯 .env 檔案設定資料庫、LDAP、SMTP 等參數
|
||||
```
|
||||
|
||||
3. **使用Docker Compose啟動**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. **初始化資料庫**
|
||||
```bash
|
||||
docker-compose exec app python init_db.py
|
||||
```
|
||||
|
||||
### 手動安裝
|
||||
|
||||
#### Windows 環境
|
||||
|
||||
1. **安裝Python依賴**
|
||||
```cmd
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. **設定環境變數**
|
||||
```cmd
|
||||
copy .env.example .env
|
||||
REM 編輯 .env 檔案
|
||||
```
|
||||
|
||||
3. **初始化資料庫**
|
||||
```cmd
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
4. **啟動 ONLYOFFICE Document Server**
|
||||
```cmd
|
||||
docker run -d -p 8080:80 --restart=always ^
|
||||
-e JWT_ENABLED=true ^
|
||||
-e JWT_SECRET=your-onlyoffice-jwt-secret-string ^
|
||||
onlyoffice/documentserver
|
||||
```
|
||||
|
||||
5. **啟動應用程式**
|
||||
```cmd
|
||||
REM 開發環境
|
||||
python app.py
|
||||
|
||||
REM 生產環境 (Windows 建議使用 Waitress)
|
||||
pip install waitress
|
||||
waitress-serve --host=0.0.0.0 --port=5000 app:app
|
||||
```
|
||||
|
||||
#### Linux 環境
|
||||
|
||||
1. **安裝Python依賴**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4. 設定環境變數
|
||||
|
||||
複製範例檔案,並填入您的實際設定:
|
||||
|
||||
2. **設定環境變數**
|
||||
```bash
|
||||
# Windows
|
||||
copy .env.example .env
|
||||
|
||||
# macOS / Linux
|
||||
cp .env.example .env
|
||||
# 編輯 .env 檔案
|
||||
```
|
||||
|
||||
編輯 `.env` 檔案,確保包含以下所有欄位:
|
||||
|
||||
```dotenv
|
||||
# Flask 應用程式的密鑰,用於保護 session
|
||||
SECRET_KEY="your-super-secret-and-random-string"
|
||||
|
||||
# 資料庫連線 URL
|
||||
DATABASE_URL="mysql+pymysql://user:password@host:port/dbname"
|
||||
|
||||
# --- ONLYOFFICE 設定 ---
|
||||
# 您 ONLYOFFICE Document Server 的公開存取位址
|
||||
ONLYOFFICE_URL="http://localhost:8080/"
|
||||
|
||||
# 用於保護 ONLYOFFICE 通訊的 JWT 密鑰 (請務必修改為一個新的隨機長字串)
|
||||
ONLYOFFICE_JWT_SECRET="your-onlyoffice-jwt-secret-string"
|
||||
```
|
||||
|
||||
**注意**: 請先在您的 MySQL 中手動建立一個資料庫。
|
||||
|
||||
### 5. 啟動 ONLYOFFICE Document Server
|
||||
|
||||
使用 Docker 啟動 ONLYOFFICE Document Server,並啟用 JWT 驗證。
|
||||
|
||||
```bash
|
||||
docker run -i -t -d -p 8080:80 --restart=always \
|
||||
-e JWT_ENABLED=true \
|
||||
-e JWT_SECRET="your-onlyoffice-jwt-secret-string" \
|
||||
onlyoffice/documentserver
|
||||
```
|
||||
|
||||
**[重要]**:指令中的 `JWT_SECRET` 值,必須和您在 `.env` 檔案中設定的 `ONLYOFFICE_JWT_SECRET` **完全一致**。
|
||||
|
||||
### 6. 初始化資料庫
|
||||
|
||||
執行初始化腳本來建立所有需要的資料表,並產生一個預設的管理員帳號。
|
||||
|
||||
3. **初始化資料庫**
|
||||
```bash
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
腳本會提示您確認操作。輸入 `yes` 後,它會建立資料表並在終端機中顯示預設 `admin` 帳號的隨機密碼。**請務必記下此密碼**。
|
||||
4. **啟動 ONLYOFFICE Document Server**
|
||||
```bash
|
||||
docker run -d -p 8080:80 --restart=always \
|
||||
-e JWT_ENABLED=true \
|
||||
-e JWT_SECRET=your-onlyoffice-jwt-secret-string \
|
||||
onlyoffice/documentserver
|
||||
```
|
||||
|
||||
5. **啟動應用程式**
|
||||
```bash
|
||||
# 開發環境
|
||||
python app.py
|
||||
|
||||
# 生產環境 (使用 Gunicorn)
|
||||
pip install gunicorn
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
|
||||
### 生產環境部署
|
||||
|
||||
#### 使用 Nginx + Gunicorn (Linux)
|
||||
|
||||
1. **安裝 Gunicorn**
|
||||
```bash
|
||||
pip install gunicorn
|
||||
```
|
||||
|
||||
2. **建立 Gunicorn 服務檔案**
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/tempspec.service
|
||||
```
|
||||
|
||||
內容:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Temp Spec System
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/path/to/your/app
|
||||
Environment="PATH=/path/to/your/app/venv/bin"
|
||||
ExecStart=/path/to/your/app/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 app:app
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. **Nginx 設定**
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **啟動服務**
|
||||
```bash
|
||||
sudo systemctl enable tempspec
|
||||
sudo systemctl start tempspec
|
||||
```
|
||||
|
||||
#### Windows IIS 部署
|
||||
|
||||
1. **安裝 IIS 與 Python**
|
||||
2. **安裝 HttpPlatformHandler**
|
||||
3. **設定 Web.config**
|
||||
```xml
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="PythonHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/>
|
||||
</handlers>
|
||||
<httpPlatform processPath="C:\path\to\python.exe"
|
||||
arguments="app.py"
|
||||
startupTimeLimit="60"
|
||||
startupRetryCount="3">
|
||||
</httpPlatform>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
## ⚙️ 組態設定
|
||||
|
||||
### 環境變數 (.env)
|
||||
|
||||
```env
|
||||
# Flask 設定
|
||||
SECRET_KEY=your_secret_key_here
|
||||
UPLOAD_FOLDER=uploads
|
||||
|
||||
# 資料庫設定
|
||||
DATABASE_URL=mysql+pymysql://user:password@localhost/tempspec_db
|
||||
|
||||
# LDAP 設定
|
||||
LDAP_SERVER=ldap://your-dc.company.com
|
||||
LDAP_PORT=389
|
||||
LDAP_USE_SSL=False
|
||||
LDAP_SEARCH_BASE=DC=company,DC=com
|
||||
LDAP_BIND_USER_DN=CN=service,DC=company,DC=com
|
||||
LDAP_BIND_USER_PASSWORD=service_password
|
||||
LDAP_USER_LOGIN_ATTR=userPrincipalName
|
||||
|
||||
# SMTP 郵件設定
|
||||
SMTP_SERVER=smtp.company.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USE_TLS=True
|
||||
SMTP_SENDER_EMAIL=noreply@company.com
|
||||
SMTP_SENDER_PASSWORD=smtp_password
|
||||
|
||||
# ONLYOFFICE 設定
|
||||
ONLYOFFICE_URL=http://onlyoffice:8080
|
||||
ONLYOFFICE_JWT_SECRET=your_jwt_secret
|
||||
```
|
||||
|
||||
### 特殊注意事項
|
||||
|
||||
#### Windows 環境差異
|
||||
|
||||
1. **排程服務限制**
|
||||
- APScheduler 在 Windows 上運行正常
|
||||
- 若使用 Celery,需要額外設定:
|
||||
```bash
|
||||
# Windows 環境需要使用 eventlet 或 solo 執行器
|
||||
celery -A app.celery worker --loglevel=info --pool=solo
|
||||
```
|
||||
|
||||
2. **路徑設定**
|
||||
```python
|
||||
# Windows 環境請使用絕對路徑或適當的路徑分隔符
|
||||
UPLOAD_FOLDER = r'C:\path\to\uploads'
|
||||
```
|
||||
|
||||
3. **服務安裝**
|
||||
```bash
|
||||
# 使用 NSSM 將 Python 應用程式安裝為 Windows 服務
|
||||
nssm install TempSpecSystem python.exe app.py
|
||||
```
|
||||
|
||||
#### Linux 環境最佳化
|
||||
|
||||
1. **系統服務設定**
|
||||
```bash
|
||||
# 設定系統服務自動啟動
|
||||
sudo systemctl enable tempspec.service
|
||||
```
|
||||
|
||||
2. **日誌管理**
|
||||
```bash
|
||||
# 使用 logrotate 管理日誌檔案
|
||||
sudo nano /etc/logrotate.d/tempspec
|
||||
```
|
||||
|
||||
3. **效能調校**
|
||||
```bash
|
||||
# Gunicorn 推薦設定
|
||||
gunicorn -w 4 -k gevent --worker-connections 1000 -b 0.0.0.0:5000 app:app
|
||||
```
|
||||
|
||||
## 🔐 安全性設定
|
||||
|
||||
### LDAP 整合
|
||||
- 支援 SSL/TLS 加密連線
|
||||
- 服務帳號權限最小化原則
|
||||
- 自動用戶同步與權限管控
|
||||
|
||||
### 資料保護
|
||||
- JWT Token 驗證
|
||||
- 檔案存取權限控制
|
||||
- SQL Injection 防護
|
||||
- XSS 攻擊防護
|
||||
|
||||
## 📚 使用說明
|
||||
|
||||
### 登入規範
|
||||
|
||||
**重要**:系統要求使用完整的UPN格式帳號登入
|
||||
|
||||
✅ **正確格式**:`user@domain.com`
|
||||
❌ **錯誤格式**:`user`
|
||||
|
||||
### 初次設定管理員
|
||||
|
||||
系統預設所有使用者為 `viewer` 權限。設定管理員的方式:
|
||||
|
||||
1. **程式設定**:修改 `routes/auth.py` 中的預設管理員帳號
|
||||
|
||||
2. **手動設定**:在資料庫中更新用戶權限:
|
||||
```sql
|
||||
UPDATE ts_user SET role='admin' WHERE username='user@domain.com';
|
||||
```
|
||||
|
||||
3. **程式設定**:修改 `routes/auth.py` 中的管理員帳號列表:
|
||||
```python
|
||||
# 將特定用戶設為管理員
|
||||
if user_info['username'].lower() == 'your_admin@domain.com':
|
||||
default_role = 'admin'
|
||||
```
|
||||
|
||||
### 排程任務說明
|
||||
|
||||
系統預設每天凌晨 2:00 執行到期檢查任務,可在 `app.py` 中調整:
|
||||
|
||||
```python
|
||||
@scheduler.task('cron', id='check_expiring_specs_job', hour=2, minute=0)
|
||||
def scheduled_job():
|
||||
check_expiring_specs(app)
|
||||
```
|
||||
|
||||
### 自訂提醒天數
|
||||
|
||||
在 `tasks.py` 中修改提醒時程:
|
||||
|
||||
```python
|
||||
seven_days_later = today + timedelta(days=7) # 7天前提醒
|
||||
three_days_later = today + timedelta(days=3) # 3天前提醒
|
||||
```
|
||||
|
||||
### 預設收件人群組設定
|
||||
|
||||
在 `tasks.py` 中設定自動提醒的收件人:
|
||||
|
||||
```python
|
||||
# 修改為實際的 AD 群組名稱
|
||||
default_recipients = get_ldap_group_members('TempSpec_Admins')
|
||||
```
|
||||
|
||||
## 🐛 疑難排解
|
||||
|
||||
### 常見問題
|
||||
|
||||
1. **LDAP 連線失敗**
|
||||
- 檢查防火牆設定 (通常是 389/636 port)
|
||||
- 確認服務帳號權限
|
||||
- 驗證 LDAP 伺服器位址和搜尋基底
|
||||
|
||||
2. **ONLYOFFICE 無法載入**
|
||||
- 確認 Document Server 運行狀態:`docker ps`
|
||||
- 檢查網路連線設定
|
||||
- 驗證 JWT Secret 設定是否一致
|
||||
|
||||
3. **郵件發送失敗**
|
||||
- 確認 SMTP 設定正確
|
||||
- 檢查郵件伺服器認證
|
||||
- 驗證防火牆規則 (通常是 25/587/465 port)
|
||||
|
||||
4. **排程任務未執行**
|
||||
- 檢查 APScheduler 初始化
|
||||
- 確認應用程式持續運行
|
||||
- 查看系統日誌
|
||||
|
||||
5. **檔案上傳失敗**
|
||||
- 檢查上傳目錄權限
|
||||
- 確認檔案大小限制設定
|
||||
- 驗證磁碟空間是否足夠
|
||||
|
||||
### 日誌查看
|
||||
|
||||
```bash
|
||||
# Docker 環境
|
||||
docker-compose logs -f app
|
||||
|
||||
# 一般環境
|
||||
tail -f logs/app.log
|
||||
|
||||
# Windows 環境
|
||||
Get-Content logs/app.log -Tail 10 -Wait
|
||||
```
|
||||
|
||||
### 效能監控
|
||||
|
||||
```bash
|
||||
# 監控資源使用
|
||||
htop
|
||||
docker stats
|
||||
|
||||
# 檢查資料庫效能
|
||||
SHOW PROCESSLIST;
|
||||
SHOW ENGINE INNODB STATUS;
|
||||
```
|
||||
|
||||
## 🤝 開發指南
|
||||
|
||||
### 開發環境設定
|
||||
|
||||
1. **虛擬環境建立**
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux
|
||||
venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
2. **安裝開發依賴**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt # 如果有開發專用依賴
|
||||
```
|
||||
|
||||
3. **資料庫遷移**
|
||||
```bash
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
### 程式碼結構
|
||||
|
||||
```
|
||||
├── app.py # 主應用程式
|
||||
├── config.py # 組態設定
|
||||
├── models.py # 資料模型
|
||||
├── tasks.py # 排程任務
|
||||
├── routes/ # 路由模組
|
||||
│ ├── auth.py # 認證相關
|
||||
│ ├── temp_spec.py # 暫規管理
|
||||
│ ├── upload.py # 檔案上傳
|
||||
│ └── api.py # API介面
|
||||
├── templates/ # 前端範本
|
||||
├── static/ # 靜態檔案
|
||||
├── utils.py # 工具函式
|
||||
└── ldap_utils.py # LDAP 工具
|
||||
```
|
||||
|
||||
### 新增功能開發
|
||||
|
||||
1. **建立新的路由模組**
|
||||
2. **新增對應的資料模型**
|
||||
3. **建立前端範本**
|
||||
4. **撰寫單元測試**
|
||||
|
||||
## 📄 授權條款
|
||||
|
||||
本專案採用 MIT 授權條款,詳見 [LICENSE](LICENSE) 檔案。
|
||||
|
||||
## 🆕 版本歷程
|
||||
|
||||
### v3.0.0 (2024-01-XX)
|
||||
- 🆕 新增 LDAP/AD 整合驗證
|
||||
- 🆕 整合 ONLYOFFICE 線上編輯器
|
||||
- 🆕 實作智慧通知系統
|
||||
- 🆕 新增自動排程提醒功能
|
||||
- 🆕 支援 Docker 容器化部署
|
||||
- ♻️ 重構權限管理系統
|
||||
- 🗑️ 移除本地帳號管理功能
|
||||
|
||||
### v2.x.x
|
||||
- Toast UI Editor 整合
|
||||
- 基本文件管理功能
|
||||
- 本地帳號系統
|
||||
|
||||
## 📞 技術支援
|
||||
|
||||
如有問題或建議,請透過以下方式聯繫:
|
||||
|
||||
- 📧 Email: support@company.com
|
||||
- 📋 Issue Tracker: GitHub Issues
|
||||
- 📖 文件wiki: GitHub Wiki
|
||||
|
||||
---
|
||||
|
||||
## 執行應用程式
|
||||
|
||||
### 開發模式
|
||||
|
||||
**請確保您的 ONLYOFFICE Docker 容器已在運行中**,然後在另一個終端機視窗執行 `app.py`:
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
應用程式預設會在 `http://127.0.0.1:5000` 上執行。
|
||||
|
||||
### 生產環境
|
||||
|
||||
在生產環境中,建議使用生產級的 WSGI 伺服器,例如 `Gunicorn` (Linux) 或 `Waitress` (Windows)。
|
||||
|
||||
**使用 Waitress (Windows) 的範例:**
|
||||
|
||||
```bash
|
||||
pip install waitress
|
||||
waitress-serve --host=0.0.0.0 --port=8000 app:app
|
||||
```
|
||||
**暫時規範管理系統 V3** - 讓企業文件管理更智慧、更高效!
|
381
USER_MANUAL.md
381
USER_MANUAL.md
@@ -1,105 +1,364 @@
|
||||
# 系統操作說明書 (User Manual)
|
||||
# 暫時規範管理系統 V3 操作說明書
|
||||
|
||||
歡迎使用新版「暫時規範管理系統」。本說明書將引導您如何操作整合了 ONLYOFFICE 線上編輯器的新系統。
|
||||
歡迎使用企業級暫時規範管理系統 V3。本系統整合了LDAP認證、ONLYOFFICE線上編輯器及智慧通知系統,提供完整的文件生命週期管理解決方案。
|
||||
|
||||
## 📋 目錄
|
||||
|
||||
1. [系統簡介](#1-系統簡介)
|
||||
2. [登入與主畫面](#2-登入與主畫面)
|
||||
3. [核心操作流程](#3-核心操作流程)
|
||||
4. [智慧通知系統](#4-智慧通知系統)
|
||||
5. [進階功能](#5-進階功能)
|
||||
6. [角色權限說明](#6-角色權限說明)
|
||||
7. [常見問題](#7-常見問題)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系統簡介
|
||||
|
||||
本系統旨在提供一個集中化平台,用於管理、追蹤和存檔所有暫時性的工程規範。它涵蓋了從草擬、線上編輯、簽核、生效到最終歸檔的完整生命週期。
|
||||
暫時規範管理系統 V3 是一個集中化平台,用於管理、追蹤和存檔所有暫時性的工程規範。它涵蓋了從草擬、線上編輯、簽核、生效到最終歸檔的完整生命週期。
|
||||
|
||||
### 🚀 V3 版本新特色
|
||||
|
||||
- **LDAP/AD 整合**:使用企業Active Directory帳號登入
|
||||
- **智慧通知系統**:動態收件人選擇與自動提醒
|
||||
- **自動排程提醒**:系統主動發送到期提醒郵件
|
||||
- **增強的編輯體驗**:ONLYOFFICE文件協作編輯
|
||||
|
||||
---
|
||||
|
||||
## 2. 登入與主畫面
|
||||
|
||||
### 2.1 登入
|
||||
### 2.1 LDAP 登入
|
||||
|
||||
請使用管理員提供的帳號和密碼,在首頁進行登入。
|
||||
🆕 **V3 新功能**:系統現在使用企業 Active Directory 進行單一登入。
|
||||
|
||||
### 2.2 主畫面 (暫時規範總表)
|
||||
**🚨 重要登入規範**:
|
||||
|
||||
登入後的主畫面會列出所有暫時規範,功能包含:
|
||||
✅ **正確格式**:必須使用完整的 UPN 格式帳號
|
||||
例如:`user@domain.com`
|
||||
|
||||
- **建立新規範按鈕**: (僅 Editor/Admin 可見) 點擊此處開始建立一份新的規範。
|
||||
- **搜尋與篩選區**: 可根據「編號」、「主題」或「狀態」快速找到目標。
|
||||
- **規範列表**: 顯示每份規範的關鍵資訊。
|
||||
- **操作按鈕**: 根據您的「角色」和規範的「狀態」提供不同的操作選項。
|
||||
❌ **錯誤格式**:不支援縮略帳號
|
||||
例如:`user`
|
||||
|
||||
**登入步驟**:
|
||||
1. 在登入頁面輸入您的 **完整 AD 帳號**(例如:user@domain.com)
|
||||
2. 輸入您的 **AD 密碼**
|
||||
3. 點擊「**登入**」按鈕
|
||||
|
||||
> **注意**:
|
||||
> - 首次登入的用戶預設為 `Viewer` 權限
|
||||
> - 如需提升權限請聯繫系統管理員
|
||||
|
||||
### 2.2 主畫面功能
|
||||
|
||||
登入後進入暫時規範總表,包含以下區域:
|
||||
|
||||
- **🔍 搜尋與篩選區**:根據編號、主題或狀態快速查找
|
||||
- **📊 狀態統計**:顯示各狀態規範的數量概覽
|
||||
- **📋 規範列表**:詳細顯示所有規範資訊
|
||||
- **⚡ 快速操作**:根據權限和規範狀態提供操作按鈕
|
||||
|
||||
### 2.3 狀態指示說明
|
||||
|
||||
| 狀態 | 圖示 | 說明 |
|
||||
|------|------|------|
|
||||
| 待生效 | 🟡 | 草稿完成,待管理員啟用 |
|
||||
| 已生效 | 🟢 | 正在生效中的規範 |
|
||||
| 已過期 | 🔴 | 已自動過期的規範 |
|
||||
| 已終止 | ⚫ | 提早終止的規範 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心操作流程
|
||||
|
||||
### 3.1 流程總覽
|
||||
### 3.1 完整工作流程
|
||||
|
||||
新版系統的標準工作流程如下:
|
||||
|
||||
1. **Editor** 填寫初始資料表單,建立一份新的暫時規範草稿。
|
||||
2. 系統將初始資料**自動填入** Word 範本,並在瀏覽器中開啟 **ONLYOFFICE 線上編輯器**。
|
||||
3. **Editor** 在線上完成文件的詳細內容撰寫,文件會自動儲存。
|
||||
4. 完成後,可下載最終的 Word 文件 (`.docx`),進行線下簽核流程。
|
||||
5. 簽核完成後,將文件儲存為 **PDF (`.pdf`) 格式**。
|
||||
6. **Admin** 登入系統,上傳簽核完成的 PDF 檔案,正式**啟用**該規範。
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Editor建立草稿] --> B[ONLYOFFICE線上編輯]
|
||||
B --> C[下載Word文件]
|
||||
C --> D[線下簽核流程]
|
||||
D --> E[Admin上傳PDF啟用]
|
||||
E --> F[🆕選擇通知對象]
|
||||
F --> G[規範正式生效]
|
||||
G --> H[🆕自動到期提醒]
|
||||
```
|
||||
|
||||
### 3.2 建立新的暫時規範 (Editor / Admin)
|
||||
|
||||
1. 在主畫面點擊 **[+ 建立新規範]** 按鈕。
|
||||
2. 在「建立新的暫時規範」頁面,填寫所有初始資料,如主題、站別、Package 等。
|
||||
3. 點擊 **[建立並開始編輯]** 按鈕。
|
||||
4. 系統將會自動重新導向至 ONLYOFFICE 編輯器頁面,您會看到一個已預先填好初始資料的 Word 文件。
|
||||
5. 在線上編輯器中完成所有內容的修改與撰寫。您的所有變更都會**自動儲存**。完成後可直接關閉分頁或返回總表。
|
||||
1. **建立草稿**
|
||||
- 點擊 **[+ 暫時規範建立]** 按鈕
|
||||
- 填寫基本資料:主題、申請人、站別等
|
||||
- 選擇適用的 TCCS 等級和 4M 類別
|
||||
- 點擊 **[建立並開始編輯]**
|
||||
|
||||
2. **線上編輯**
|
||||
- 系統自動開啟 ONLYOFFICE 編輯器
|
||||
- 文件已預填初始資料
|
||||
- 支援多人協作編輯
|
||||
- 所有變更自動儲存
|
||||
|
||||
3. **完成草稿**
|
||||
- 編輯完成後可直接關閉編輯器
|
||||
- 或點擊 **[完成編輯]** 返回總表
|
||||
|
||||
### 3.3 啟用暫時規範 (僅限 Admin)
|
||||
|
||||
當一份規範的線下簽核流程完成後,管理員需執行以下操作使其生效:
|
||||
🆕 **新增智慧通知功能**
|
||||
|
||||
1. 在總表中找到狀態為「**待生效**」的目標規範。
|
||||
2. 點擊該規範右側的 **啟用圖示 (✅)**。
|
||||
3. 在「啟用暫時規範」頁面,點擊 **[選擇檔案]**,並上傳**已簽核完成的 PDF 檔案**。
|
||||
4. 點擊 **[啟用規範]** 按鈕。
|
||||
5. 完成後,該規範的狀態會變為「**已生效**」。
|
||||
1. **開始啟用流程**
|
||||
- 找到狀態為「待生效」的規範
|
||||
- 點擊 **啟用圖示 (✅)**
|
||||
|
||||
### 3.4 管理已生效的規範 (Editor / Admin)
|
||||
2. **上傳簽核文件**
|
||||
- 上傳已簽核完成的 PDF 檔案
|
||||
- 系統自動驗證檔案格式
|
||||
|
||||
對於「**已生效**」的規範,您可以進行展延或提早終止:
|
||||
3. **🆕 選擇通知對象**
|
||||
- 在「郵件通知對象」欄位輸入姓名或Email
|
||||
- 系統即時搜尋 LDAP 用戶
|
||||
- 支援多人選擇
|
||||
- 可選擇不發送通知
|
||||
|
||||
- **展延**:
|
||||
1. 點擊 **展延圖示 (📅+**)。
|
||||
2. 選擇新的結束日期,並**必須上傳新的佐證文件 (PDF 格式)**。
|
||||
3. 點擊儲存,規範的效期將會延長。
|
||||
- **終止**:
|
||||
1. 點擊 **終止圖示 (❌)**。
|
||||
2. 填寫提早終止的原因。
|
||||
3. 提交後,規範狀態將變為「**已終止**」。
|
||||
4. **確認啟用**
|
||||
- 點擊 **[上傳並啟用]**
|
||||
- 系統發送啟用通知郵件
|
||||
- 規範狀態變更為「已生效」
|
||||
|
||||
### 3.5 搜尋、篩選與下載
|
||||
### 3.4 展延暫時規範 (Editor / Admin)
|
||||
|
||||
- **搜尋/篩選**: 在主畫面的搜尋框和下拉選單中操作。
|
||||
- **下載**:
|
||||
- **待生效規範**:
|
||||
- Editor 和 Admin 可下載仍在編輯中的 **Word** 原始檔。
|
||||
- **已生效/已終止/已過期規範**:
|
||||
- 所有角色都可以下載由 Admin 上傳的**最終簽核版 PDF**。
|
||||
🆕 **新增智慧通知功能**
|
||||
|
||||
### 3.6 檢視歷史紀錄
|
||||
1. **開始展延**
|
||||
- 點擊 **展延圖示 (📅+)**
|
||||
- 選擇新的結束日期
|
||||
|
||||
點擊規範最右側的 **歷史紀錄圖示 (🕒)**,可查看該規範的所有變更紀錄。
|
||||
2. **上傳佐證文件**
|
||||
- 必須上傳新的佐證文件 (PDF格式)
|
||||
|
||||
3. **🆕 通知對象選擇**
|
||||
- 選擇需要通知的相關人員
|
||||
- 使用動態搜尋功能選擇收件人
|
||||
|
||||
4. **確認展延**
|
||||
- 點擊 **[確認展延]**
|
||||
- 系統發送展延通知郵件
|
||||
|
||||
### 3.5 終止暫時規範 (Editor / Admin)
|
||||
|
||||
🆕 **新增智慧通知功能**
|
||||
|
||||
1. **開始終止**
|
||||
- 點擊 **終止圖示 (❌)**
|
||||
- 填寫提早結束原因
|
||||
|
||||
2. **🆕 通知設定**
|
||||
- 選擇需要通知終止訊息的人員
|
||||
|
||||
3. **確認終止**
|
||||
- 點擊 **[確認終止]**
|
||||
- 系統發送終止通知郵件
|
||||
- 規範狀態變為「已終止」
|
||||
|
||||
---
|
||||
|
||||
## 4. 使用者管理 (僅限 Admin)
|
||||
## 4. 智慧通知系統
|
||||
|
||||
管理員可點擊導覽列的 **[帳號管理]** 來新增、編輯或刪除使用者帳號。
|
||||
🆕 **V3 全新功能**
|
||||
|
||||
### 4.1 動態收件人選擇
|
||||
|
||||
所有主要操作(啟用、展延、終止)都支援智慧通知:
|
||||
|
||||
- **即時搜尋**:輸入姓名或Email即時搜尋AD用戶
|
||||
- **多人選擇**:支援選擇多位收件人
|
||||
- **自動完成**:顯示完整姓名和Email資訊
|
||||
- **移除功能**:可隨時移除已選擇的收件人
|
||||
|
||||
### 4.2 通知郵件內容
|
||||
|
||||
系統會根據操作類型自動發送相應的通知郵件:
|
||||
|
||||
- **啟用通知**:包含規範編號、主題、生效/結束日期
|
||||
- **展延通知**:包含新的結束日期和展延原因
|
||||
- **終止通知**:包含終止原因和終止日期
|
||||
|
||||
### 4.3 🆕 自動到期提醒
|
||||
|
||||
系統每天自動檢查即將到期的規範:
|
||||
|
||||
- **7天前提醒**:首次到期預警
|
||||
- **3天前提醒**:最終到期提醒
|
||||
- **自動發送**:無需人工干預
|
||||
- **群組通知**:發送給預設管理群組
|
||||
|
||||
> **管理員設定**:可在 `tasks.py` 中修改提醒天數和收件人群組
|
||||
|
||||
---
|
||||
|
||||
## 5. 常見問題 (FAQ)
|
||||
## 5. 進階功能
|
||||
|
||||
**Q: 為什麼我看不到「建立規範」或「啟用」的按鈕?**
|
||||
A: 您的帳號權限不足。建立規範需要 `Editor` 或 `Admin` 權限;啟用規範僅限 `Admin`。如有需要,請聯繫系統管理員。
|
||||
### 5.1 搜尋與篩選
|
||||
|
||||
**Q: 我忘記密碼了怎麼辦?**
|
||||
A: 請聯繫系統管理員,請他/她為您重設密碼。
|
||||
**搜尋功能**:
|
||||
- 支援規範編號模糊搜尋
|
||||
- 支援主題關鍵字搜尋
|
||||
- 即時搜尋結果更新
|
||||
|
||||
**Q: 為什麼編輯器頁面顯示 "Download failed" 或無法儲存?**
|
||||
A: 這通常是開發環境的網路設定問題。請確認您主機的防火牆是否允許來自 Docker 的連線,或聯繫系統管理員檢查網路設定。
|
||||
**篩選功能**:
|
||||
- 按狀態篩選(待生效/已生效/已過期/已終止)
|
||||
- 多條件組合篩選
|
||||
- 篩選結果分頁顯示
|
||||
|
||||
**Q: 我可以上傳 Word 檔案來啟用規範嗎?**
|
||||
A: 不行。為了確保文件的最終性和不可修改性,系統規定必須上傳已簽核的 **PDF 檔案**來啟用規範。
|
||||
### 5.2 文件下載
|
||||
|
||||
**Word文件下載**:
|
||||
- Editor/Admin 可下載編輯中的Word原始檔
|
||||
- 適用於線下簽核流程
|
||||
|
||||
**PDF文件下載**:
|
||||
- 所有用戶都可下載最終簽核版PDF
|
||||
- 適用於已生效/已終止的規範
|
||||
|
||||
### 5.3 歷史紀錄追蹤
|
||||
|
||||
點擊 **歷史紀錄圖示 (🕒)** 查看:
|
||||
|
||||
- 操作時間戳記
|
||||
- 執行用戶
|
||||
- 操作類型(建立/啟用/展延/終止)
|
||||
- 詳細說明
|
||||
|
||||
### 5.4 🆕 即將到期警示
|
||||
|
||||
在規範列表中會特別標示即將到期的規範:
|
||||
|
||||
- **🟡 橙色標示**:7天內到期
|
||||
- **🔴 紅色標示**:3天內到期
|
||||
- **閃爍動畫**:今日到期
|
||||
|
||||
---
|
||||
|
||||
## 6. 角色權限說明
|
||||
|
||||
### 6.1 權限等級
|
||||
|
||||
| 角色 | 登入 | 檢視 | 建立 | 編輯 | 啟用 | 展延/終止 | 刪除 |
|
||||
|------|------|------|------|------|------|-----------|------|
|
||||
| **Viewer** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Editor** | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
| **Admin** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### 6.2 🆕 LDAP 權限管理
|
||||
|
||||
- **自動用戶創建**:AD用戶首次登入自動建立帳號
|
||||
- **預設權限**:新用戶預設為 `Viewer` 權限
|
||||
- **權限提升**:需要管理員手動調整資料庫或修改程式碼
|
||||
|
||||
**管理員手動提升權限**:
|
||||
```sql
|
||||
-- 使用完整UPN格式帳號
|
||||
UPDATE ts_user SET role='admin' WHERE username='user@domain.com';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 常見問題
|
||||
|
||||
### 7.1 登入相關
|
||||
|
||||
**Q: 無法使用AD帳號登入?**
|
||||
A: 請確認:
|
||||
1. **必須使用完整UPN格式**:例如 user@domain.com(不支援縮略帳號如 user)
|
||||
2. 密碼正確
|
||||
3. 帳號未被鎖定
|
||||
4. 聯繫IT部門確認LDAP連線狀態
|
||||
|
||||
**Q: 忘記密碼怎麼辦?**
|
||||
A: 系統使用AD驗證,請透過企業標準流程重設AD密碼。
|
||||
|
||||
### 7.2 權限相關
|
||||
|
||||
**Q: 為什麼我看不到建立規範按鈕?**
|
||||
A: 您的權限可能為 `Viewer`,請聯繫管理員提升權限至 `Editor`。
|
||||
|
||||
**Q: 為什麼我無法啟用規範?**
|
||||
A: 啟用功能僅限 `Admin` 權限,請聯繫系統管理員操作。
|
||||
|
||||
### 7.3 編輯器相關
|
||||
|
||||
**Q: ONLYOFFICE編輯器無法載入?**
|
||||
A: 請檢查:
|
||||
1. 瀏覽器是否允許彈出視窗
|
||||
2. 網路連線是否正常
|
||||
3. 是否安裝最新版瀏覽器
|
||||
4. 聯繫IT確認Document Server狀態
|
||||
|
||||
**Q: 編輯內容未儲存?**
|
||||
A: ONLYOFFICE有自動儲存功能,但請確認:
|
||||
1. 編輯期間保持網路連線
|
||||
2. 避免同時多人編輯同一文件
|
||||
3. 定期手動儲存 (Ctrl+S)
|
||||
|
||||
### 7.4 🆕 通知相關
|
||||
|
||||
**Q: 搜尋不到AD用戶?**
|
||||
A: 請確認:
|
||||
1. 輸入至少2個字元才開始搜尋
|
||||
2. 用戶在AD中確實存在
|
||||
3. 服務帳號有足夠權限搜尋AD
|
||||
|
||||
**Q: 沒有收到通知郵件?**
|
||||
A: 請檢查:
|
||||
1. Email地址是否正確
|
||||
2. 垃圾郵件資料夾
|
||||
3. 公司郵件伺服器設定
|
||||
4. 聯繫IT確認SMTP設定
|
||||
|
||||
**Q: 自動提醒郵件何時發送?**
|
||||
A: 系統每天凌晨2:00自動檢查並發送提醒,分別在到期前7天和3天發送。
|
||||
|
||||
### 7.5 檔案相關
|
||||
|
||||
**Q: 可以上傳Word檔案來啟用規範嗎?**
|
||||
A: 不可以。為確保文件完整性,啟用時必須上傳已簽核的 **PDF檔案**。
|
||||
|
||||
**Q: 檔案上傳失敗?**
|
||||
A: 請確認:
|
||||
1. 檔案格式正確(PDF)
|
||||
2. 檔案大小未超過限制
|
||||
3. 檔案名稱不含特殊字元
|
||||
4. 網路連線穩定
|
||||
|
||||
### 7.6 效能相關
|
||||
|
||||
**Q: 系統回應速度慢?**
|
||||
A: 可能原因:
|
||||
1. 網路連線問題
|
||||
2. 伺服器負載過高
|
||||
3. 資料庫查詢耗時
|
||||
4. 聯繫系統管理員檢查
|
||||
|
||||
---
|
||||
|
||||
## 🆘 技術支援
|
||||
|
||||
如遇到本手冊未涵蓋的問題,請透過以下方式聯繫:
|
||||
|
||||
- **📧 系統管理員**: [admin@company.com](mailto:admin@company.com)
|
||||
- **📞 IT支援專線**: 分機 2345
|
||||
- **💬 企業即時通**: #temp-spec-support
|
||||
|
||||
---
|
||||
|
||||
## 📝 版本資訊
|
||||
|
||||
- **文件版本**: V3.0.0
|
||||
- **最後更新**: 2024年1月
|
||||
- **適用系統**: 暫時規範管理系統 V3
|
||||
|
||||
---
|
||||
|
||||
**感謝您使用暫時規範管理系統 V3!**
|
||||
希望這個操作手冊能幫助您更有效地使用系統功能。如有任何建議或回饋,歡迎與我們聯繫。
|
41
app.py
41
app.py
@@ -1,10 +1,12 @@
|
||||
from flask import Flask, redirect, url_for, render_template
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_apscheduler import APScheduler
|
||||
from models import db, User
|
||||
from routes.auth import auth_bp
|
||||
from routes.temp_spec import temp_spec_bp
|
||||
from routes.upload import upload_bp
|
||||
from routes.admin import admin_bp
|
||||
from routes.api import api_bp
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('config.Config')
|
||||
@@ -12,6 +14,11 @@ app.config.from_object('config.Config')
|
||||
# 初始化資料庫
|
||||
db.init_app(app)
|
||||
|
||||
# 初始化排程器
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
# 初始化登入管理
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
@@ -40,6 +47,15 @@ app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(temp_spec_bp)
|
||||
app.register_blueprint(upload_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
# 導入任務
|
||||
from tasks import check_expiring_specs
|
||||
|
||||
# 註冊排程任務:每天凌晨 2:00 執行一次
|
||||
@scheduler.task('cron', id='check_expiring_specs_job', hour=2, minute=0)
|
||||
def scheduled_job():
|
||||
check_expiring_specs(app)
|
||||
|
||||
# 註冊錯誤處理函式
|
||||
@app.errorhandler(404)
|
||||
@@ -51,4 +67,29 @@ def forbidden_error(error):
|
||||
return render_template('403.html'), 403
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 設定日誌等級以便偵錯
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# 設定日誌輸出到 console
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
# 設定 Flask app 的日誌
|
||||
app.logger.setLevel(logging.INFO)
|
||||
app.logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
# 確保 LDAP 相關的日誌也能輸出
|
||||
ldap_logger = logging.getLogger('ldap_utils')
|
||||
ldap_logger.setLevel(logging.INFO)
|
||||
|
||||
print("=== 暫時規範系統 V3 啟動中 ===")
|
||||
print("日誌等級: INFO")
|
||||
print("系統準備就緒,可開始登入測試...")
|
||||
|
||||
app.run(debug=True)
|
||||
|
47
docker-compose.override.yml
Normal file
47
docker-compose.override.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Docker Compose Override 檔案
|
||||
# 用於開發環境的特殊設定
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
# 開發環境設定
|
||||
environment:
|
||||
FLASK_ENV: development
|
||||
FLASK_DEBUG: "true"
|
||||
|
||||
# 開發時啟用代碼熱重載
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/__pycache__
|
||||
|
||||
# 開發用命令
|
||||
command: >
|
||||
sh -c "
|
||||
echo 'Starting development server...' &&
|
||||
python init_db.py --auto-yes &&
|
||||
python app.py
|
||||
"
|
||||
|
||||
# 開發時不需要健康檢查
|
||||
healthcheck:
|
||||
disable: true
|
||||
|
||||
mysql:
|
||||
# 開發環境可以使用不同的密碼
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: dev123
|
||||
MYSQL_PASSWORD: dev123
|
||||
|
||||
# 開發時暴露 MySQL 端口以便外部連接
|
||||
ports:
|
||||
- "3307:3306"
|
||||
|
||||
onlyoffice:
|
||||
# 開發時可以禁用 JWT 以便測試
|
||||
environment:
|
||||
JWT_ENABLED: "false"
|
||||
|
||||
# 開發時暴露不同端口避免衝突
|
||||
ports:
|
||||
- "8081:80"
|
135
docker-compose.yml
Normal file
135
docker-compose.yml
Normal file
@@ -0,0 +1,135 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL 資料庫服務
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: tempspec-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-tempspec123}
|
||||
MYSQL_DATABASE: ${DB_NAME:-tempspec_db}
|
||||
MYSQL_USER: ${DB_USER:-tempspec_user}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD:-tempspec_pass}
|
||||
ports:
|
||||
- "${DB_PORT:-3306}:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./mysql/init:/docker-entrypoint-initdb.d:ro
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
networks:
|
||||
- tempspec-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD:-tempspec123}"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# ONLYOFFICE Document Server
|
||||
onlyoffice:
|
||||
image: onlyoffice/documentserver:latest
|
||||
container_name: tempspec-onlyoffice
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
JWT_ENABLED: "true"
|
||||
JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-your_jwt_secret_key_here}
|
||||
JWT_HEADER: "Authorization"
|
||||
JWT_IN_BODY: "true"
|
||||
ports:
|
||||
- "${ONLYOFFICE_PORT:-8080}:80"
|
||||
volumes:
|
||||
- onlyoffice_data:/var/www/onlyoffice/Data
|
||||
- onlyoffice_logs:/var/log/onlyoffice
|
||||
networks:
|
||||
- tempspec-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/healthcheck"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# Flask 應用程式
|
||||
app:
|
||||
build: .
|
||||
container_name: tempspec-app
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Flask 設定
|
||||
FLASK_ENV: ${FLASK_ENV:-production}
|
||||
SECRET_KEY: ${SECRET_KEY:-your-secret-key-here}
|
||||
|
||||
# 資料庫設定
|
||||
DATABASE_URL: mysql+pymysql://${DB_USER:-tempspec_user}:${DB_PASSWORD:-tempspec_pass}@mysql:3306/${DB_NAME:-tempspec_db}
|
||||
|
||||
# LDAP 設定
|
||||
LDAP_SERVER: ${LDAP_SERVER:-ldap://your-dc.company.com}
|
||||
LDAP_PORT: ${LDAP_PORT:-389}
|
||||
LDAP_USE_SSL: ${LDAP_USE_SSL:-False}
|
||||
LDAP_SEARCH_BASE: ${LDAP_SEARCH_BASE:-DC=company,DC=com}
|
||||
LDAP_BIND_USER_DN: ${LDAP_BIND_USER_DN:-CN=service,DC=company,DC=com}
|
||||
LDAP_BIND_USER_PASSWORD: ${LDAP_BIND_USER_PASSWORD:-service_password}
|
||||
LDAP_USER_LOGIN_ATTR: ${LDAP_USER_LOGIN_ATTR:-userPrincipalName}
|
||||
|
||||
# SMTP 郵件設定
|
||||
SMTP_SERVER: ${SMTP_SERVER:-smtp.company.com}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS:-True}
|
||||
SMTP_SENDER_EMAIL: ${SMTP_SENDER_EMAIL:-noreply@company.com}
|
||||
SMTP_SENDER_PASSWORD: ${SMTP_SENDER_PASSWORD:-smtp_password}
|
||||
|
||||
# ONLYOFFICE 設定
|
||||
ONLYOFFICE_URL: http://onlyoffice:80
|
||||
ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:-your_jwt_secret_key_here}
|
||||
|
||||
# 其他設定
|
||||
UPLOAD_FOLDER: uploads
|
||||
ports:
|
||||
- "${APP_PORT:-5000}:5000"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./static/generated:/app/static/generated
|
||||
- ./logs:/app/logs
|
||||
- ./template_with_placeholders.docx:/app/template_with_placeholders.docx:ro
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
onlyoffice:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- tempspec-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# Nginx 反向代理 (可選)
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: tempspec-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${NGINX_PORT:-80}:80"
|
||||
- "${NGINX_SSL_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
depends_on:
|
||||
- app
|
||||
networks:
|
||||
- tempspec-network
|
||||
profiles:
|
||||
- production
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
onlyoffice_data:
|
||||
driver: local
|
||||
onlyoffice_logs:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
tempspec-network:
|
||||
driver: bridge
|
52
init_db.py
52
init_db.py
@@ -1,40 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
from flask import Flask
|
||||
from werkzeug.security import generate_password_hash
|
||||
from models import db, User
|
||||
from models import db
|
||||
from config import Config
|
||||
|
||||
def create_default_admin(app):
|
||||
"""在應用程式上下文中建立一個預設的管理員帳號。"""
|
||||
with app.app_context():
|
||||
# 檢查管理員是否已存在
|
||||
if User.query.filter_by(username='admin').first():
|
||||
print("ℹ️ 'admin' 使用者已存在,跳過建立程序。")
|
||||
return
|
||||
|
||||
# 產生一個安全隨機的密碼
|
||||
password_length = 12
|
||||
alphabet = string.ascii_letters + string.digits + '!@#$%^&*()'
|
||||
password = ''.join(secrets.choice(alphabet) for i in range(password_length))
|
||||
|
||||
# 建立新使用者
|
||||
admin_user = User(
|
||||
username='admin',
|
||||
password_hash=generate_password_hash(password),
|
||||
role='admin'
|
||||
)
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
|
||||
print("✅ 預設管理員帳號已建立!")
|
||||
print(" ===================================")
|
||||
print(f" 👤 使用者名稱: admin")
|
||||
print(f" 🔑 密碼: {password}")
|
||||
print(" ===================================")
|
||||
print(" 請妥善保管此密碼,並在首次登入後考慮變更。")
|
||||
def create_admin_note():
|
||||
"""顯示 LDAP 管理員設定說明。"""
|
||||
print("✅ 資料庫已初始化!")
|
||||
print(" ===================================")
|
||||
print(" 📋 LDAP 管理員設定說明")
|
||||
print(" ===================================")
|
||||
print(" 由於系統使用 LDAP 驗證,管理員權限需要在首次登入後手動設定。")
|
||||
print(" ")
|
||||
print(" 步驟:")
|
||||
print(" 1. 使用 AD 帳號登入系統")
|
||||
print(" 2. 直接在資料庫中將該用戶的 role 更新為 'admin'")
|
||||
print(" 3. 或修改 auth.py 中新用戶的預設權限設定")
|
||||
print(" ")
|
||||
print(" SQL 範例:")
|
||||
print(" UPDATE ts_user SET role='admin' WHERE username='你的AD帳號';")
|
||||
print(" ===================================")
|
||||
|
||||
def init_database(app):
|
||||
"""初始化資料庫:刪除所有現有資料表並重新建立。"""
|
||||
@@ -66,7 +50,7 @@ if __name__ == '__main__':
|
||||
|
||||
if confirmation.lower() == 'yes':
|
||||
init_database(app)
|
||||
create_default_admin(app)
|
||||
create_admin_note()
|
||||
print("\n🎉 全部完成!")
|
||||
else:
|
||||
print("❌ 操作已取消。")
|
438
ldap_utils.py
438
ldap_utils.py
@@ -1,4 +1,4 @@
|
||||
from ldap3 import Server, Connection, ALL, Tls
|
||||
from ldap3 import Server, Connection, ALL, Tls, SUBTREE
|
||||
import ssl
|
||||
from flask import current_app
|
||||
|
||||
@@ -6,16 +6,14 @@ def authenticate_ldap_user(username, password):
|
||||
"""
|
||||
Authenticates a user against the LDAP server using their credentials.
|
||||
Returns a dictionary with user info upon success, otherwise None.
|
||||
要求使用完整的UPN格式帳號 (例如: user@panjit.com.tw)
|
||||
"""
|
||||
# Ensure username is in UPN format (e.g., user@domain.com)
|
||||
# Assuming the domain part is derivable or static.
|
||||
# This logic might need adjustment based on how users log in.
|
||||
# 驗證帳號格式必須包含 @ 符號
|
||||
if '@' not in username:
|
||||
# This assumes a fixed domain, which should be configured or improved
|
||||
domain = current_app.config['LDAP_SEARCH_BASE'].split('dc=')[1].replace(',', '.')
|
||||
user_upn = f"{username}@{domain}"
|
||||
else:
|
||||
user_upn = username
|
||||
current_app.logger.error(f"Invalid username format: {username}. Must use full UPN format (e.g., user@domain.com)")
|
||||
return None
|
||||
|
||||
user_upn = username
|
||||
|
||||
ldap_server = current_app.config['LDAP_SERVER']
|
||||
ldap_port = current_app.config['LDAP_PORT']
|
||||
@@ -29,46 +27,190 @@ def authenticate_ldap_user(username, password):
|
||||
server = Server(**server_options, get_info=ALL)
|
||||
|
||||
try:
|
||||
print(f"[DEBUG] LDAP 連線資訊:")
|
||||
print(f" - 伺服器: {ldap_server}:{ldap_port}")
|
||||
print(f" - SSL: {use_ssl}")
|
||||
print(f" - 使用者 UPN: {user_upn}")
|
||||
current_app.logger.info(f"Connecting to LDAP server: {ldap_server}:{ldap_port}")
|
||||
|
||||
# Attempt to bind with the user's credentials to authenticate
|
||||
print(f"[DEBUG] 嘗試 LDAP 連線綁定...")
|
||||
conn = Connection(server, user=user_upn, password=password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
print(f"[DEBUG] LDAP 連線綁定成功!")
|
||||
current_app.logger.info(f"LDAP bind successful for: {user_upn}")
|
||||
|
||||
# Authentication successful. Now, get user details.
|
||||
search_base = current_app.config['LDAP_SEARCH_BASE']
|
||||
login_attr = current_app.config['LDAP_USER_LOGIN_ATTR']
|
||||
search_filter = f'({login_attr}={user_upn})'
|
||||
|
||||
conn.search(search_base, search_filter, attributes=['dn', 'mail', 'displayName', 'sAMAccountName'])
|
||||
current_app.logger.debug(f"LDAP search - Base: {search_base}, Filter: {search_filter}")
|
||||
|
||||
conn.search(search_base, search_filter, attributes=['mail', 'displayName', 'sAMAccountName'])
|
||||
|
||||
if conn.entries:
|
||||
entry = conn.entries[0]
|
||||
user_info = {
|
||||
'dn': entry.entry_dn,
|
||||
'email': str(entry.mail) if 'mail' in entry else None,
|
||||
'display_name': str(entry.displayName) if 'displayName' in entry else None,
|
||||
'username': str(entry.sAMAccountName) if 'sAMAccountName' in entry else username
|
||||
'dn': entry.entry_dn, # DN 直接從 entry 物件獲取
|
||||
'email': str(entry.mail) if 'mail' in entry and entry.mail else None,
|
||||
'display_name': str(entry.displayName) if 'displayName' in entry and entry.displayName else None,
|
||||
'username': user_upn # 使用原始UPN作為username
|
||||
}
|
||||
print(f"[DEBUG] 使用者詳細資訊:")
|
||||
print(f" - 顯示名稱: {user_info['display_name']}")
|
||||
print(f" - Email: {user_info['email']}")
|
||||
print(f" - 使用者名稱: {user_info['username']}")
|
||||
print(f" - DN: {user_info['dn']}")
|
||||
current_app.logger.info(f"User details retrieved: {user_info['display_name']} ({user_upn})")
|
||||
conn.unbind()
|
||||
return user_info
|
||||
else:
|
||||
# This case is unlikely if bind succeeded, but handle it just in case
|
||||
current_app.logger.warning(f"LDAP bind successful but user not found in search: {user_upn}")
|
||||
conn.unbind()
|
||||
return None
|
||||
else:
|
||||
# Authentication failed
|
||||
print(f"[DEBUG] LDAP 連線綁定失敗! 可能是帳號密碼錯誤")
|
||||
current_app.logger.warning(f"LDAP bind failed for: {user_upn}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
# Log the exception in a real application
|
||||
print(f"LDAP authentication error: {e}")
|
||||
# Log the exception with more detail
|
||||
print(f"[DEBUG] LDAP 連線發生異常: {str(e)}")
|
||||
print(f"[DEBUG] 伺服器設定 - {ldap_server}:{ldap_port}, SSL={use_ssl}")
|
||||
current_app.logger.error(f"LDAP authentication error for {user_upn}: {str(e)}")
|
||||
current_app.logger.error(f"LDAP server: {ldap_server}, Port: {ldap_port}, SSL: {use_ssl}")
|
||||
return None
|
||||
|
||||
|
||||
def get_ldap_group_members(group_name):
|
||||
"""
|
||||
Retrieves a list of email addresses for members of a given LDAP group.
|
||||
Retrieves a list of email addresses for members of a given LDAP group or organizational unit.
|
||||
Uses the application's bind credentials for searching.
|
||||
Enhanced with detailed debugging.
|
||||
"""
|
||||
print(f"[GROUP DEBUG] 開始獲取群組成員: {group_name}")
|
||||
|
||||
ldap_server = current_app.config['LDAP_SERVER']
|
||||
ldap_port = current_app.config['LDAP_PORT']
|
||||
use_ssl = current_app.config['LDAP_USE_SSL']
|
||||
bind_dn = current_app.config['LDAP_BIND_USER_DN']
|
||||
bind_password = current_app.config['LDAP_BIND_USER_PASSWORD']
|
||||
search_base = current_app.config['LDAP_SEARCH_BASE']
|
||||
|
||||
print(f"[GROUP DEBUG] LDAP 設定:")
|
||||
print(f"[GROUP DEBUG] - 伺服器: {ldap_server}:{ldap_port}")
|
||||
print(f"[GROUP DEBUG] - 搜尋基底: {search_base}")
|
||||
|
||||
server_options = {'host': ldap_server, 'port': ldap_port, 'use_ssl': use_ssl}
|
||||
if use_ssl:
|
||||
tls_config = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
|
||||
server_options['tls'] = tls_config
|
||||
|
||||
server = Server(**server_options, get_info=ALL)
|
||||
|
||||
try:
|
||||
# Bind with the service account
|
||||
conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
print(f"[GROUP DEBUG] LDAP 服務帳號連線成功")
|
||||
|
||||
# 嘗試多種搜尋方式
|
||||
emails = []
|
||||
|
||||
# 1. 首先嘗試按 cn 搜尋群組
|
||||
print(f"[GROUP DEBUG] 嘗試搜尋群組 (cn): {group_name}")
|
||||
group_search_filter = f'(&(objectClass=group)(cn={group_name}))'
|
||||
conn.search(search_base, group_search_filter, attributes=['member', 'mail'])
|
||||
|
||||
if conn.entries:
|
||||
print(f"[GROUP DEBUG] 找到群組: {conn.entries[0].entry_dn}")
|
||||
members_dn = conn.entries[0].member.values if 'member' in conn.entries[0] else []
|
||||
print(f"[GROUP DEBUG] 群組有 {len(members_dn)} 個成員")
|
||||
|
||||
# 獲取成員郵件
|
||||
for i, member_dn in enumerate(members_dn):
|
||||
print(f"[GROUP DEBUG] 處理成員 {i+1}: {member_dn}")
|
||||
try:
|
||||
member_conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
member_conn.search(member_dn, '(objectClass=*)', attributes=['mail', 'sAMAccountName', 'displayName'])
|
||||
if member_conn.entries and 'mail' in member_conn.entries[0]:
|
||||
email = str(member_conn.entries[0].mail)
|
||||
emails.append(email)
|
||||
print(f"[GROUP DEBUG] 獲取成員郵件: {email}")
|
||||
elif member_conn.entries:
|
||||
# 如果沒有 mail 屬性,嘗試用 sAMAccountName 生成
|
||||
sam = str(member_conn.entries[0].sAMAccountName) if 'sAMAccountName' in member_conn.entries[0] else None
|
||||
if sam:
|
||||
email = f"{sam}@panjit.com.tw"
|
||||
emails.append(email)
|
||||
print(f"[GROUP DEBUG] 生成成員郵件: {email}")
|
||||
member_conn.unbind()
|
||||
except Exception as member_error:
|
||||
print(f"[GROUP DEBUG] 處理成員錯誤: {member_error}")
|
||||
else:
|
||||
# 2. 如果找不到群組,嘗試按 OU 搜尋組織單位
|
||||
print(f"[GROUP DEBUG] 群組未找到,嘗試搜尋組織單位: {group_name}")
|
||||
ou_search_filter = f'(&(objectClass=organizationalUnit)(|(ou=*{group_name}*)(name=*{group_name}*)))'
|
||||
conn.search(search_base, ou_search_filter, attributes=['ou', 'name'])
|
||||
|
||||
if conn.entries:
|
||||
print(f"[GROUP DEBUG] 找到組織單位: {conn.entries[0].entry_dn}")
|
||||
# 搜尋組織單位下的所有用戶
|
||||
ou_dn = conn.entries[0].entry_dn
|
||||
user_conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
user_conn.search(
|
||||
ou_dn,
|
||||
'(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))',
|
||||
attributes=['mail', 'sAMAccountName', 'displayName'],
|
||||
search_scope=SUBTREE,
|
||||
size_limit=50
|
||||
)
|
||||
|
||||
print(f"[GROUP DEBUG] 在組織單位下找到 {len(user_conn.entries)} 個用戶")
|
||||
for entry in user_conn.entries:
|
||||
if 'mail' in entry:
|
||||
email = str(entry.mail)
|
||||
emails.append(email)
|
||||
print(f"[GROUP DEBUG] 獲取用戶郵件: {email}")
|
||||
elif 'sAMAccountName' in entry:
|
||||
sam = str(entry.sAMAccountName)
|
||||
email = f"{sam}@panjit.com.tw"
|
||||
emails.append(email)
|
||||
print(f"[GROUP DEBUG] 生成用戶郵件: {email}")
|
||||
|
||||
user_conn.unbind()
|
||||
else:
|
||||
print(f"[GROUP DEBUG] 找不到群組或組織單位: {group_name}")
|
||||
|
||||
conn.unbind()
|
||||
print(f"[GROUP DEBUG] 最終獲取 {len(emails)} 個郵件地址")
|
||||
return emails
|
||||
else:
|
||||
print("[GROUP ERROR] Failed to bind to LDAP with service account.")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f"LDAP group search error: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def search_ldap_principals(search_term, limit=10):
|
||||
"""
|
||||
Searches for LDAP principals (users) based on a search term.
|
||||
Returns a list of dictionaries with 'name' and 'email' keys.
|
||||
Uses the application's bind credentials for searching.
|
||||
"""
|
||||
if not search_term or len(search_term.strip()) < 2:
|
||||
print(f"[DEBUG] search_ldap_principals: 搜尋詞無效 '{search_term}'")
|
||||
return []
|
||||
|
||||
print(f"[DEBUG] search_ldap_principals: 搜尋 '{search_term}'")
|
||||
|
||||
ldap_server = current_app.config['LDAP_SERVER']
|
||||
ldap_port = current_app.config['LDAP_PORT']
|
||||
use_ssl = current_app.config['LDAP_USE_SSL']
|
||||
@@ -84,35 +226,259 @@ def get_ldap_group_members(group_name):
|
||||
server = Server(**server_options, get_info=ALL)
|
||||
|
||||
try:
|
||||
print(f"[DEBUG] LDAP 搜尋設定:")
|
||||
print(f" - 伺服器: {ldap_server}:{ldap_port}")
|
||||
print(f" - 搜尋基底: {search_base}")
|
||||
print(f" - 服務帳號: {bind_dn}")
|
||||
|
||||
# Bind with the service account
|
||||
conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
# First, find the group to get its member list
|
||||
group_search_filter = f'(&(objectClass=group)(cn={group_name}))'
|
||||
conn.search(search_base, group_search_filter, attributes=['member'])
|
||||
print(f"[DEBUG] LDAP 服務帳號連線成功")
|
||||
|
||||
# Search for users matching the search term in multiple attributes
|
||||
search_filter = f'(&(objectClass=user)(objectCategory=person)(|(displayName=*{search_term}*)(sAMAccountName=*{search_term}*)(mail=*{search_term}*))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
|
||||
print(f"[DEBUG] LDAP 搜尋篩選器: {search_filter}")
|
||||
|
||||
conn.search(
|
||||
search_base,
|
||||
search_filter,
|
||||
attributes=['displayName', 'mail', 'sAMAccountName'],
|
||||
size_limit=limit
|
||||
)
|
||||
|
||||
print(f"[DEBUG] LDAP 搜尋找到 {len(conn.entries)} 個條目")
|
||||
|
||||
if not conn.entries:
|
||||
print(f"LDAP group '{group_name}' not found.")
|
||||
conn.unbind()
|
||||
return []
|
||||
|
||||
members_dn = conn.entries[0].member.values
|
||||
emails = []
|
||||
|
||||
# For each member DN, fetch their email
|
||||
for member_dn in members_dn:
|
||||
member_filter = f'(objectDN={member_dn})'
|
||||
conn.search(member_dn, '(objectClass=*)', attributes=['mail'])
|
||||
if conn.entries and 'mail' in conn.entries[0]:
|
||||
emails.append(str(conn.entries[0].mail))
|
||||
results = []
|
||||
for i, entry in enumerate(conn.entries):
|
||||
display_name = str(entry.displayName) if 'displayName' in entry and entry.displayName else None
|
||||
email = str(entry.mail) if 'mail' in entry and entry.mail else None
|
||||
sam_account = str(entry.sAMAccountName) if 'sAMAccountName' in entry and entry.sAMAccountName else None
|
||||
|
||||
print(f"[DEBUG] 條目 {i+1}: {display_name}, {email}, {sam_account}")
|
||||
|
||||
# Include entries with display name, generate email if missing
|
||||
if display_name:
|
||||
# Generate email from SAM account if not provided
|
||||
if not email and sam_account:
|
||||
email = f"{sam_account}@panjit.com.tw"
|
||||
|
||||
result = {
|
||||
'name': display_name,
|
||||
'email': email or 'No Email',
|
||||
'username': sam_account
|
||||
}
|
||||
results.append(result)
|
||||
print(f"[DEBUG] 加入結果: {result}")
|
||||
else:
|
||||
print(f"[DEBUG] 跳過條目 (缺少顯示名稱)")
|
||||
|
||||
conn.unbind()
|
||||
return emails
|
||||
print(f"[DEBUG] 最終返回 {len(results)} 個結果")
|
||||
return results
|
||||
else:
|
||||
print("[DEBUG] Failed to bind to LDAP with service account.")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] LDAP principal search error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
|
||||
def search_ldap_groups(search_term, limit=10):
|
||||
"""
|
||||
搜尋 LDAP 群組 (組織) 用於郵件發送
|
||||
Returns a list of dictionaries with 'name' and 'members' keys.
|
||||
"""
|
||||
if not search_term or len(search_term.strip()) < 2:
|
||||
print(f"[DEBUG] search_ldap_groups: 搜尋詞無效 '{search_term}'")
|
||||
return []
|
||||
|
||||
print(f"[DEBUG] search_ldap_groups: 搜尋群組 '{search_term}'")
|
||||
|
||||
ldap_server = current_app.config['LDAP_SERVER']
|
||||
ldap_port = current_app.config['LDAP_PORT']
|
||||
use_ssl = current_app.config['LDAP_USE_SSL']
|
||||
bind_dn = current_app.config['LDAP_BIND_USER_DN']
|
||||
bind_password = current_app.config['LDAP_BIND_USER_PASSWORD']
|
||||
search_base = current_app.config['LDAP_SEARCH_BASE']
|
||||
|
||||
server_options = {'host': ldap_server, 'port': ldap_port, 'use_ssl': use_ssl}
|
||||
if use_ssl:
|
||||
tls_config = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
|
||||
server_options['tls'] = tls_config
|
||||
|
||||
server = Server(**server_options, get_info=ALL)
|
||||
|
||||
try:
|
||||
print(f"[DEBUG] LDAP 群組搜尋設定:")
|
||||
print(f" - 伺服器: {ldap_server}:{ldap_port}")
|
||||
print(f" - 搜尋基底: {search_base}")
|
||||
|
||||
# Bind with the service account
|
||||
conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
print(f"[DEBUG] LDAP 服務帳號連線成功")
|
||||
|
||||
# 先搜尋組織單位,再搜尋群組,分開處理避免複雜的篩選器語法
|
||||
results = []
|
||||
|
||||
# 1. 搜尋組織單位
|
||||
ou_filter = f'(&(objectClass=organizationalUnit)(|(ou=*{search_term}*)(name=*{search_term}*)))'
|
||||
print(f"[DEBUG] LDAP 組織單位搜尋篩選器: {ou_filter}")
|
||||
|
||||
try:
|
||||
conn.search(
|
||||
search_base,
|
||||
ou_filter,
|
||||
attributes=['ou', 'name', 'mail'],
|
||||
size_limit=limit//2
|
||||
)
|
||||
|
||||
print(f"[DEBUG] 找到 {len(conn.entries)} 個組織單位")
|
||||
|
||||
for i, entry in enumerate(conn.entries):
|
||||
ou = str(entry.ou) if 'ou' in entry and entry.ou else None
|
||||
name = str(entry.name) if 'name' in entry and entry.name else None
|
||||
group_mail = str(entry.mail) if 'mail' in entry and entry.mail else None
|
||||
|
||||
group_name = ou or name
|
||||
|
||||
if group_name:
|
||||
# 計算組織單位下的成員數
|
||||
member_count = 0
|
||||
try:
|
||||
# 使用新的連線來計算成員,避免覆蓋當前結果
|
||||
temp_conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
if temp_conn.bound:
|
||||
temp_conn.search(
|
||||
entry.entry_dn,
|
||||
'(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))',
|
||||
attributes=['sAMAccountName'],
|
||||
size_limit=100,
|
||||
search_scope=SUBTREE
|
||||
)
|
||||
member_count = len(temp_conn.entries)
|
||||
temp_conn.unbind()
|
||||
except Exception as member_err:
|
||||
print(f"[DEBUG] 計算組織成員數錯誤: {member_err}")
|
||||
member_count = 0
|
||||
|
||||
result = {
|
||||
'name': group_name,
|
||||
'email': group_mail,
|
||||
'type': 'organizationalUnit',
|
||||
'member_count': member_count,
|
||||
'dn': entry.entry_dn,
|
||||
'members': []
|
||||
}
|
||||
results.append(result)
|
||||
print(f"[DEBUG] 加入組織單位: {result}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] 組織單位搜尋錯誤: {e}")
|
||||
|
||||
# 2. 搜尋群組
|
||||
group_filter = f'(&(objectClass=group)(|(cn=*{search_term}*)(displayName=*{search_term}*)))'
|
||||
print(f"[DEBUG] LDAP 群組搜尋篩選器: {group_filter}")
|
||||
|
||||
try:
|
||||
conn.search(
|
||||
search_base,
|
||||
group_filter,
|
||||
attributes=['cn', 'displayName', 'member', 'mail'],
|
||||
size_limit=limit//2
|
||||
)
|
||||
|
||||
print(f"[DEBUG] 找到 {len(conn.entries)} 個群組")
|
||||
|
||||
for i, entry in enumerate(conn.entries):
|
||||
cn = str(entry.cn) if 'cn' in entry and entry.cn else None
|
||||
display_name = str(entry.displayName) if 'displayName' in entry and entry.displayName else None
|
||||
group_mail = str(entry.mail) if 'mail' in entry and entry.mail else None
|
||||
members = entry.member.values if 'member' in entry else []
|
||||
|
||||
group_name = display_name or cn
|
||||
|
||||
print(f"[DEBUG] 群組 {i+1}: CN={cn}, DisplayName={display_name}")
|
||||
print(f"[DEBUG] 群組名稱: {group_name}, 成員數: {len(members)}")
|
||||
|
||||
if group_name:
|
||||
result = {
|
||||
'name': group_name,
|
||||
'email': group_mail,
|
||||
'type': 'group',
|
||||
'member_count': len(members),
|
||||
'dn': entry.entry_dn,
|
||||
'members': members[:5] # 只顯示前5個成員作為預覽
|
||||
}
|
||||
results.append(result)
|
||||
print(f"[DEBUG] 加入群組結果: {result}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] 群組搜尋錯誤: {e}")
|
||||
|
||||
conn.unbind()
|
||||
print(f"[DEBUG] 最終返回 {len(results)} 個群組結果")
|
||||
return results
|
||||
else:
|
||||
print("[DEBUG] Failed to bind to LDAP with service account.")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] LDAP group search error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
|
||||
def get_group_member_emails(group_dn):
|
||||
"""
|
||||
獲取群組內所有成員的郵件地址
|
||||
"""
|
||||
ldap_server = current_app.config['LDAP_SERVER']
|
||||
ldap_port = current_app.config['LDAP_PORT']
|
||||
use_ssl = current_app.config['LDAP_USE_SSL']
|
||||
bind_dn = current_app.config['LDAP_BIND_USER_DN']
|
||||
bind_password = current_app.config['LDAP_BIND_USER_PASSWORD']
|
||||
|
||||
server_options = {'host': ldap_server, 'port': ldap_port, 'use_ssl': use_ssl}
|
||||
if use_ssl:
|
||||
tls_config = Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
|
||||
server_options['tls'] = tls_config
|
||||
|
||||
server = Server(**server_options, get_info=ALL)
|
||||
|
||||
try:
|
||||
conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
# First get the group members
|
||||
conn.search(group_dn, '(objectClass=*)', attributes=['member'])
|
||||
|
||||
if conn.entries and 'member' in conn.entries[0]:
|
||||
members_dn = conn.entries[0].member.values
|
||||
emails = []
|
||||
|
||||
# For each member DN, fetch their email
|
||||
for member_dn in members_dn:
|
||||
conn.search(member_dn, '(objectClass=*)', attributes=['mail'])
|
||||
if conn.entries and 'mail' in conn.entries[0] and conn.entries[0].mail:
|
||||
emails.append(str(conn.entries[0].mail))
|
||||
|
||||
conn.unbind()
|
||||
return emails
|
||||
|
||||
conn.unbind()
|
||||
return []
|
||||
else:
|
||||
print("Failed to bind to LDAP with service account.")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f"LDAP group search error: {e}")
|
||||
print(f"Get group member emails error: {e}")
|
||||
return []
|
||||
|
18
mysql/init/01-init.sql
Normal file
18
mysql/init/01-init.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- 初始化暫時規範管理系統資料庫
|
||||
-- 此檔案會在 MySQL 容器啟動時自動執行
|
||||
|
||||
-- 設定字符集和排序規則
|
||||
ALTER DATABASE tempspec_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- 確保時區設定
|
||||
SET time_zone = '+08:00';
|
||||
|
||||
-- 創建必要的索引以提升效能
|
||||
-- 注意:資料表結構由 SQLAlchemy 自動創建,這裡只需要創建額外的索引
|
||||
|
||||
-- 記錄初始化完成
|
||||
INSERT INTO mysql.general_log (event_time, user_host, thread_id, server_id, command_type, argument)
|
||||
VALUES (NOW(), 'init_script', 0, 1, 'Query', 'TempSpec Database Initialized');
|
||||
|
||||
-- 輸出初始化資訊
|
||||
SELECT 'TempSpec System Database Initialized Successfully' as STATUS;
|
83
nginx/conf.d/default.conf
Normal file
83
nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,83 @@
|
||||
# 暫時規範管理系統主站
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 重定向到 HTTPS (可選,需要 SSL 證書)
|
||||
# return 301 https://$server_name$request_uri;
|
||||
|
||||
# 應用程式代理
|
||||
location / {
|
||||
proxy_pass http://app:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 支援
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# 超時設定
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# ONLYOFFICE Document Server 代理
|
||||
location /onlyoffice/ {
|
||||
proxy_pass http://onlyoffice:80/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# ONLYOFFICE 特殊設定
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
|
||||
# 大檔案上傳支援
|
||||
client_max_body_size 100M;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
# 靜態檔案快取
|
||||
location /static/ {
|
||||
proxy_pass http://app:5000/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 健康檢查
|
||||
location /health {
|
||||
proxy_pass http://app:5000/;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 錯誤頁面
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
}
|
||||
|
||||
# HTTPS 設定 (需要 SSL 證書)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name localhost;
|
||||
#
|
||||
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
||||
# ssl_prefer_server_ciphers off;
|
||||
# ssl_session_cache shared:SSL:10m;
|
||||
# ssl_session_timeout 10m;
|
||||
#
|
||||
# # HSTS
|
||||
# add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
#
|
||||
# # 其餘配置與 HTTP 相同
|
||||
# # ...
|
||||
# }
|
71
nginx/nginx.conf
Normal file
71
nginx/nginx.conf
Normal file
@@ -0,0 +1,71 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日誌格式
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# 基本設定
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# 檔案大小限制
|
||||
client_max_body_size 100M;
|
||||
client_body_timeout 120s;
|
||||
client_header_timeout 120s;
|
||||
|
||||
# Gzip 壓縮
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
application/atom+xml
|
||||
application/geo+json
|
||||
application/javascript
|
||||
application/x-javascript
|
||||
application/json
|
||||
application/ld+json
|
||||
application/manifest+json
|
||||
application/rdf+xml
|
||||
application/rss+xml
|
||||
application/xhtml+xml
|
||||
application/xml
|
||||
font/eot
|
||||
font/otf
|
||||
font/ttf
|
||||
image/svg+xml
|
||||
text/css
|
||||
text/javascript
|
||||
text/plain
|
||||
text/xml;
|
||||
|
||||
# 安全標頭
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
|
||||
# 包含站點配置
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
@@ -11,4 +11,5 @@ lxml
|
||||
python-dotenv
|
||||
mistune
|
||||
PyJWT
|
||||
ldap3
|
||||
ldap3
|
||||
Flask-APScheduler
|
112
routes/admin.py
112
routes/admin.py
@@ -1,76 +1,96 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.security import generate_password_hash
|
||||
from models import User, db
|
||||
from utils import admin_required
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@admin_bp.before_request
|
||||
@admin_bp.route('/users')
|
||||
@login_required
|
||||
@admin_required
|
||||
def before_request():
|
||||
"""在處理此藍圖中的任何請求之前,確保使用者是已登入的管理員。"""
|
||||
pass
|
||||
|
||||
@admin_bp.route('/users')
|
||||
def user_list():
|
||||
users = User.query.all()
|
||||
"""顯示所有使用者列表,供管理員管理權限。"""
|
||||
# MySQL 不支援 nullslast(),改用 COALESCE 處理 NULL 值
|
||||
users = User.query.order_by(User.last_login.desc(), User.username).all()
|
||||
return render_template('user_management.html', users=users)
|
||||
|
||||
@admin_bp.route('/users/create', methods=['POST'])
|
||||
def create_user():
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
role = request.form.get('role')
|
||||
|
||||
if not all([username, password, role]):
|
||||
flash('所有欄位都是必填的!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('該使用者名稱已存在!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
new_user = User(
|
||||
username=username,
|
||||
password_hash=generate_password_hash(password),
|
||||
role=role
|
||||
)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
flash('新使用者已成功建立!', 'success')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
@admin_bp.route('/users/edit/<int:user_id>', methods=['POST'])
|
||||
def edit_user(user_id):
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_user_role(user_id):
|
||||
"""編輯使用者權限。僅允許修改角色。"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
new_role = request.form.get('role')
|
||||
new_password = request.form.get('password')
|
||||
|
||||
if new_role:
|
||||
# 防止 admin 修改自己的角色,導致失去管理權限
|
||||
if user.id == current_user.id and user.role == 'admin' and new_role != 'admin':
|
||||
flash('無法變更自己的管理員角色!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
user.role = new_role
|
||||
|
||||
if new_password:
|
||||
user.password_hash = generate_password_hash(new_password)
|
||||
if new_role not in ['viewer', 'editor', 'admin']:
|
||||
flash('無效的權限設定!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
# 防止管理員修改自己的角色導致失去管理權限
|
||||
if user.id == current_user.id and user.role == 'admin' and new_role != 'admin':
|
||||
flash('無法變更自己的管理員權限!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
old_role = user.role
|
||||
user.role = new_role
|
||||
db.session.commit()
|
||||
flash(f"使用者 '{user.username}' 的資料已更新。", 'success')
|
||||
|
||||
flash(f"使用者 '{user.username}' 的權限已從 '{old_role}' 更新為 '{new_role}'。", 'success')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
@admin_bp.route('/users/delete/<int:user_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
# 避免 admin 刪除自己
|
||||
"""刪除使用者帳號。"""
|
||||
# 避免管理員刪除自己
|
||||
if user_id == current_user.id:
|
||||
flash('無法刪除自己的帳號!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
username = user.username
|
||||
|
||||
# 檢查是否為最後一個管理員
|
||||
admin_count = User.query.filter_by(role='admin').count()
|
||||
if user.role == 'admin' and admin_count <= 1:
|
||||
flash('無法刪除最後一個管理員帳號!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f"使用者 '{user.username}' 已被刪除。", 'success')
|
||||
flash(f"使用者 '{username}' 已被刪除。", 'success')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
@admin_bp.route('/users/set-admin', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def set_admin():
|
||||
"""設定特定AD帳號為管理員權限。"""
|
||||
username = request.form.get('username', '').strip()
|
||||
|
||||
if not username:
|
||||
flash('請輸入有效的AD帳號!', 'danger')
|
||||
return redirect(url_for('admin.user_list'))
|
||||
|
||||
# 查找或建立使用者
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if not user:
|
||||
# 建立新的使用者記錄
|
||||
user = User(
|
||||
username=username,
|
||||
password_hash='ldap_authenticated', # LDAP使用者不需要本地密碼
|
||||
role='admin'
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash(f"已為 AD 帳號 '{username}' 建立管理員權限。", 'success')
|
||||
else:
|
||||
# 更新現有使用者權限
|
||||
old_role = user.role
|
||||
user.role = 'admin'
|
||||
db.session.commit()
|
||||
flash(f"已將 '{username}' 的權限從 '{old_role}' 更新為 'admin'。", 'success')
|
||||
|
||||
return redirect(url_for('admin.user_list'))
|
80
routes/api.py
Normal file
80
routes/api.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_login import login_required
|
||||
from ldap_utils import search_ldap_principals, search_ldap_groups
|
||||
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
@api_bp.route('/ldap-search', methods=['GET'])
|
||||
@login_required
|
||||
def ldap_search():
|
||||
"""
|
||||
API endpoint for searching LDAP principals.
|
||||
Returns JSON data for Tom Select dropdown.
|
||||
"""
|
||||
search_term = request.args.get('q', '').strip()
|
||||
|
||||
print(f"[DEBUG] LDAP API 搜尋請求: '{search_term}'")
|
||||
|
||||
if not search_term or len(search_term) < 2:
|
||||
print(f"[DEBUG] 搜尋詞太短,返回空結果")
|
||||
return jsonify([])
|
||||
|
||||
try:
|
||||
print(f"[DEBUG] 開始 LDAP 搜尋: '{search_term}'")
|
||||
|
||||
# 搜尋使用者
|
||||
user_results = search_ldap_principals(search_term, limit=15)
|
||||
print(f"[DEBUG] LDAP 使用者搜尋結果數量: {len(user_results)}")
|
||||
|
||||
# 搜尋群組
|
||||
group_results = search_ldap_groups(search_term, limit=5)
|
||||
print(f"[DEBUG] LDAP 群組搜尋結果數量: {len(group_results)}")
|
||||
|
||||
# Format results for Tom Select
|
||||
formatted_results = []
|
||||
|
||||
# 加入使用者結果
|
||||
for result in user_results:
|
||||
formatted_result = {
|
||||
'value': result['email'],
|
||||
'text': f"👤 {result['name']} ({result['email']})",
|
||||
'type': 'user'
|
||||
}
|
||||
formatted_results.append(formatted_result)
|
||||
print(f"[DEBUG] 格式化使用者結果: {result['name']} - {result['email']}")
|
||||
|
||||
# 加入群組結果
|
||||
for result in group_results:
|
||||
# 對群組,使用群組名稱作為 value,在發送郵件時再展開成員
|
||||
formatted_result = {
|
||||
'value': f"group:{result['name']}", # 特殊前綴表示這是群組
|
||||
'text': f"👥 {result['name']} ({result['member_count']} 成員)",
|
||||
'type': 'group'
|
||||
}
|
||||
formatted_results.append(formatted_result)
|
||||
print(f"[DEBUG] 格式化群組結果: {result['name']} - {result['member_count']} 成員")
|
||||
|
||||
print(f"[DEBUG] 返回 {len(formatted_results)} 個搜尋結果")
|
||||
return jsonify(formatted_results)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DEBUG] LDAP search API error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': 'Search failed'}), 500
|
||||
|
||||
@api_bp.route('/debug-form', methods=['POST'])
|
||||
@login_required
|
||||
def debug_form():
|
||||
"""接收前端表單除錯資訊"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
print(f"[FRONTEND DEBUG] 收到前端除錯資料:")
|
||||
print(f"[FRONTEND DEBUG] selectedValues: {data.get('selectedValues')}")
|
||||
print(f"[FRONTEND DEBUG] recipientValue: {data.get('recipientValue')}")
|
||||
print(f"[FRONTEND DEBUG] hiddenFieldValue: {data.get('hiddenFieldValue')}")
|
||||
print(f"[FRONTEND DEBUG] tomSelectValue: {data.get('tomSelectValue')}")
|
||||
return jsonify({'status': 'received'})
|
||||
except Exception as e:
|
||||
print(f"[FRONTEND DEBUG] 錯誤: {e}")
|
||||
return jsonify({'error': str(e)}), 400
|
103
routes/auth.py
103
routes/auth.py
@@ -1,11 +1,9 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from werkzeug.security import check_password_hash
|
||||
from ldap_utils import authenticate_ldap_user
|
||||
from models import User, db
|
||||
from datetime import datetime
|
||||
from werkzeug.security import check_password_hash
|
||||
from ldap_utils import authenticate_ldap_user, generate_password_hash
|
||||
import logging
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@@ -15,35 +13,82 @@ def login():
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
username = request.form['username'].strip()
|
||||
password = request.form['password']
|
||||
|
||||
# Step 1: Authenticate against LDAP
|
||||
user_info = authenticate_ldap_user(username, password)
|
||||
|
||||
if user_info:
|
||||
# Step 2: User authenticated successfully, find or create local user
|
||||
local_user = User.query.filter_by(username=user_info['username']).first()
|
||||
|
||||
if not local_user:
|
||||
# Create a new user in the local database
|
||||
local_user = User(
|
||||
username=user_info['username'],
|
||||
# password_hash is no longer needed for login, can be empty or random
|
||||
password_hash='ldap_authenticated',
|
||||
role='viewer' # Default role for new users
|
||||
)
|
||||
db.session.add(local_user)
|
||||
# 記錄登入嘗試
|
||||
print(f"[DEBUG] 登入嘗試 - 帳號: {username}")
|
||||
current_app.logger.info(f"Login attempt for user: {username}")
|
||||
|
||||
# 驗證帳號格式
|
||||
if '@' not in username:
|
||||
print(f"[DEBUG] 帳號格式錯誤 - 缺少 @ 符號: {username}")
|
||||
current_app.logger.warning(f"Invalid username format (missing @): {username}")
|
||||
flash('請使用完整的 AD 帳號格式 (包含 @domain)', 'warning')
|
||||
return render_template('login.html')
|
||||
|
||||
try:
|
||||
# Step 1: Authenticate against LDAP
|
||||
print(f"[DEBUG] 準備進行 LDAP 驗證: {username}")
|
||||
current_app.logger.info(f"Attempting LDAP authentication for: {username}")
|
||||
|
||||
# Update last_login time
|
||||
local_user.last_login = datetime.now()
|
||||
db.session.commit()
|
||||
user_info = authenticate_ldap_user(username, password)
|
||||
print(f"[DEBUG] LDAP 驗證結果: {user_info}")
|
||||
|
||||
# Step 3: Log in the user with Flask-Login
|
||||
login_user(local_user)
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
else:
|
||||
flash('帳號或密碼錯誤,請重新輸入', 'danger')
|
||||
if user_info:
|
||||
print(f"[DEBUG] LDAP 驗證成功: {username}")
|
||||
current_app.logger.info(f"LDAP authentication successful for: {username}")
|
||||
|
||||
# Step 2: User authenticated successfully, find or create local user
|
||||
local_user = User.query.filter_by(username=user_info['username']).first()
|
||||
|
||||
if not local_user:
|
||||
print(f"[DEBUG] 建立新的本地使用者帳號: {user_info['username']}")
|
||||
current_app.logger.info(f"Creating new local user account: {user_info['username']}")
|
||||
|
||||
# Create a new user in the local database
|
||||
# 預設權限為 viewer,特殊帳號設為 admin
|
||||
default_role = 'viewer' # 預設權限
|
||||
|
||||
# 特殊處理:設定特定帳號為管理員權限
|
||||
if user_info['username'].lower() == 'ymirliu@panjit.com.tw':
|
||||
default_role = 'admin'
|
||||
print(f"[DEBUG] 特殊帳號:{user_info['username']} 設定為管理員權限")
|
||||
|
||||
local_user = User(
|
||||
username=user_info['username'],
|
||||
# password_hash is no longer needed for login, can be empty or random
|
||||
password_hash='ldap_authenticated',
|
||||
role=default_role
|
||||
)
|
||||
db.session.add(local_user)
|
||||
print(f"[DEBUG] 新使用者建立完成,權限: {default_role}")
|
||||
current_app.logger.info(f"New user created with role: {default_role}")
|
||||
else:
|
||||
print(f"[DEBUG] 找到現有使用者: {user_info['username']}")
|
||||
current_app.logger.info(f"Existing user found: {user_info['username']}")
|
||||
|
||||
# Update last_login time
|
||||
local_user.last_login = datetime.now()
|
||||
db.session.commit()
|
||||
|
||||
# Step 3: Log in the user with Flask-Login
|
||||
login_user(local_user)
|
||||
print(f"[DEBUG] 使用者登入成功: {username}")
|
||||
current_app.logger.info(f"User successfully logged in: {username}")
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
else:
|
||||
# LDAP 驗證失敗
|
||||
print(f"[DEBUG] LDAP 驗證失敗: {username}")
|
||||
current_app.logger.warning(f"LDAP authentication failed for: {username}")
|
||||
flash('AD帳號或密碼錯誤,請檢查後重新輸入', 'danger')
|
||||
|
||||
except Exception as e:
|
||||
# 系統錯誤
|
||||
print(f"[DEBUG] 系統錯誤: {str(e)}")
|
||||
current_app.logger.error(f"Login system error for user {username}: {str(e)}")
|
||||
flash('系統登入發生錯誤,請稍後再試或聯繫系統管理員', 'danger')
|
||||
|
||||
return render_template('login.html')
|
||||
|
||||
|
@@ -3,7 +3,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime, timedelta
|
||||
from models import TempSpec, db, Upload, SpecHistory
|
||||
from utils import editor_or_admin_required, add_history_log, admin_required, send_email
|
||||
from utils import editor_or_admin_required, add_history_log, admin_required, send_email, process_recipients
|
||||
from ldap_utils import get_ldap_group_members
|
||||
import os
|
||||
import shutil
|
||||
@@ -269,30 +269,29 @@ def activate_spec(spec_id):
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已生效!", 'success')
|
||||
|
||||
# --- Start of Email Notification Example ---
|
||||
# Get recipient list from a predefined LDAP group
|
||||
# NOTE: 'TempSpec_Approvers' is an example group name. Replace with the actual group name.
|
||||
recipients = get_ldap_group_members('TempSpec_Approvers')
|
||||
if recipients:
|
||||
subject = f"[暫規通知] 規範 '{spec.spec_code}' 已正式生效"
|
||||
# Using f-strings and triple quotes for a readable HTML body
|
||||
body = f"""
|
||||
<html>
|
||||
<body>
|
||||
<p>您好,</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 已由管理員啟用,並正式生效。</p>
|
||||
<p>詳細資訊請登入系統查看。</p>
|
||||
<p>生效日期: {spec.start_date.strftime('%Y-%m-%d')}<br>
|
||||
結束日期: {spec.end_date.strftime('%Y-%m-%d')}</p>
|
||||
<p>此為系統自動發送的通知郵件,請勿直接回覆。</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
send_email(recipients, subject, body)
|
||||
else:
|
||||
# Log a warning if no recipients were found, but don't block the main process
|
||||
current_app.logger.warning(f"Could not find recipients in LDAP group 'TempSpec_Approvers' for spec {spec.id}.")
|
||||
# --- End of Email Notification Example ---
|
||||
# --- Start of Dynamic Email Notification ---
|
||||
recipients_str = request.form.get('recipients')
|
||||
if recipients_str:
|
||||
recipients = process_recipients(recipients_str)
|
||||
if recipients:
|
||||
subject = f"[暫規通知] 規範 '{spec.spec_code}' 已正式生效"
|
||||
# Using f-strings and triple quotes for a readable HTML body
|
||||
body = f"""
|
||||
<html>
|
||||
<body>
|
||||
<p>您好,</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 已由管理員啟用,並正式生效。</p>
|
||||
<p>詳細資訊請登入系統查看。</p>
|
||||
<p>生效日期: {spec.start_date.strftime('%Y-%m-%d')}<br>
|
||||
結束日期: {spec.end_date.strftime('%Y-%m-%d')}</p>
|
||||
<p>申請人: {spec.applicant}</p>
|
||||
<p>此為系統自動發送的通知郵件,請勿直接回覆。</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
send_email(recipients, subject, body)
|
||||
current_app.logger.info(f"Sent activation notification for spec {spec.spec_code} to {len(recipients)} recipients.")
|
||||
# --- End of Dynamic Email Notification ---
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
return render_template('activate_spec.html', spec=spec)
|
||||
@@ -311,6 +310,30 @@ def terminate_spec(spec_id):
|
||||
spec.termination_reason = reason
|
||||
spec.end_date = datetime.today().date()
|
||||
add_history_log(spec.id, '終止', f"原因: {reason}")
|
||||
|
||||
# --- Start of Dynamic Email Notification ---
|
||||
recipients_str = request.form.get('recipients')
|
||||
if recipients_str:
|
||||
recipients = process_recipients(recipients_str)
|
||||
if recipients:
|
||||
subject = f"[暫規通知] 規範 '{spec.spec_code}' 已提早終止"
|
||||
body = f"""
|
||||
<html>
|
||||
<body>
|
||||
<p>您好,</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 已被提早終止。</p>
|
||||
<p>終止日期: <b>{spec.end_date.strftime('%Y-%m-%d')}</b></p>
|
||||
<p>申請人: {spec.applicant}</p>
|
||||
<p>終止原因: {reason}</p>
|
||||
<p>詳細資訊請登入系統查看。</p>
|
||||
<p>此為系統自動發送的通知郵件,請勿直接回覆。</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
send_email(recipients, subject, body)
|
||||
current_app.logger.info(f"Sent termination notification for spec {spec.spec_code} to {len(recipients)} recipients.")
|
||||
# --- End of Dynamic Email Notification ---
|
||||
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已被提早終止。", 'warning')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
@@ -384,6 +407,28 @@ def extend_spec(spec_id):
|
||||
details += f",並上傳新檔案 '{new_upload.filename}'"
|
||||
add_history_log(spec.id, '展延', details)
|
||||
|
||||
# --- Start of Dynamic Email Notification ---
|
||||
recipients_str = request.form.get('recipients')
|
||||
if recipients_str:
|
||||
recipients = process_recipients(recipients_str)
|
||||
if recipients:
|
||||
subject = f"[暫規通知] 規範 '{spec.spec_code}' 已展延"
|
||||
body = f"""
|
||||
<html>
|
||||
<body>
|
||||
<p>您好,</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 已成功展延。</p>
|
||||
<p>新的結束日期為: <b>{spec.end_date.strftime('%Y-%m-%d')}</b></p>
|
||||
<p>申請人: {spec.applicant}</p>
|
||||
<p>詳細資訊請登入系統查看。</p>
|
||||
<p>此為系統自動發送的通知郵件,請勿直接回覆。</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
send_email(recipients, subject, body)
|
||||
current_app.logger.info(f"Sent extension notification for spec {spec.spec_code} to {len(recipients)} recipients.")
|
||||
# --- End of Dynamic Email Notification ---
|
||||
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已成功展延!", 'success')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
368
start-linux.sh
Normal file
368
start-linux.sh
Normal file
@@ -0,0 +1,368 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ========================================
|
||||
# 暫時規範管理系統 V3 - Linux 啟動腳本
|
||||
# ========================================
|
||||
|
||||
set -e # 遇到錯誤立即退出
|
||||
|
||||
echo
|
||||
echo "====================================="
|
||||
echo "暫時規範管理系統 V3 - Linux 啟動"
|
||||
echo "====================================="
|
||||
echo
|
||||
|
||||
# 顏色定義
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 檢查 Python 是否已安裝
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo -e "${RED}[錯誤]${NC} Python3 未安裝"
|
||||
echo "請安裝 Python 3.8+ 後重新執行此腳本"
|
||||
echo
|
||||
echo "Ubuntu/Debian: sudo apt update && sudo apt install python3 python3-pip python3-venv"
|
||||
echo "CentOS/RHEL: sudo yum install python3 python3-pip"
|
||||
echo "或使用包管理器安裝"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 檢查 Docker 是否已安裝
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${YELLOW}[警告]${NC} Docker 未安裝或未運行"
|
||||
echo "如需使用 Docker 模式,請先安裝 Docker"
|
||||
echo
|
||||
USE_DOCKER=false
|
||||
else
|
||||
# 檢查 Docker 是否運行
|
||||
if ! docker info &> /dev/null; then
|
||||
echo -e "${YELLOW}[警告]${NC} Docker 未運行"
|
||||
echo "請啟動 Docker 服務: sudo systemctl start docker"
|
||||
USE_DOCKER=false
|
||||
else
|
||||
USE_DOCKER=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# 檢查 docker-compose 是否存在
|
||||
if [[ "$USE_DOCKER" == true ]] && ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
echo -e "${YELLOW}[警告]${NC} docker-compose 未安裝"
|
||||
USE_DOCKER=false
|
||||
fi
|
||||
|
||||
# 檢查 .env 檔案
|
||||
if [[ ! -f ".env" ]]; then
|
||||
echo -e "${BLUE}[提示]${NC} 未找到 .env 檔案,正在複製範例檔案..."
|
||||
cp ".env.example" ".env"
|
||||
echo
|
||||
echo -e "${YELLOW}[重要]${NC} 請編輯 .env 檔案並設定正確的參數值:"
|
||||
echo "- 資料庫連線資訊"
|
||||
echo "- LDAP/AD 伺服器設定"
|
||||
echo "- SMTP 郵件伺服器設定"
|
||||
echo
|
||||
read -p "是否已完成 .env 檔案設定?(y/N): " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "請先設定 .env 檔案後再執行此腳本"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 提供啟動選項
|
||||
echo "請選擇啟動模式:"
|
||||
echo
|
||||
if [[ "$USE_DOCKER" == true ]]; then
|
||||
echo "[1] Docker 容器化部署 (推薦)"
|
||||
echo "[2] 本地 Python 直接執行"
|
||||
echo "[3] 僅啟動外部服務 (MySQL + ONLYOFFICE)"
|
||||
echo "[4] 生產環境部署 (含 Nginx)"
|
||||
echo "[5] 系統服務安裝 (systemd)"
|
||||
else
|
||||
echo "[2] 本地 Python 直接執行"
|
||||
echo "[5] 系統服務安裝 (systemd)"
|
||||
echo
|
||||
echo -e "${YELLOW}注意:Docker 相關選項不可用${NC}"
|
||||
fi
|
||||
echo
|
||||
|
||||
read -p "請輸入選項: " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
if [[ "$USE_DOCKER" != true ]]; then
|
||||
echo -e "${RED}[錯誤]${NC} Docker 不可用,請選擇其他選項"
|
||||
exit 1
|
||||
fi
|
||||
start_docker
|
||||
;;
|
||||
2)
|
||||
start_manual
|
||||
;;
|
||||
3)
|
||||
if [[ "$USE_DOCKER" != true ]]; then
|
||||
echo -e "${RED}[錯誤]${NC} Docker 不可用,請選擇其他選項"
|
||||
exit 1
|
||||
fi
|
||||
start_services_only
|
||||
;;
|
||||
4)
|
||||
if [[ "$USE_DOCKER" != true ]]; then
|
||||
echo -e "${RED}[錯誤]${NC} Docker 不可用,請選擇其他選項"
|
||||
exit 1
|
||||
fi
|
||||
start_production
|
||||
;;
|
||||
5)
|
||||
install_systemd_service
|
||||
;;
|
||||
*)
|
||||
if [[ "$USE_DOCKER" == true ]]; then
|
||||
start_docker
|
||||
else
|
||||
start_manual
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
function start_docker() {
|
||||
echo
|
||||
echo "================================="
|
||||
echo "使用 Docker Compose 啟動系統..."
|
||||
echo "================================="
|
||||
echo
|
||||
|
||||
# 檢查 compose 命令
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
# 停止並清理舊的容器
|
||||
echo -e "${BLUE}[步驟 1]${NC} 清理舊容器..."
|
||||
$COMPOSE_CMD down
|
||||
|
||||
# 重新構建並啟動服務
|
||||
echo -e "${BLUE}[步驟 2]${NC} 構建並啟動服務..."
|
||||
$COMPOSE_CMD up --build -d
|
||||
|
||||
# 等待服務啟動
|
||||
echo -e "${BLUE}[步驟 3]${NC} 等待服務啟動..."
|
||||
sleep 30
|
||||
|
||||
# 初始化資料庫
|
||||
echo -e "${BLUE}[步驟 4]${NC} 初始化資料庫..."
|
||||
$COMPOSE_CMD exec -T app python init_db.py --auto-yes || true
|
||||
|
||||
# 顯示狀態
|
||||
echo -e "${BLUE}[步驟 5]${NC} 檢查服務狀態..."
|
||||
$COMPOSE_CMD ps
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}============================"
|
||||
echo "系統啟動完成!"
|
||||
echo "============================${NC}"
|
||||
echo
|
||||
echo "服務訪問地址:"
|
||||
echo "- 主系統:http://localhost:5000"
|
||||
echo "- ONLYOFFICE:http://localhost:8080"
|
||||
echo "- MySQL:localhost:3306"
|
||||
echo
|
||||
echo "管理命令:"
|
||||
echo "- 查看日誌:$COMPOSE_CMD logs -f"
|
||||
echo "- 停止服務:$COMPOSE_CMD down"
|
||||
echo "- 重啟服務:$COMPOSE_CMD restart"
|
||||
echo
|
||||
}
|
||||
|
||||
function start_services_only() {
|
||||
echo
|
||||
echo "==============================="
|
||||
echo "僅啟動外部服務..."
|
||||
echo "==============================="
|
||||
echo
|
||||
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
$COMPOSE_CMD up -d mysql onlyoffice
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}外部服務已啟動!${NC}"
|
||||
echo "- MySQL:localhost:3306"
|
||||
echo "- ONLYOFFICE:http://localhost:8080"
|
||||
echo
|
||||
echo "請使用以下命令啟動 Flask 應用:"
|
||||
echo "python3 app.py"
|
||||
echo
|
||||
}
|
||||
|
||||
function start_production() {
|
||||
echo
|
||||
echo "==============================="
|
||||
echo "生產環境部署 (含 Nginx)..."
|
||||
echo "==============================="
|
||||
echo
|
||||
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
COMPOSE_CMD="docker compose"
|
||||
fi
|
||||
|
||||
$COMPOSE_CMD --profile production up --build -d
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}生產環境已啟動!${NC}"
|
||||
echo "- 系統入口:http://localhost (透過 Nginx)"
|
||||
echo "- 直接訪問:http://localhost:5000"
|
||||
echo
|
||||
}
|
||||
|
||||
function start_manual() {
|
||||
echo
|
||||
echo "==============================="
|
||||
echo "本地 Python 環境啟動..."
|
||||
echo "==============================="
|
||||
echo
|
||||
|
||||
# 檢查虛擬環境
|
||||
if [[ -d "venv" ]]; then
|
||||
echo -e "${BLUE}[步驟 1]${NC} 啟用虛擬環境..."
|
||||
source venv/bin/activate
|
||||
else
|
||||
echo -e "${YELLOW}[警告]${NC} 未找到虛擬環境,建議先建立:"
|
||||
echo "python3 -m venv venv"
|
||||
echo "source venv/bin/activate"
|
||||
echo "pip install -r requirements.txt"
|
||||
echo
|
||||
read -p "是否立即建立虛擬環境?(y/N): " create_venv
|
||||
if [[ "$create_venv" =~ ^[Yy]$ ]]; then
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
echo -e "${GREEN}虛擬環境已建立並啟用${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 安裝依賴
|
||||
echo -e "${BLUE}[步驟 2]${NC} 檢查並安裝依賴..."
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 檢查外部服務
|
||||
echo -e "${BLUE}[步驟 3]${NC} 檢查外部服務..."
|
||||
echo
|
||||
echo -e "${YELLOW}[重要提醒]${NC} 請確保以下服務已啟動:"
|
||||
echo
|
||||
echo "1. MySQL 資料庫伺服器"
|
||||
echo " - 可使用: docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0"
|
||||
echo
|
||||
echo "2. ONLYOFFICE Document Server"
|
||||
echo " - 可使用: docker run -d -p 8080:80 onlyoffice/documentserver"
|
||||
echo
|
||||
echo "3. LDAP/AD 伺服器 (如果適用)"
|
||||
echo "4. SMTP 郵件伺服器 (如果適用)"
|
||||
echo
|
||||
|
||||
read -p "外部服務是否已準備就緒?(y/N): " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "請先啟動必要的外部服務"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 初始化資料庫
|
||||
echo -e "${BLUE}[步驟 4]${NC} 初始化資料庫..."
|
||||
python3 init_db.py || python init_db.py
|
||||
|
||||
# 啟動應用
|
||||
echo -e "${BLUE}[步驟 5]${NC} 啟動 Flask 應用..."
|
||||
echo
|
||||
echo "啟動中... (Ctrl+C 停止)"
|
||||
echo "訪問地址: http://localhost:5000"
|
||||
echo
|
||||
|
||||
# 根據系統環境選擇啟動方式
|
||||
if command -v gunicorn &> /dev/null; then
|
||||
echo "使用 Gunicorn 啟動生產模式..."
|
||||
gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
else
|
||||
echo "使用 Flask 開發伺服器啟動..."
|
||||
python3 app.py || python app.py
|
||||
fi
|
||||
}
|
||||
|
||||
function install_systemd_service() {
|
||||
echo
|
||||
echo "==============================="
|
||||
echo "安裝為系統服務 (systemd)..."
|
||||
echo "==============================="
|
||||
echo
|
||||
|
||||
# 檢查是否為 root 用戶
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}[錯誤]${NC} 需要 root 權限安裝系統服務"
|
||||
echo "請使用 sudo 執行此腳本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 獲取當前目錄和用戶
|
||||
CURRENT_DIR=$(pwd)
|
||||
CURRENT_USER=${SUDO_USER:-$USER}
|
||||
|
||||
echo "安裝目錄: $CURRENT_DIR"
|
||||
echo "執行用戶: $CURRENT_USER"
|
||||
echo
|
||||
|
||||
# 建立服務檔案
|
||||
cat > /etc/systemd/system/tempspec.service << EOF
|
||||
[Unit]
|
||||
Description=Temp Spec System V3
|
||||
After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$CURRENT_USER
|
||||
Group=$CURRENT_USER
|
||||
WorkingDirectory=$CURRENT_DIR
|
||||
Environment=PATH=$CURRENT_DIR/venv/bin
|
||||
ExecStart=$CURRENT_DIR/venv/bin/gunicorn -w 4 -b 0.0.0.0:5000 app:app
|
||||
ExecReload=/bin/kill -HUP \$MAINPID
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# 重新載入 systemd
|
||||
systemctl daemon-reload
|
||||
|
||||
# 啟用服務
|
||||
systemctl enable tempspec.service
|
||||
|
||||
echo -e "${GREEN}系統服務安裝完成!${NC}"
|
||||
echo
|
||||
echo "管理命令:"
|
||||
echo "- 啟動服務: sudo systemctl start tempspec"
|
||||
echo "- 停止服務: sudo systemctl stop tempspec"
|
||||
echo "- 重啟服務: sudo systemctl restart tempspec"
|
||||
echo "- 查看狀態: sudo systemctl status tempspec"
|
||||
echo "- 查看日誌: sudo journalctl -u tempspec -f"
|
||||
echo
|
||||
|
||||
read -p "是否立即啟動服務?(y/N): " start_now
|
||||
if [[ "$start_now" =~ ^[Yy]$ ]]; then
|
||||
systemctl start tempspec
|
||||
systemctl status tempspec --no-pager
|
||||
fi
|
||||
}
|
||||
|
||||
# 捕捉 Ctrl+C
|
||||
trap 'echo -e "\n${YELLOW}操作已取消${NC}"; exit 130' INT
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}腳本執行完成!${NC}"
|
206
start-windows.bat
Normal file
206
start-windows.bat
Normal file
@@ -0,0 +1,206 @@
|
||||
@echo off
|
||||
REM ========================================
|
||||
REM 暫時規範管理系統 V3 - Windows 啟動腳本
|
||||
REM ========================================
|
||||
|
||||
echo.
|
||||
echo =====================================
|
||||
echo 暫時規範管理系統 V3 - Windows 啟動
|
||||
echo =====================================
|
||||
echo.
|
||||
|
||||
REM 檢查 Python 是否已安裝
|
||||
python --version >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [錯誤] Python 未安裝或未加入 PATH
|
||||
echo 請安裝 Python 3.8+ 並重新執行此腳本
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 檢查 Docker 是否已安裝並運行
|
||||
docker --version >nul 2>&1
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo [警告] Docker 未安裝或未運行
|
||||
echo 如需使用 Docker 模式,請先安裝並啟動 Docker Desktop
|
||||
echo.
|
||||
goto MANUAL_START
|
||||
)
|
||||
|
||||
REM 檢查是否存在 docker-compose.yml
|
||||
if not exist "docker-compose.yml" (
|
||||
echo [警告] 找不到 docker-compose.yml 檔案
|
||||
goto MANUAL_START
|
||||
)
|
||||
|
||||
REM 檢查 .env 檔案
|
||||
if not exist ".env" (
|
||||
echo [提示] 未找到 .env 檔案,正在複製範例檔案...
|
||||
copy ".env.example" ".env"
|
||||
echo.
|
||||
echo [重要] 請編輯 .env 檔案並設定正確的參數值:
|
||||
echo - 資料庫連線資訊
|
||||
echo - LDAP/AD 伺服器設定
|
||||
echo - SMTP 郵件伺服器設定
|
||||
echo.
|
||||
set /p confirm="是否已完成 .env 檔案設定?(y/N): "
|
||||
if /i not "%confirm%"=="y" (
|
||||
echo 請先設定 .env 檔案後再執行此腳本
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
REM 提供啟動選項
|
||||
echo 請選擇啟動模式:
|
||||
echo.
|
||||
echo [1] Docker 容器化部署 (推薦)
|
||||
echo [2] 本地 Python 直接執行
|
||||
echo [3] 僅啟動外部服務 (MySQL + ONLYOFFICE)
|
||||
echo [4] 生產環境部署 (含 Nginx)
|
||||
echo.
|
||||
set /p choice="請輸入選項 [1-4]: "
|
||||
|
||||
if "%choice%"=="1" goto DOCKER_START
|
||||
if "%choice%"=="2" goto MANUAL_START
|
||||
if "%choice%"=="3" goto SERVICES_ONLY
|
||||
if "%choice%"=="4" goto PRODUCTION_START
|
||||
goto DOCKER_START
|
||||
|
||||
:DOCKER_START
|
||||
echo.
|
||||
echo =================================
|
||||
echo 使用 Docker Compose 啟動系統...
|
||||
echo =================================
|
||||
echo.
|
||||
|
||||
REM 停止並清理舊的容器
|
||||
echo [步驟 1] 清理舊容器...
|
||||
docker-compose down
|
||||
|
||||
REM 重新構建並啟動服務
|
||||
echo [步驟 2] 構建並啟動服務...
|
||||
docker-compose up --build -d
|
||||
|
||||
REM 等待服務啟動
|
||||
echo [步驟 3] 等待服務啟動...
|
||||
timeout /t 30 /nobreak
|
||||
|
||||
REM 初始化資料庫
|
||||
echo [步驟 4] 初始化資料庫...
|
||||
docker-compose exec app python init_db.py --auto-yes
|
||||
|
||||
REM 顯示狀態
|
||||
echo [步驟 5] 檢查服務狀態...
|
||||
docker-compose ps
|
||||
|
||||
echo.
|
||||
echo ============================
|
||||
echo 系統啟動完成!
|
||||
echo ============================
|
||||
echo.
|
||||
echo 服務訪問地址:
|
||||
echo - 主系統:http://localhost:5000
|
||||
echo - ONLYOFFICE:http://localhost:8080
|
||||
echo - MySQL:localhost:3306
|
||||
echo.
|
||||
echo 查看日誌:docker-compose logs -f
|
||||
echo 停止服務:docker-compose down
|
||||
echo.
|
||||
goto END
|
||||
|
||||
:SERVICES_ONLY
|
||||
echo.
|
||||
echo ===============================
|
||||
echo 僅啟動外部服務...
|
||||
echo ===============================
|
||||
echo.
|
||||
|
||||
docker-compose up -d mysql onlyoffice
|
||||
|
||||
echo.
|
||||
echo 外部服務已啟動!
|
||||
echo - MySQL:localhost:3306
|
||||
echo - ONLYOFFICE:http://localhost:8080
|
||||
echo.
|
||||
echo 請使用以下命令啟動 Flask 應用:
|
||||
echo python app.py
|
||||
echo.
|
||||
goto END
|
||||
|
||||
:PRODUCTION_START
|
||||
echo.
|
||||
echo ===============================
|
||||
echo 生產環境部署 (含 Nginx)...
|
||||
echo ===============================
|
||||
echo.
|
||||
|
||||
docker-compose --profile production up --build -d
|
||||
|
||||
echo.
|
||||
echo 生產環境已啟動!
|
||||
echo - 系統入口:http://localhost (透過 Nginx)
|
||||
echo - 直接訪問:http://localhost:5000
|
||||
echo.
|
||||
goto END
|
||||
|
||||
:MANUAL_START
|
||||
echo.
|
||||
echo ===============================
|
||||
echo 本地 Python 環境啟動...
|
||||
echo ===============================
|
||||
echo.
|
||||
|
||||
REM 檢查虛擬環境
|
||||
if exist "venv\Scripts\activate.bat" (
|
||||
echo [步驟 1] 啟用虛擬環境...
|
||||
call venv\Scripts\activate.bat
|
||||
) else (
|
||||
echo [警告] 未找到虛擬環境,建議先建立:
|
||||
echo python -m venv venv
|
||||
echo venv\Scripts\activate
|
||||
echo pip install -r requirements.txt
|
||||
echo.
|
||||
)
|
||||
|
||||
REM 安裝依賴
|
||||
echo [步驟 2] 檢查並安裝依賴...
|
||||
pip install -r requirements.txt
|
||||
|
||||
REM 檢查外部服務
|
||||
echo [步驟 3] 檢查外部服務...
|
||||
echo.
|
||||
echo [重要提醒] 請確保以下服務已啟動:
|
||||
echo.
|
||||
echo 1. MySQL 資料庫伺服器
|
||||
echo - 可使用: docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0
|
||||
echo.
|
||||
echo 2. ONLYOFFICE Document Server
|
||||
echo - 可使用: docker run -d -p 8080:80 onlyoffice/documentserver
|
||||
echo.
|
||||
echo 3. LDAP/AD 伺服器 (如果適用)
|
||||
echo 4. SMTP 郵件伺服器 (如果適用)
|
||||
echo.
|
||||
|
||||
set /p confirm="外部服務是否已準備就緒?(y/N): "
|
||||
if /i not "%confirm%"=="y" (
|
||||
echo 請先啟動必要的外部服務
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM 初始化資料庫
|
||||
echo [步驟 4] 初始化資料庫...
|
||||
python init_db.py
|
||||
|
||||
REM 啟動應用
|
||||
echo [步驟 5] 啟動 Flask 應用...
|
||||
echo.
|
||||
echo 啟動中... (Ctrl+C 停止)
|
||||
python app.py
|
||||
|
||||
goto END
|
||||
|
||||
:END
|
||||
echo.
|
||||
pause
|
BIN
static/generated/PE1140801.docx
Normal file
BIN
static/generated/PE1140801.docx
Normal file
Binary file not shown.
55
tasks.py
Normal file
55
tasks.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from datetime import date, timedelta
|
||||
from models import TempSpec
|
||||
from utils import send_email
|
||||
from ldap_utils import get_ldap_group_members
|
||||
|
||||
def check_expiring_specs(app):
|
||||
"""
|
||||
每日執行的排程任務:檢查即將到期的暫規並發送提醒郵件。
|
||||
"""
|
||||
with app.app_context():
|
||||
print("Running scheduled task: Checking for expiring specs...")
|
||||
today = date.today()
|
||||
seven_days_later = today + timedelta(days=7)
|
||||
three_days_later = today + timedelta(days=3)
|
||||
|
||||
# 找出 7 天後 和 3 天後到期的暫規
|
||||
expiring_soon = TempSpec.query.filter(
|
||||
TempSpec.status == 'active',
|
||||
TempSpec.end_date.in_([seven_days_later, three_days_later])
|
||||
).all()
|
||||
|
||||
if not expiring_soon:
|
||||
print("No specs expiring in 3 or 7 days.")
|
||||
return
|
||||
|
||||
# 定義預設的通知對象
|
||||
# 可以根據需要修改群組名稱
|
||||
default_recipients = get_ldap_group_members('TempSpec_Admins')
|
||||
if not default_recipients:
|
||||
print("Warning: Could not find default recipients in AD group 'TempSpec_Admins'.")
|
||||
# 如果找不到預設群組,可以設定備用收件人
|
||||
default_recipients = ['admin@example.com'] # 請根據實際情況修改
|
||||
|
||||
for spec in expiring_soon:
|
||||
remaining_days = (spec.end_date - today).days
|
||||
|
||||
# 組合通知郵件
|
||||
subject = f"[暫規到期提醒] 規範 '{spec.spec_code}' 將於 {remaining_days} 天後到期"
|
||||
body = f"""
|
||||
<html>
|
||||
<body>
|
||||
<p>您好,</p>
|
||||
<p>此為自動提醒郵件。</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 即將到期。</p>
|
||||
<p><b>結束日期: {spec.end_date.strftime('%Y-%m-%d')} (剩餘 {remaining_days} 天)</b></p>
|
||||
<p>申請人: {spec.applicant}</p>
|
||||
<p>請及時處理,如需展延請登入系統操作。</p>
|
||||
<p>此為系統自動發送的通知郵件,請勿直接回覆。</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# 發送郵件給預設群組
|
||||
send_email(default_recipients, subject, body)
|
||||
print(f"Sent expiry reminder for spec {spec.spec_code} to {len(default_recipients)} recipients.")
|
@@ -19,9 +19,133 @@
|
||||
<label for="signed_file" class="form-label"><strong>已簽核的 PDF 檔案</strong></label>
|
||||
<input class="form-control" type="file" id="signed_file" name="signed_file" accept=".pdf" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">上傳並啟用</button>
|
||||
|
||||
<!-- 郵件通知對象選擇 -->
|
||||
<div class="mb-3">
|
||||
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
|
||||
<select id="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
|
||||
</select>
|
||||
<input type="hidden" id="recipients-hidden" name="recipients" value="" />
|
||||
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-success" onclick="submitFormWithDebug()">上傳並啟用</button>
|
||||
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function updateHiddenField() {
|
||||
if (window.recipientTomSelect) {
|
||||
const selectedValues = window.recipientTomSelect.getValue();
|
||||
const hiddenField = document.getElementById('recipients-hidden');
|
||||
hiddenField.value = Array.isArray(selectedValues) ? selectedValues.join(',') : selectedValues;
|
||||
console.log('[DEBUG] 更新隱藏字段:', hiddenField.value);
|
||||
}
|
||||
}
|
||||
|
||||
function submitFormWithDebug() {
|
||||
// 確保隱藏字段有最新的值
|
||||
updateHiddenField();
|
||||
|
||||
const recipientSelect = document.getElementById('recipients');
|
||||
const hiddenField = document.getElementById('recipients-hidden');
|
||||
const selectedValues = Array.from(recipientSelect.selectedOptions).map(option => option.value);
|
||||
|
||||
console.log('[FORM DEBUG] 表單提交時選中的收件者:', selectedValues);
|
||||
console.log('[FORM DEBUG] Recipients input value:', recipientSelect.value);
|
||||
console.log('[FORM DEBUG] Hidden field value:', hiddenField.value);
|
||||
|
||||
// 確認 Tom Select 的值
|
||||
if (window.recipientTomSelect) {
|
||||
console.log('[FORM DEBUG] Tom Select getValue():', window.recipientTomSelect.getValue());
|
||||
}
|
||||
|
||||
// 發送除錯資訊到後端
|
||||
const debugData = {
|
||||
selectedValues: selectedValues,
|
||||
recipientValue: recipientSelect.value,
|
||||
hiddenFieldValue: hiddenField.value,
|
||||
tomSelectValue: window.recipientTomSelect ? window.recipientTomSelect.getValue() : null
|
||||
};
|
||||
|
||||
// 透過 fetch 發送除錯資訊
|
||||
fetch('/api/debug-form', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(debugData)
|
||||
}).then(() => {
|
||||
console.log('[FORM DEBUG] 除錯資訊已發送,準備提交表單...');
|
||||
// 延遲一點點後提交表單
|
||||
setTimeout(() => {
|
||||
document.querySelector('form').submit();
|
||||
}, 200);
|
||||
}).catch(error => {
|
||||
console.error('Debug sending failed:', error);
|
||||
// 即使除錯失敗也要提交表單
|
||||
document.querySelector('form').submit();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const recipientSelect = new TomSelect('#recipients', {
|
||||
valueField: 'value',
|
||||
labelField: 'text',
|
||||
searchField: 'text',
|
||||
placeholder: '請輸入姓名或 Email 來搜尋...',
|
||||
plugins: ['remove_button'],
|
||||
maxItems: null,
|
||||
create: false,
|
||||
load: function(query, callback) {
|
||||
if (!query || query.length < 2) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/ldap-search?q=${encodeURIComponent(query)}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
callback(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('LDAP search error:', error);
|
||||
callback();
|
||||
});
|
||||
},
|
||||
onItemAdd: function(value, item) {
|
||||
// 選擇項目後清空搜尋框
|
||||
this.setTextboxValue('');
|
||||
this.refreshOptions(false);
|
||||
// 更新隱藏字段
|
||||
updateHiddenField();
|
||||
},
|
||||
onItemRemove: function(value) {
|
||||
// 移除項目時更新隱藏字段
|
||||
updateHiddenField();
|
||||
},
|
||||
render: {
|
||||
option: function(item, escape) {
|
||||
return `<div class="py-1">${escape(item.text)}</div>`;
|
||||
},
|
||||
item: function(item, escape) {
|
||||
// 移除藍底背景,改用淺灰背景
|
||||
return `<div class="badge bg-light text-dark border me-1">${escape(item.text)}</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 將 Tom Select 實例儲存到全域變數以便偵錯
|
||||
window.recipientTomSelect = recipientSelect;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -14,6 +14,8 @@
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.css" />
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.css" />
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.min.css">
|
||||
<!-- Tom Select CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.bootstrap5.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
@@ -35,7 +37,7 @@
|
||||
</li>
|
||||
{% if current_user.role == 'admin' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('admin.user_list') }}">帳號管理</a>
|
||||
<a class="nav-link" href="{{ url_for('admin.user_list') }}">權限管理</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
@@ -82,6 +84,8 @@
|
||||
<!-- Plugins JS -->
|
||||
<script src="https://uicdn.toast.com/editor-plugin-color-syntax/latest/toastui-editor-plugin-color-syntax.min.js"></script>
|
||||
<script src="https://uicdn.toast.com/editor-plugin-table-merged-cell/latest/toastui-editor-plugin-table-merged-cell.min.js"></script>
|
||||
<!-- Tom Select JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 啟用所有 Toast
|
||||
|
@@ -27,9 +27,68 @@
|
||||
<div class="form-text">請上傳展延申請的相關佐證文件 (PDF 格式)。</div>
|
||||
</div>
|
||||
|
||||
<!-- 郵件通知對象選擇 -->
|
||||
<div class="mb-3">
|
||||
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
|
||||
<select id="recipients" name="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
|
||||
</select>
|
||||
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">確認展延</button>
|
||||
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const recipientSelect = new TomSelect('#recipients', {
|
||||
valueField: 'value',
|
||||
labelField: 'text',
|
||||
searchField: 'text',
|
||||
placeholder: '請輸入姓名或 Email 來搜尋...',
|
||||
plugins: ['remove_button'],
|
||||
maxItems: null,
|
||||
create: false,
|
||||
load: function(query, callback) {
|
||||
if (!query || query.length < 2) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/ldap-search?q=${encodeURIComponent(query)}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
callback(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('LDAP search error:', error);
|
||||
callback();
|
||||
});
|
||||
},
|
||||
onItemAdd: function(value, item) {
|
||||
// 選擇項目後清空搜尋框
|
||||
this.setTextboxValue('');
|
||||
this.refreshOptions(false);
|
||||
},
|
||||
render: {
|
||||
option: function(item, escape) {
|
||||
return `<div class="py-1">${escape(item.text)}</div>`;
|
||||
},
|
||||
item: function(item, escape) {
|
||||
// 移除藍底背景,改用淺灰背景
|
||||
return `<div class="badge bg-light text-dark border me-1">${escape(item.text)}</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@@ -22,8 +22,10 @@
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">使用者帳號</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
<label for="username" class="form-label">使用者帳號 (完整格式)</label>
|
||||
<input type="email" class="form-control" id="username" name="username"
|
||||
placeholder="請輸入完整AD帳號 (如: user@domain.com)" required>
|
||||
<div class="form-text text-light fw-bold">請輸入完整的 AD 帳號格式 (包含 @domain)</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">密碼</label>
|
||||
@@ -33,10 +35,6 @@
|
||||
<button type="submit" class="btn btn-primary">登入</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<p class="mb-0">還沒有帳號嗎?</p>
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn-outline-success">立即註冊</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,47 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}註冊新帳號 - 暫時規範系統{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<h2 class="text-center mb-4">建立新帳號</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category or 'danger' }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">使用者帳號</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">密碼</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">確認密碼</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success">註冊</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<a href="{{ url_for('auth.login') }}">已經有帳號了?返回登入</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -19,9 +19,69 @@
|
||||
<label for="reason" class="form-label"><strong>提早結束原因</strong></label>
|
||||
<textarea class="form-control" id="reason" name="reason" rows="4" required></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 郵件通知對象選擇 -->
|
||||
<div class="mb-3">
|
||||
<label for="recipients" class="form-label"><strong>郵件通知對象</strong></label>
|
||||
<select id="recipients" name="recipients" multiple placeholder="請輸入姓名或 Email 來搜尋...">
|
||||
</select>
|
||||
<div class="form-text">可搜尋姓名或 Email 地址,支援多人選擇</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-danger">確認終止</button>
|
||||
<a href="{{ url_for('temp_spec.spec_list') }}" class="btn btn-secondary">取消</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const recipientSelect = new TomSelect('#recipients', {
|
||||
valueField: 'value',
|
||||
labelField: 'text',
|
||||
searchField: 'text',
|
||||
placeholder: '請輸入姓名或 Email 來搜尋...',
|
||||
plugins: ['remove_button'],
|
||||
maxItems: null,
|
||||
create: false,
|
||||
load: function(query, callback) {
|
||||
if (!query || query.length < 2) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/ldap-search?q=${encodeURIComponent(query)}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
callback(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('LDAP search error:', error);
|
||||
callback();
|
||||
});
|
||||
},
|
||||
onItemAdd: function(value, item) {
|
||||
// 選擇項目後清空搜尋框
|
||||
this.setTextboxValue('');
|
||||
this.refreshOptions(false);
|
||||
},
|
||||
render: {
|
||||
option: function(item, escape) {
|
||||
return `<div class="py-1">${escape(item.text)}</div>`;
|
||||
},
|
||||
item: function(item, escape) {
|
||||
// 移除藍底背景,改用淺灰背景
|
||||
return `<div class="badge bg-light text-dark border me-1">${escape(item.text)}</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}帳號管理{% endblock %}
|
||||
{% block title %}權限管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">帳號管理</h2>
|
||||
<h2 class="mb-4">權限管理</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
@@ -16,76 +16,219 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- 新增使用者表單 -->
|
||||
<!-- 設定新管理員 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
新增使用者
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="bi bi-person-plus-fill"></i> 設定管理員權限
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('admin.create_user') }}" method="post" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="username" class="form-control" placeholder="使用者名稱" required>
|
||||
<form action="{{ url_for('admin.set_admin') }}" method="post" class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label for="username" class="form-label">AD 帳號</label>
|
||||
<input type="text" name="username" class="form-control" id="username"
|
||||
placeholder="例如:user@panjit.com.tw 或 username" required>
|
||||
<div class="form-text">輸入需要設定為管理員的 AD 帳號</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="password" name="password" class="form-control" placeholder="密碼" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select name="role" class="form-select" required>
|
||||
<option value="viewer">Viewer</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">建立</button>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-shield-check"></i> 設定為管理員
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用者列表 -->
|
||||
<!-- 現有使用者權限管理 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
現有使用者列表
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<i class="bi bi-people-fill"></i> 現有使用者權限管理
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>使用者名稱</th>
|
||||
<th>權限</th>
|
||||
<th>上次登入</th>
|
||||
<th colspan="2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<form action="{{ url_for('admin.edit_user', user_id=user.id) }}" method="post" class="d-inline">
|
||||
{% if users %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>AD 帳號</th>
|
||||
<th>目前權限</th>
|
||||
<th>上次登入</th>
|
||||
<th>權限管理</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr {% if user.id == current_user.id %}class="table-warning"{% endif %}>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>
|
||||
<select name="role" class="form-select form-select-sm">
|
||||
<option value="viewer" {% if user.role == 'viewer' %}selected{% endif %}>Viewer</option>
|
||||
<option value="editor" {% if user.role == 'editor' %}selected{% endif %}>Editor</option>
|
||||
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
|
||||
</select>
|
||||
{{ user.username }}
|
||||
{% if user.id == current_user.id %}
|
||||
<span class="badge bg-info ms-1">目前使用者</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.last_login.strftime('%Y-%m-%d %H:%M') if user.last_login else '從未' }}</td>
|
||||
<td>
|
||||
<button type="submit" class="btn btn-sm btn-success">更新</button>
|
||||
<span class="badge
|
||||
{% if user.role == 'admin' %}bg-danger
|
||||
{% elif user.role == 'editor' %}bg-warning text-dark
|
||||
{% else %}bg-secondary
|
||||
{% endif %}">
|
||||
{% if user.role == 'admin' %}
|
||||
<i class="bi bi-shield-fill"></i> 管理員
|
||||
{% elif user.role == 'editor' %}
|
||||
<i class="bi bi-pencil-fill"></i> 編輯者
|
||||
{% else %}
|
||||
<i class="bi bi-eye-fill"></i> 檢視者
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
</form>
|
||||
<td>
|
||||
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="post" onsubmit="return confirm('確定要刪除這位使用者嗎?');" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger" {% if user.id == current_user.id %}disabled{% endif %}>刪除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<td>
|
||||
{% if user.last_login %}
|
||||
{{ user.last_login.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
<span class="text-muted">從未登入</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('admin.edit_user_role', user_id=user.id) }}"
|
||||
method="post" class="d-inline">
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="role" class="form-select form-select-sm">
|
||||
<option value="viewer" {% if user.role == 'viewer' %}selected{% endif %}>
|
||||
檢視者 (Viewer)
|
||||
</option>
|
||||
<option value="editor" {% if user.role == 'editor' %}selected{% endif %}>
|
||||
編輯者 (Editor)
|
||||
</option>
|
||||
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>
|
||||
管理員 (Admin)
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.id != current_user.id %}
|
||||
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}"
|
||||
method="post"
|
||||
onsubmit="return confirm('確定要刪除使用者 {{ user.username }} 嗎?此操作無法復原!');"
|
||||
class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" disabled
|
||||
title="無法刪除自己的帳號">
|
||||
<i class="bi bi-shield-x"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
目前沒有任何使用者記錄。使用者會在首次透過 AD 登入時自動建立。
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 權限說明 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle"></i> 權限等級說明
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h6><span class="badge bg-secondary"><i class="bi bi-eye-fill"></i> 檢視者 (Viewer)</span></h6>
|
||||
<ul class="small">
|
||||
<li>登入系統</li>
|
||||
<li>檢視規範列表</li>
|
||||
<li>下載已生效的 PDF 文件</li>
|
||||
<li>查看歷史記錄</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6><span class="badge bg-warning text-dark"><i class="bi bi-pencil-fill"></i> 編輯者 (Editor)</span></h6>
|
||||
<ul class="small">
|
||||
<li>包含 Viewer 所有權限</li>
|
||||
<li>建立新的暫時規範</li>
|
||||
<li>編輯規範內容</li>
|
||||
<li>展延規範</li>
|
||||
<li>終止規範</li>
|
||||
<li>下載 Word 編輯檔案</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6><span class="badge bg-danger"><i class="bi bi-shield-fill"></i> 管理員 (Admin)</span></h6>
|
||||
<ul class="small">
|
||||
<li>包含 Editor 所有權限</li>
|
||||
<li>啟用待生效的規範</li>
|
||||
<li>管理使用者權限</li>
|
||||
<li>刪除規範</li>
|
||||
<li>系統設定管理</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LDAP 整合說明 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<i class="bi bi-diagram-3"></i> LDAP/AD 整合說明
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>系統運作方式:</strong></p>
|
||||
<ol>
|
||||
<li>使用者首次使用 AD 帳號登入時,系統會自動建立本地使用者記錄,預設權限為 <code>viewer</code></li>
|
||||
<li>管理員可以在此頁面調整使用者權限等級</li>
|
||||
<li>使用者的身份認證完全由 Active Directory 處理,本系統不儲存密碼</li>
|
||||
<li>刪除本地使用者記錄不會影響 AD 帳號,使用者仍可重新登入(但會重置為 viewer 權限)</li>
|
||||
</ol>
|
||||
|
||||
<div class="alert alert-warning mt-3">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>重要提醒:</strong>確保至少保留一個管理員帳號,避免無法進行權限管理。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// 自動提交表單當選擇權限改變時的確認
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const roleSelects = document.querySelectorAll('select[name="role"]');
|
||||
roleSelects.forEach(select => {
|
||||
select.addEventListener('change', function() {
|
||||
const form = this.closest('form');
|
||||
const username = form.closest('tr').querySelector('td:nth-child(2)').textContent.trim();
|
||||
const newRole = this.value;
|
||||
|
||||
if (confirm(`確定要將使用者 "${username}" 的權限變更為 "${newRole}" 嗎?`)) {
|
||||
// 使用者確認後自動提交
|
||||
setTimeout(() => {
|
||||
form.submit();
|
||||
}, 100);
|
||||
} else {
|
||||
// 使用者取消,恢復原來的選擇
|
||||
this.selectedIndex = this.getAttribute('data-original-index') || 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 記錄原始選擇索引
|
||||
select.setAttribute('data-original-index', select.selectedIndex);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
72
test_api.py
Normal file
72
test_api.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
測試 LDAP API 端點功能
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
|
||||
def test_api_search():
|
||||
"""測試 API 搜尋功能"""
|
||||
print("=== 測試 LDAP API 搜尋功能 ===")
|
||||
|
||||
# 測試不同的搜尋詞
|
||||
test_terms = ["liu", "劉", "PE", "管理", "admin"]
|
||||
|
||||
base_url = "http://127.0.0.1:5000"
|
||||
|
||||
# 先登入獲取 session cookie
|
||||
session = requests.Session()
|
||||
|
||||
print("正在登入系統...")
|
||||
login_data = {
|
||||
'username': 'ymirliu@panjit.com.tw',
|
||||
'password': input("請輸入 ymirliu@panjit.com.tw 的密碼: ")
|
||||
}
|
||||
|
||||
# 嘗試登入
|
||||
login_response = session.post(f"{base_url}/login", data=login_data)
|
||||
|
||||
if login_response.status_code == 200 and "總表檢視" in login_response.text:
|
||||
print("✅ 登入成功")
|
||||
|
||||
for term in test_terms:
|
||||
print(f"\n🔍 測試搜尋: '{term}'")
|
||||
print("-" * 40)
|
||||
|
||||
api_url = f"{base_url}/api/ldap-search"
|
||||
params = {'q': term}
|
||||
|
||||
try:
|
||||
response = session.get(api_url, params=params)
|
||||
print(f"HTTP 狀態: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
print(f"找到 {len(data)} 個結果:")
|
||||
|
||||
for i, result in enumerate(data, 1):
|
||||
print(f" {i}. {result.get('text', 'N/A')}")
|
||||
print(f" 值: {result.get('value', 'N/A')}")
|
||||
print(f" 類型: {result.get('type', 'N/A')}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print("❌ 回應不是有效的 JSON")
|
||||
print(f"回應內容: {response.text[:200]}")
|
||||
else:
|
||||
print(f"❌ API 請求失敗: {response.status_code}")
|
||||
print(f"回應: {response.text[:200]}")
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"❌ 請求錯誤: {e}")
|
||||
else:
|
||||
print("❌ 登入失敗")
|
||||
print(f"HTTP 狀態: {login_response.status_code}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_api_search()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n測試已中斷")
|
||||
except Exception as e:
|
||||
print(f"\n測試發生錯誤: {e}")
|
116
test_ldap.py
Normal file
116
test_ldap.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
簡單的 LDAP 連線測試腳本
|
||||
用於驗證 LDAP 設定是否正確
|
||||
"""
|
||||
|
||||
from ldap3 import Server, Connection, ALL
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 載入環境變數
|
||||
load_dotenv()
|
||||
|
||||
def test_ldap_connection():
|
||||
"""測試 LDAP 伺服器連線"""
|
||||
print("=== LDAP 連線測試 ===")
|
||||
|
||||
# 讀取設定
|
||||
ldap_server = os.getenv('LDAP_SERVER')
|
||||
ldap_port = int(os.getenv('LDAP_PORT', 389))
|
||||
use_ssl = os.getenv('LDAP_USE_SSL', 'false').lower() in ['true', '1', 't']
|
||||
bind_dn = os.getenv('LDAP_BIND_USER_DN')
|
||||
bind_password = os.getenv('LDAP_BIND_USER_PASSWORD')
|
||||
search_base = os.getenv('LDAP_SEARCH_BASE')
|
||||
|
||||
print(f"LDAP 伺服器: {ldap_server}")
|
||||
print(f"LDAP 連接埠: {ldap_port}")
|
||||
print(f"使用 SSL: {use_ssl}")
|
||||
print(f"搜尋基底: {search_base}")
|
||||
print(f"服務帳號 DN: {bind_dn}")
|
||||
|
||||
try:
|
||||
# 建立伺服器連線
|
||||
server = Server(ldap_server, port=ldap_port, use_ssl=use_ssl, get_info=ALL)
|
||||
print(f"✅ LDAP 伺服器物件建立成功")
|
||||
|
||||
# 測試服務帳號連線
|
||||
print("正在測試服務帳號連線...")
|
||||
conn = Connection(server, user=bind_dn, password=bind_password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
print("✅ 服務帳號連線成功!")
|
||||
|
||||
# 測試搜尋功能
|
||||
print("正在測試 LDAP 搜尋功能...")
|
||||
search_filter = "(objectClass=user)"
|
||||
conn.search(search_base, search_filter, attributes=['dn'], size_limit=5)
|
||||
|
||||
if conn.entries:
|
||||
print(f"✅ LDAP 搜尋成功,找到 {len(conn.entries)} 個條目")
|
||||
for entry in conn.entries[:3]:
|
||||
print(f" - {entry.entry_dn}")
|
||||
else:
|
||||
print("⚠️ LDAP 搜尋沒有找到任何條目")
|
||||
|
||||
conn.unbind()
|
||||
else:
|
||||
print("❌ 服務帳號連線失敗")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ LDAP 連線錯誤: {e}")
|
||||
return False
|
||||
|
||||
print("=== LDAP 連線測試完成 ===")
|
||||
return True
|
||||
|
||||
def test_user_authentication():
|
||||
"""測試使用者認證 (需要手動輸入測試帳號)"""
|
||||
print("\n=== 使用者認證測試 ===")
|
||||
|
||||
test_user = input("請輸入測試用帳號 (完整UPN格式,如 user@domain.com): ").strip()
|
||||
if not test_user or '@' not in test_user:
|
||||
print("❌ 帳號格式不正確")
|
||||
return False
|
||||
|
||||
test_password = input("請輸入測試密碼: ").strip()
|
||||
if not test_password:
|
||||
print("❌ 密碼不可為空")
|
||||
return False
|
||||
|
||||
# 讀取設定
|
||||
ldap_server = os.getenv('LDAP_SERVER')
|
||||
ldap_port = int(os.getenv('LDAP_PORT', 389))
|
||||
use_ssl = os.getenv('LDAP_USE_SSL', 'false').lower() in ['true', '1', 't']
|
||||
|
||||
try:
|
||||
server = Server(ldap_server, port=ldap_port, use_ssl=use_ssl, get_info=ALL)
|
||||
|
||||
print(f"正在驗證 {test_user}...")
|
||||
conn = Connection(server, user=test_user, password=test_password, auto_bind=True)
|
||||
|
||||
if conn.bound:
|
||||
print("✅ 使用者認證成功!")
|
||||
conn.unbind()
|
||||
return True
|
||||
else:
|
||||
print("❌ 使用者認證失敗 - 帳號或密碼錯誤")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 認證過程發生錯誤: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("LDAP 測試工具")
|
||||
print("此工具用於測試 LDAP 連線和認證功能\n")
|
||||
|
||||
# 測試 LDAP 連線
|
||||
if test_ldap_connection():
|
||||
# 如果連線測試通過,可以選擇測試使用者認證
|
||||
choice = input("\n是否要測試使用者認證? (y/N): ").strip().lower()
|
||||
if choice == 'y':
|
||||
test_user_authentication()
|
||||
|
||||
input("\n按 Enter 鍵結束...")
|
113
test_ldap_search.py
Normal file
113
test_ldap_search.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
測試 LDAP 搜尋功能的獨立腳本
|
||||
用於偵錯郵件通知對象搜尋問題
|
||||
"""
|
||||
|
||||
from ldap_utils import search_ldap_principals, search_ldap_groups
|
||||
from app import app
|
||||
import sys
|
||||
|
||||
def test_user_search():
|
||||
"""測試使用者搜尋功能"""
|
||||
print("=== 測試使用者搜尋功能 ===")
|
||||
|
||||
test_terms = ["劉", "liu", "管理", "admin", "工程"]
|
||||
|
||||
for term in test_terms:
|
||||
print(f"\n搜尋詞: '{term}'")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
results = search_ldap_principals(term, limit=10)
|
||||
print(f"找到 {len(results)} 個結果:")
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"{i}. {result['name']} ({result['email']})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜尋失敗: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_group_search():
|
||||
"""測試群組搜尋功能"""
|
||||
print("\n=== 測試群組搜尋功能 ===")
|
||||
|
||||
test_terms = ["管理", "工程", "admin", "group"]
|
||||
|
||||
for term in test_terms:
|
||||
print(f"\n搜尋詞: '{term}'")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
results = search_ldap_groups(term, limit=10)
|
||||
print(f"找到 {len(results)} 個群組:")
|
||||
|
||||
for i, result in enumerate(results, 1):
|
||||
print(f"{i}. {result['name']} (成員數: {result['member_count']})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"群組搜尋失敗: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_interactive_search():
|
||||
"""互動式搜尋測試"""
|
||||
print("\n=== 互動式搜尋測試 ===")
|
||||
|
||||
while True:
|
||||
search_term = input("\n請輸入搜尋詞 (或輸入 'quit' 結束): ").strip()
|
||||
|
||||
if search_term.lower() == 'quit':
|
||||
break
|
||||
|
||||
if not search_term:
|
||||
continue
|
||||
|
||||
print(f"\n搜尋 '{search_term}'...")
|
||||
|
||||
# 搜尋使用者
|
||||
try:
|
||||
user_results = search_ldap_principals(search_term, limit=10)
|
||||
print(f"\n👤 使用者結果 ({len(user_results)}):")
|
||||
for i, result in enumerate(user_results, 1):
|
||||
print(f" {i}. {result['name']} ({result['email']})")
|
||||
except Exception as e:
|
||||
print(f"使用者搜尋失敗: {e}")
|
||||
|
||||
# 搜尋群組
|
||||
try:
|
||||
group_results = search_ldap_groups(search_term, limit=5)
|
||||
print(f"\n👥 群組結果 ({len(group_results)}):")
|
||||
for i, result in enumerate(group_results, 1):
|
||||
print(f" {i}. {result['name']} (成員: {result['member_count']})")
|
||||
except Exception as e:
|
||||
print(f"群組搜尋失敗: {e}")
|
||||
|
||||
def main():
|
||||
"""主測試函式"""
|
||||
print("LDAP 搜尋功能測試工具")
|
||||
print("=" * 50)
|
||||
|
||||
with app.app_context():
|
||||
# 測試預設搜尋詞
|
||||
test_user_search()
|
||||
test_group_search()
|
||||
|
||||
# 互動式測試
|
||||
choice = input("\n是否要進行互動式搜尋測試? (y/N): ").strip().lower()
|
||||
if choice == 'y':
|
||||
test_interactive_search()
|
||||
|
||||
print("\n測試完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n測試已中斷")
|
||||
except Exception as e:
|
||||
print(f"\n測試發生錯誤: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
126
test_org_search.py
Normal file
126
test_org_search.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
測試組織架構搜尋功能的獨立腳本
|
||||
根據您提供的組織架構圖進行測試
|
||||
"""
|
||||
|
||||
from ldap_utils import search_ldap_groups
|
||||
from app import app
|
||||
|
||||
def test_org_units():
|
||||
"""測試組織單位搜尋"""
|
||||
print("=== 測試組織單位搜尋 ===")
|
||||
|
||||
# 根據您的組織架構圖測試這些關鍵字
|
||||
test_terms = [
|
||||
"PANJIT", # 主要組織
|
||||
"Audit Office", # 稽核室
|
||||
"Fab bu", # 晶圓事業部
|
||||
"CFPU", # 客戶產品事業部
|
||||
"GMO", # 總經理室
|
||||
"RBU", # 新創事業處
|
||||
"kM_ESP", # 知識管理
|
||||
"SBG", # 特殊應用事業群
|
||||
"AUBU", # 車載事業處
|
||||
"IQBU", # 檢測事業處
|
||||
"MBU1", # 行銷事業處1
|
||||
"AssEng", # 製程工程
|
||||
"scottlee", # 個人帳號
|
||||
"staceychu", # 個人帳號
|
||||
"ymirliu" # 劉經理
|
||||
]
|
||||
|
||||
for term in test_terms:
|
||||
print(f"\n搜尋: '{term}'")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
results = search_ldap_groups(term, limit=10)
|
||||
|
||||
if results:
|
||||
print(f"找到 {len(results)} 個結果:")
|
||||
for i, result in enumerate(results, 1):
|
||||
type_text = "組織單位" if result['type'] == 'organizationalUnit' else "群組"
|
||||
print(f" {i}. {result['name']} ({type_text})")
|
||||
print(f" 成員: {result['member_count']}")
|
||||
if result.get('email'):
|
||||
print(f" 郵件: {result['email']}")
|
||||
print(f" DN: {result['dn']}")
|
||||
print()
|
||||
else:
|
||||
print("沒有找到結果")
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜尋錯誤: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_specific_search():
|
||||
"""測試特定搜尋詞"""
|
||||
print("\n=== 特定搜尋測試 ===")
|
||||
|
||||
while True:
|
||||
term = input("\n請輸入要搜尋的組織名稱 (或輸入 'quit' 結束): ").strip()
|
||||
|
||||
if term.lower() == 'quit':
|
||||
break
|
||||
|
||||
if not term:
|
||||
continue
|
||||
|
||||
print(f"\n搜尋組織: '{term}'")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
results = search_ldap_groups(term, limit=15)
|
||||
|
||||
if results:
|
||||
print(f"找到 {len(results)} 個組織:")
|
||||
for i, result in enumerate(results, 1):
|
||||
type_text = "組織單位" if result['type'] == 'organizationalUnit' else "群組"
|
||||
print(f"{i}. {result['name']} ({type_text}, 成員: {result['member_count']})")
|
||||
|
||||
# 提供選擇選項
|
||||
if results:
|
||||
choice = input(f"\n要查看哪個組織的詳細資訊? (1-{len(results)}, 或按 Enter 跳過): ").strip()
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(results):
|
||||
selected = results[int(choice) - 1]
|
||||
print(f"\n{selected['name']} 詳細資訊:")
|
||||
print(f" 類型: {selected['type']}")
|
||||
print(f" 成員數: {selected['member_count']}")
|
||||
print(f" DN: {selected['dn']}")
|
||||
if selected.get('email'):
|
||||
print(f" 群組郵件: {selected['email']}")
|
||||
|
||||
else:
|
||||
print("沒有找到相符的組織")
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜尋失敗: {e}")
|
||||
|
||||
def main():
|
||||
"""主測試函式"""
|
||||
print("組織架構搜尋測試工具")
|
||||
print("=" * 50)
|
||||
print("此工具將測試根據您的 AD 組織架構搜尋群組和組織單位")
|
||||
|
||||
with app.app_context():
|
||||
# 自動測試常見組織名稱
|
||||
test_org_units()
|
||||
|
||||
# 互動式測試
|
||||
choice = input("\n是否要進行互動式組織搜尋測試? (y/N): ").strip().lower()
|
||||
if choice == 'y':
|
||||
test_specific_search()
|
||||
|
||||
print("\n測試完成!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n測試已中斷")
|
||||
except Exception as e:
|
||||
print(f"\n測試發生錯誤: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
137
test_startup.py
Normal file
137
test_startup.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
測試系統啟動的腳本
|
||||
用於檢查各個模組是否可以正常導入和初始化
|
||||
"""
|
||||
|
||||
def test_imports():
|
||||
"""測試所有模組導入"""
|
||||
print("🔧 測試模組導入...")
|
||||
|
||||
try:
|
||||
print(" ├─ 測試基本 Flask 組件...")
|
||||
from flask import Flask
|
||||
from flask_login import LoginManager
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
print(" │ ✅ Flask 基本組件導入成功")
|
||||
|
||||
print(" ├─ 測試資料庫模型...")
|
||||
from models import db, User, TempSpec
|
||||
print(" │ ✅ 資料庫模型導入成功")
|
||||
|
||||
print(" ├─ 測試工具函式...")
|
||||
from utils import admin_required, editor_or_admin_required, send_email
|
||||
print(" │ ✅ 工具函式導入成功")
|
||||
|
||||
print(" ├─ 測試 LDAP 工具...")
|
||||
from ldap_utils import authenticate_ldap_user, search_ldap_principals
|
||||
print(" │ ✅ LDAP 工具導入成功")
|
||||
|
||||
print(" ├─ 測試路由模組...")
|
||||
from routes.auth import auth_bp
|
||||
print(" │ ✅ auth 路由導入成功")
|
||||
|
||||
from routes.temp_spec import temp_spec_bp
|
||||
print(" │ ✅ temp_spec 路由導入成功")
|
||||
|
||||
from routes.admin import admin_bp
|
||||
print(" │ ✅ admin 路由導入成功")
|
||||
|
||||
from routes.api import api_bp
|
||||
print(" │ ✅ api 路由導入成功")
|
||||
|
||||
from routes.upload import upload_bp
|
||||
print(" │ ✅ upload 路由導入成功")
|
||||
|
||||
print(" ├─ 測試排程任務...")
|
||||
from tasks import check_expiring_specs
|
||||
print(" │ ✅ 排程任務導入成功")
|
||||
|
||||
print(" └─ 所有模組導入成功!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 模組導入失敗: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_app_creation():
|
||||
"""測試 Flask 應用程式建立"""
|
||||
print("\n🔧 測試應用程式建立...")
|
||||
|
||||
try:
|
||||
from app import app
|
||||
print(" ✅ Flask 應用程式建立成功")
|
||||
|
||||
# 測試路由註冊
|
||||
with app.app_context():
|
||||
print(f" ✅ 應用程式名稱: {app.name}")
|
||||
print(f" ✅ 註冊的藍圖數量: {len(app.blueprints)}")
|
||||
for bp_name in app.blueprints:
|
||||
print(f" - {bp_name}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 應用程式建立失敗: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_config():
|
||||
"""測試設定檔"""
|
||||
print("\n🔧 測試設定檔...")
|
||||
|
||||
try:
|
||||
from config import Config
|
||||
print(" ✅ 設定檔導入成功")
|
||||
|
||||
# 檢查重要設定
|
||||
config = Config()
|
||||
print(f" ✅ SECRET_KEY 已設定: {'是' if hasattr(config, 'SECRET_KEY') and config.SECRET_KEY else '否'}")
|
||||
print(f" ✅ DATABASE_URL 已設定: {'是' if hasattr(config, 'SQLALCHEMY_DATABASE_URI') and config.SQLALCHEMY_DATABASE_URI else '否'}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ 設定檔測試失敗: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主測試函式"""
|
||||
print("=" * 50)
|
||||
print("🚀 暫時規範管理系統 V3 - 啟動測試")
|
||||
print("=" * 50)
|
||||
|
||||
success_count = 0
|
||||
total_tests = 3
|
||||
|
||||
# 測試模組導入
|
||||
if test_imports():
|
||||
success_count += 1
|
||||
|
||||
# 測試設定檔
|
||||
if test_config():
|
||||
success_count += 1
|
||||
|
||||
# 測試應用程式建立
|
||||
if test_app_creation():
|
||||
success_count += 1
|
||||
|
||||
# 結果統計
|
||||
print("\n" + "=" * 50)
|
||||
print(f"📊 測試結果: {success_count}/{total_tests} 個測試通過")
|
||||
|
||||
if success_count == total_tests:
|
||||
print("🎉 所有測試通過!系統可以正常啟動。")
|
||||
return True
|
||||
else:
|
||||
print("⚠️ 部分測試失敗,請檢查上述錯誤訊息。")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
exit(0 if success else 1)
|
37
update_admin.py
Normal file
37
update_admin.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
更新使用者權限為管理員的腳本
|
||||
"""
|
||||
|
||||
from models import db, User
|
||||
from app import app
|
||||
|
||||
def update_user_to_admin(username):
|
||||
"""將指定使用者設為管理員權限"""
|
||||
with app.app_context():
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user:
|
||||
print(f'找到使用者: {user.username}')
|
||||
print(f'當前權限: {user.role}')
|
||||
|
||||
if user.role != 'admin':
|
||||
user.role = 'admin'
|
||||
db.session.commit()
|
||||
print(f'權限已更新為: {user.role}')
|
||||
else:
|
||||
print('使用者已經是管理員權限')
|
||||
return True
|
||||
else:
|
||||
print(f'使用者 {username} 不存在')
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
username = 'ymirliu@panjit.com.tw'
|
||||
print(f"正在更新使用者 {username} 的權限...")
|
||||
|
||||
success = update_user_to_admin(username)
|
||||
|
||||
if success:
|
||||
print("\n管理員權限設定完成!")
|
||||
else:
|
||||
print("\n請先使用該帳號登入一次以建立使用者記錄")
|
100
utils.py
100
utils.py
@@ -165,33 +165,125 @@ from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
from flask import current_app
|
||||
|
||||
def process_recipients(recipients_str):
|
||||
"""
|
||||
處理收件者字串,支援個人郵件和群組展開
|
||||
輸入格式: "email1,email2,group:GroupName"
|
||||
返回: 展開後的郵件地址列表
|
||||
"""
|
||||
print(f"[RECIPIENTS DEBUG] 開始處理收件者: {recipients_str}")
|
||||
|
||||
if not recipients_str:
|
||||
print(f"[RECIPIENTS DEBUG] 收件者字串為空")
|
||||
return []
|
||||
|
||||
recipients = [item.strip() for item in recipients_str.split(',') if item.strip()]
|
||||
final_emails = []
|
||||
|
||||
for recipient in recipients:
|
||||
print(f"[RECIPIENTS DEBUG] 處理收件者項目: {recipient}")
|
||||
|
||||
if recipient.startswith('group:'):
|
||||
# 這是一個群組,需要展開
|
||||
group_name = recipient[6:] # 移除 'group:' 前綴
|
||||
print(f"[RECIPIENTS DEBUG] 發現群組: {group_name}")
|
||||
|
||||
try:
|
||||
from ldap_utils import get_ldap_group_members
|
||||
group_emails = get_ldap_group_members(group_name)
|
||||
print(f"[RECIPIENTS DEBUG] 群組 {group_name} 包含 {len(group_emails)} 個成員")
|
||||
|
||||
for email in group_emails:
|
||||
if email and email not in final_emails:
|
||||
final_emails.append(email)
|
||||
print(f"[RECIPIENTS DEBUG] 添加群組成員郵件: {email}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[RECIPIENTS ERROR] 群組 {group_name} 展開失敗: {e}")
|
||||
|
||||
else:
|
||||
# 這是個人郵件地址
|
||||
if recipient and recipient not in final_emails:
|
||||
final_emails.append(recipient)
|
||||
print(f"[RECIPIENTS DEBUG] 添加個人郵件: {recipient}")
|
||||
|
||||
print(f"[RECIPIENTS DEBUG] 最終收件者列表 ({len(final_emails)} 個): {final_emails}")
|
||||
return final_emails
|
||||
|
||||
def send_email(to_addrs, subject, body):
|
||||
"""
|
||||
Sends an email using the SMTP settings from the config.
|
||||
Enhanced with detailed debugging information.
|
||||
"""
|
||||
print(f"[EMAIL DEBUG] 開始發送郵件...")
|
||||
print(f"[EMAIL DEBUG] 收件者數量: {len(to_addrs)}")
|
||||
print(f"[EMAIL DEBUG] 收件者: {to_addrs}")
|
||||
print(f"[EMAIL DEBUG] 主旨: {subject}")
|
||||
|
||||
try:
|
||||
# 取得 SMTP 設定
|
||||
smtp_server = current_app.config['SMTP_SERVER']
|
||||
smtp_port = current_app.config['SMTP_PORT']
|
||||
use_tls = current_app.config['SMTP_USE_TLS']
|
||||
sender_email = current_app.config['SMTP_SENDER_EMAIL']
|
||||
sender_password = current_app.config['SMTP_SENDER_PASSWORD']
|
||||
|
||||
print(f"[EMAIL DEBUG] SMTP 設定:")
|
||||
print(f"[EMAIL DEBUG] - 伺服器: {smtp_server}:{smtp_port}")
|
||||
print(f"[EMAIL DEBUG] - 使用 TLS: {use_tls}")
|
||||
print(f"[EMAIL DEBUG] - 寄件者: {sender_email}")
|
||||
print(f"[EMAIL DEBUG] - 有密碼: {'是' if sender_password else '否'}")
|
||||
|
||||
# 建立郵件內容
|
||||
print(f"[EMAIL DEBUG] 建立郵件內容...")
|
||||
msg = MIMEText(body, 'html', 'utf-8')
|
||||
msg['Subject'] = Header(subject, 'utf-8')
|
||||
msg['From'] = sender_email
|
||||
msg['To'] = ', '.join(to_addrs)
|
||||
print(f"[EMAIL DEBUG] 郵件內容建立完成")
|
||||
|
||||
# 連接 SMTP 伺服器
|
||||
print(f"[EMAIL DEBUG] 連接 SMTP 伺服器 {smtp_server}:{smtp_port}...")
|
||||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||||
print(f"[EMAIL DEBUG] SMTP 伺服器連接成功")
|
||||
|
||||
if use_tls:
|
||||
print(f"[EMAIL DEBUG] 啟用 TLS...")
|
||||
server.starttls()
|
||||
print(f"[EMAIL DEBUG] TLS 啟用成功")
|
||||
|
||||
if sender_password:
|
||||
print(f"[EMAIL DEBUG] 登入 SMTP 伺服器...")
|
||||
server.login(sender_email, sender_password)
|
||||
print(f"[EMAIL DEBUG] SMTP 登入成功")
|
||||
else:
|
||||
print(f"[EMAIL DEBUG] 無需密碼認證")
|
||||
|
||||
# 發送郵件
|
||||
print(f"[EMAIL DEBUG] 發送郵件...")
|
||||
result = server.sendmail(sender_email, to_addrs, msg.as_string())
|
||||
print(f"[EMAIL DEBUG] 郵件發送結果: {result}")
|
||||
|
||||
server.sendmail(sender_email, to_addrs, msg.as_string())
|
||||
server.quit()
|
||||
print(f"Email sent to {', '.join(to_addrs)}")
|
||||
print(f"[EMAIL DEBUG] SMTP 連接已關閉")
|
||||
print(f"[EMAIL SUCCESS] 郵件成功發送至: {', '.join(to_addrs)}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to send email: {e}")
|
||||
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
print(f"[EMAIL ERROR] SMTP 認證失敗: {e}")
|
||||
print(f"[EMAIL ERROR] 請檢查寄件者帳號和密碼設定")
|
||||
return False
|
||||
except smtplib.SMTPConnectError as e:
|
||||
print(f"[EMAIL ERROR] SMTP 連接失敗: {e}")
|
||||
print(f"[EMAIL ERROR] 請檢查 SMTP 伺服器設定")
|
||||
return False
|
||||
except smtplib.SMTPRecipientsRefused as e:
|
||||
print(f"[EMAIL ERROR] 收件者被拒絕: {e}")
|
||||
print(f"[EMAIL ERROR] 請檢查收件者郵件地址")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"[EMAIL ERROR] 郵件發送失敗: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
print(f"[EMAIL ERROR] 詳細錯誤:")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
45
代辦.txt
45
代辦.txt
@@ -1,45 +0,0 @@
|
||||
### **交付與後續步驟**
|
||||
|
||||
程式碼已準備就緒,但要使其正常運作,您需要完成以下**關鍵步驟**:
|
||||
|
||||
**1. 填寫環境變數 (`.env` 檔案):**
|
||||
請根據您從 IT 部門獲取的資訊,在專案根目錄的 `.env` 檔案中新增或修改以下變數。這一步至關重要。
|
||||
|
||||
```ini
|
||||
# --- LDAP Settings ---
|
||||
LDAP_SERVER=dc1.panjit.com.tw
|
||||
LDAP_PORT=389
|
||||
LDAP_USE_SSL=false
|
||||
# 服務帳號 (用於查詢群組)
|
||||
LDAP_BIND_USER_DN="CN=ServiceAccount,OU=Services,DC=panjit,DC=com,DC=tw"
|
||||
LDAP_BIND_USER_PASSWORD="service_account_password"
|
||||
# 使用者和群組的搜尋基礎
|
||||
LDAP_SEARCH_BASE="OU=Users,DC=panjit,DC=com,DC=tw"
|
||||
LDAP_USER_LOGIN_ATTR=userPrincipalName
|
||||
|
||||
# --- SMTP Settings ---
|
||||
SMTP_SERVER=mail.panjit.com.tw
|
||||
SMTP_PORT=25
|
||||
SMTP_USE_TLS=false
|
||||
# 發信人帳號
|
||||
SMTP_SENDER_EMAIL=app-noreply@panjit.com.tw
|
||||
SMTP_SENDER_PASSWORD="email_password_if_needed"
|
||||
```
|
||||
|
||||
**2. 安裝新的 Python 套件:**
|
||||
在您的開發環境中執行以下指令,以安裝 `ldap3`:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**3. 自訂郵件通知:**
|
||||
* 我添加的郵件通知僅作為一個**範例**。您需要:
|
||||
* 將範例中的 `'TempSpec_Approvers'` 替換為貴公司**實際的郵件群組名稱**。
|
||||
* 將此通知邏輯複製並修改,應用到其他需要發送通知的地方,例如 `terminate_spec` (結束通知) 和 `extend_spec` (展延通知)。
|
||||
* 根據需要建立一個排程任務 (例如,使用 APScheduler 或 Windows 工作排程器),定期檢查即將到期的暫規並發送提醒郵件。
|
||||
|
||||
**4. 測試:**
|
||||
完成上述配置後,請啟動應用程式並使用您的 AD 帳號進行登入測試。同時,觸發一個暫規生效的流程,檢查郵件是否能成功發送。
|
||||
|
||||
本次的架構轉移工作已完成。如果您在配置或測試過程中遇到任何問題,或需要對通知邏輯進行進一步的調整,請隨時提出。
|
204
功能升級規劃.txt
Normal file
204
功能升級規劃.txt
Normal file
@@ -0,0 +1,204 @@
|
||||
功能升級計畫:智慧通知模組
|
||||
1. 專案願景
|
||||
本次升級旨在將系統的郵件通知功能從一個靜態的範例,全面進化為一個智慧化、高彈性、自動化的通知中樞。完成後,系統將具備以下三大核心能力:
|
||||
|
||||
互動式通知:在所有關鍵操作(啟用、展延、終止)中,提供類似 Outlook 的動態收件人搜尋介面。
|
||||
|
||||
全流程覆蓋:確保暫規生命週期中的每一個重要節點,都能觸發對應的事件通知。
|
||||
|
||||
前瞻性提醒:從被動通知轉向主動預警,自動發送即將到期的提醒郵件,防止暫規失效。
|
||||
|
||||
2. 專案藍圖 (Roadmap)
|
||||
本計畫將分為三個獨立但環環相扣的模組進行開發,建議依序實施。
|
||||
|
||||
模組 核心功能 開發重點 狀態
|
||||
模組 A 動態收件人介面 建置 LDAP 搜尋 API、前端 UI 元件整合 全新開發
|
||||
模組 B 全流程事件通知 將模組 A 的成果應用於「展延」與「終止」流程 功能擴展
|
||||
模組 C 自動化排程提醒 引入排程器、建立每日檢查任務、定義提醒規則 全新開發
|
||||
|
||||
匯出到試算表
|
||||
3. 模組 A:動態收件人介面 (互動式通知)
|
||||
此模組是整個計畫的基石,目標是打造一個可複用的收件人選擇器。
|
||||
|
||||
A.1. 後端:建置 LDAP 搜尋 API
|
||||
強化 ldap_utils.py:新增 search_ldap_principals 函式,使用服務帳號安全地查詢 AD 中符合條件的使用者,並回傳其姓名與 Email。
|
||||
|
||||
建立 API 路由 routes/api.py:建立一個新的 Blueprint,提供 /api/ldap-search 端點,並使用 @login_required 保護,僅供登入者呼叫。
|
||||
|
||||
註冊 API 路由:在 app.py 中註冊 api_bp,使 API 生效。
|
||||
|
||||
(詳細程式碼請參考前次對話中的計畫,此處不再贅述)
|
||||
|
||||
A.2. 前端:整合 Tom Select 動態搜尋元件
|
||||
修改 templates/base.html:透過 CDN 引入 Tom Select 的 CSS 與 JavaScript 函式庫。
|
||||
|
||||
修改 templates/activate_spec.html:
|
||||
|
||||
新增一個 <select multiple> 輸入框,用於選擇收件人。
|
||||
|
||||
編寫 JavaScript,初始化 Tom Select 元件,並設定其 load 事件去呼叫後端的 /api/ldap-search API,實現使用者輸入時的動態搜尋與載入功能。
|
||||
|
||||
A.3. 邏輯:更新啟用流程
|
||||
修改 routes/temp_spec.py 中的 activate_spec 函式:
|
||||
|
||||
移除原先寫死的 get_ldap_group_members('TempSpec_Approvers') 邏輯。
|
||||
|
||||
改為從 request.form.get('recipients') 獲取由前端 Tom Select 傳來、以逗號分隔的 Email 字串。
|
||||
|
||||
解析字串為 Email 列表,並傳遞給 send_email 函式。
|
||||
|
||||
4. 模組 B:全流程事件通知 (全流程覆蓋)
|
||||
此模組的目標是將 模組 A 的成果,擴展應用到「展延」與「終止」這兩個操作上。
|
||||
|
||||
B.1. 修改 templates/extend_spec.html
|
||||
複製 activate_spec.html 中新增的「郵件通知對象」HTML 區塊。
|
||||
|
||||
將其貼到 extend_spec.html 的表單中,<button type="submit"> 之前。
|
||||
|
||||
複製 activate_spec.html 中 {% block scripts %} 內的 Tom Select 初始化腳本,並貼到 extend_spec.html 的 scripts 區塊中。
|
||||
|
||||
B.2. 修改 routes/temp_spec.py 中的 extend_spec 函式
|
||||
Python
|
||||
|
||||
# In: routes/temp_spec.py
|
||||
@temp_spec_bp.route('/extend/<int:spec_id>', methods=['GET', 'POST'])
|
||||
@editor_or_admin_required
|
||||
def extend_spec(spec_id):
|
||||
spec = TempSpec.query.get_or_404(spec_id)
|
||||
if request.method == 'POST':
|
||||
# ... (原有的檔案上傳、日期更新等邏輯不變) ...
|
||||
|
||||
# ===== START: 新增郵件通知邏輯 =====
|
||||
recipients_str = request.form.get('recipients')
|
||||
if recipients_str:
|
||||
recipients = [email.strip() for email in recipients_str.split(',')]
|
||||
subject = f"[暫規通知] 規範 '{spec.spec_code}' 已展延"
|
||||
body = f"""
|
||||
<html><body>
|
||||
<p>您好,</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 已成功展延。</p>
|
||||
<p>新的結束日期為: <b>{spec.end_date.strftime('%Y-%m-%d')}</b></p>
|
||||
<p>詳細資訊請登入系統查看。</p>
|
||||
</body></html>
|
||||
"""
|
||||
send_email(recipients, subject, body)
|
||||
# ===== END: 新增郵件通知邏輯 =====
|
||||
|
||||
db.session.commit()
|
||||
flash(f"規範 '{spec.spec_code}' 已成功展延!", 'success')
|
||||
return redirect(url_for('temp_spec.spec_list'))
|
||||
|
||||
default_new_end_date = spec.end_date + timedelta(days=30)
|
||||
return render_template('extend_spec.html', spec=spec, default_new_end_date=default_new_end_date)
|
||||
B.3. 針對 terminate_spec 重複 B.1 與 B.2 的步驟
|
||||
對 templates/terminate_spec.html 和 routes/temp_spec.py 中的 terminate_spec 函式執行相同的修改,調整郵件的 subject 和 body 以符合「終止」的情境。
|
||||
|
||||
5. 模組 C:自動化排程提醒 (前瞻性提醒)
|
||||
此模組將為系統引入大腦,使其能夠主動進行管理。
|
||||
|
||||
C.1. 安裝與設定排程器
|
||||
安裝套件:
|
||||
|
||||
Bash
|
||||
|
||||
pip install Flask-APScheduler
|
||||
並將 Flask-APScheduler 新增到 requirements.txt 檔案中。
|
||||
|
||||
在 app.py 中初始化排程器:
|
||||
|
||||
Python
|
||||
|
||||
# In: app.py
|
||||
from flask_apscheduler import APScheduler # <-- 新增
|
||||
|
||||
# ...
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('config.Config')
|
||||
|
||||
# ===== START: 初始化排程器 =====
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
# ===== END: 初始化排程器 =====
|
||||
|
||||
db.init_app(app)
|
||||
# ...
|
||||
C.2. 建立排程任務 (tasks.py)
|
||||
為了保持程式碼的模組化,我們建立一個新檔案來存放排程任務。
|
||||
|
||||
建立新檔案 tasks.py:
|
||||
|
||||
Python
|
||||
|
||||
# 檔案位置: beabigegg/temp_spec_system_v3/TEMP_spec_system_V3-b9557250a410cf778a51ece25ffe28543f494ffb/tasks.py (新檔案)
|
||||
|
||||
from datetime import date, timedelta
|
||||
from models import TempSpec
|
||||
from utils import send_email
|
||||
from ldap_utils import get_ldap_group_members
|
||||
|
||||
def check_expiring_specs(app):
|
||||
"""
|
||||
每日執行的排程任務:檢查即將到期的暫規並發送提醒郵件。
|
||||
"""
|
||||
with app.app_context():
|
||||
print("Running scheduled task: Checking for expiring specs...")
|
||||
today = date.today()
|
||||
seven_days_later = today + timedelta(days=7)
|
||||
three_days_later = today + timedelta(days=3)
|
||||
|
||||
# 找出 7 天後 和 3 天後到期的暫規
|
||||
expiring_soon = TempSpec.query.filter(
|
||||
TempSpec.status == 'active',
|
||||
TempSpec.end_date.in_([seven_days_later, three_days_later])
|
||||
).all()
|
||||
|
||||
if not expiring_soon:
|
||||
print("No specs expiring in 3 or 7 days.")
|
||||
return
|
||||
|
||||
# **重要**: 定義預設的通知對象,例如某個管理群組
|
||||
# 您需要將 'TempSpec_Admins' 替換為實際的 AD 群組名稱
|
||||
default_recipients = get_ldap_group_members('TempSpec_Admins')
|
||||
if not default_recipients:
|
||||
print("Warning: Could not find default recipients in AD group 'TempSpec_Admins'.")
|
||||
return
|
||||
|
||||
for spec in expiring_soon:
|
||||
remaining_days = (spec.end_date - today).days
|
||||
|
||||
# 組合通知郵件
|
||||
subject = f"[暫規到期提醒] 規範 '{spec.spec_code}' 將於 {remaining_days} 天後到期"
|
||||
body = f"""
|
||||
<html><body>
|
||||
<p>您好,</p>
|
||||
<p>此為自動提醒郵件。</p>
|
||||
<p>暫時規範 <b>{spec.spec_code} - {spec.title}</b> 即將到期。</p>
|
||||
<p><b>結束日期: {spec.end_date.strftime('%Y-%m-%d')} (剩餘 {remaining_days} 天)</b></p>
|
||||
<p>申請人: {spec.applicant}</p>
|
||||
<p>請及時處理,如需展延請登入系統操作。</p>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
# 發送郵件給預設群組
|
||||
send_email(default_recipients, subject, body)
|
||||
print(f"Sent expiry reminder for spec {spec.spec_code} to {len(default_recipients)} recipients.")
|
||||
|
||||
C.3. 在 app.py 中註冊與啟動任務
|
||||
最後,我們需要告訴排程器有這個任務,並設定它每天執行。
|
||||
|
||||
Python
|
||||
|
||||
# In: app.py
|
||||
|
||||
# ... (在 from ... import ... 之後)
|
||||
from tasks import check_expiring_specs
|
||||
|
||||
# ... (在 scheduler.start() 之後)
|
||||
|
||||
# 註冊排程任務:每天凌晨 2:00 執行一次
|
||||
@scheduler.task('cron', id='check_expiring_specs_job', hour=2, minute=0)
|
||||
def scheduled_job():
|
||||
check_expiring_specs(app)
|
||||
|
||||
# ... (Flask app 的其他部分)
|
Reference in New Issue
Block a user