Compare commits

...

67 Commits
v3 ... master

Author SHA1 Message Date
wzj
f64e7b18b0 docker支持 2025-09-06 09:50:47 +08:00
wzj
9c92be13d9 修复验证码bug 2025-06-16 12:53:16 +08:00
wzj
89cb0af79a 修复验证码bug 2025-06-16 12:50:36 +08:00
wzj
9cbb6b022c 修复验证码bug 2025-06-16 12:49:25 +08:00
wzj
375816df87 修复验证码bug 2025-06-16 12:04:59 +08:00
wzj
2b36de569d 调整描述 2025-06-15 10:13:19 +08:00
wzj
21142a6104 调整描述 2025-06-15 10:10:18 +08:00
wzj
f3a2f05b1c 调整描述 2025-06-15 10:08:25 +08:00
wzj
722d88a017 开源协议 2025-06-15 09:54:09 +08:00
wzj
179400f9ed 调整应用信息 2025-06-15 09:27:35 +08:00
wzj
cc2b18ba80 显示证书创建人 2025-06-15 09:23:42 +08:00
wzj
fc3801f0f2 显示证书创建人 2025-06-15 09:21:44 +08:00
wzj
c2ca546955 添加配置模板 2025-06-15 08:57:10 +08:00
wzj
f711ca8280 Remove .env and add to gitignore 2025-06-15 08:51:36 +08:00
wzj
6993679971 Remove .idea directory from repository 2025-06-15 08:46:01 +08:00
wzj
2ae605e60b Remove .idea directory from repository 2025-06-15 08:45:12 +08:00
wzj
117a7885d7 del __pycache__ 2025-06-15 08:42:14 +08:00
wzj
5aeaab5747 CA详情页证书列表分页支持 2025-06-15 08:39:50 +08:00
wzj
35eea7a803 CA详情页证书列表分页支持 2025-06-15 08:36:37 +08:00
wzj
0001c8f0d3 支持批量删除 2025-06-15 08:00:03 +08:00
wzj
5c2381c3a8 支持批量删除 2025-06-15 07:56:35 +08:00
wzj
25add10b35 修复删除CA失败问题 2025-06-15 07:43:46 +08:00
wzj
6f578be53e 修复根证书信任问题 2025-06-15 07:33:08 +08:00
wzj
c5e8fd6e3f 修复根证书信任问题 2025-06-15 07:28:54 +08:00
wzj
47f0c572b3 修复根证书信任问题 2025-06-15 07:19:50 +08:00
wzj
afa61f7a04 修复根证书信任问题 2025-06-15 07:03:27 +08:00
wzj
35165bd58b 修复根证书信任问题 2025-06-15 06:51:55 +08:00
wzj
1ad7b4f642 修复验证码字体问题 2025-06-14 19:37:51 +08:00
wzj
61d9ba7db9 优化邮箱认证功能 2025-06-14 19:25:22 +08:00
wzj
6e3838b9f0 优化邮箱认证功能 2025-06-14 19:22:45 +08:00
wzj
01e546ab51 优化邮箱认证功能 2025-06-14 19:22:42 +08:00
wzj
5854180c29 调整数据库权限 2025-06-14 14:37:03 +08:00
wzj
1450c6aca6 优化图标显示 2025-06-14 14:32:47 +08:00
wzj
a73d27f687 删除env文件 2025-06-14 14:19:23 +08:00
wzj
4d802ba369 全面优化样式,修复注册验证码功能 2025-06-14 14:11:56 +08:00
wzj
1720d69be6 数据库优化 2025-06-14 12:41:30 +08:00
wzj
ce31a591af 数据库优化 2025-06-14 12:32:35 +08:00
wzj
357d7814ee 修复bug 2025-06-14 11:47:23 +08:00
wzj
d04209f0eb 修复bug 2025-06-14 11:45:58 +08:00
wzj
6669404299 修复bug 2025-06-14 11:42:52 +08:00
wzj
fed5afd41a 修复bug 2025-06-14 11:39:54 +08:00
wzj
78a2a188b9 修复bug 2025-06-14 11:38:14 +08:00
wzj
81ce256047 修复bug 2025-06-14 11:32:08 +08:00
wzj
da5071b3c1 修复bug 2025-06-14 11:26:18 +08:00
wzj
fd9f2d6e52 支持删除CA和证书 2025-06-14 11:22:40 +08:00
wzj
56b4a91972 支持删除CA和证书 2025-06-14 11:14:00 +08:00
wzj
c390c63323 支持删除CA和证书 2025-06-14 11:10:16 +08:00
wzj
dac0655cff 修复显示证书创建者 2025-06-14 11:02:39 +08:00
wzj
f329def763 修复显示证书创建者 2025-06-14 10:58:07 +08:00
wzj
2032476234 修复bug 2025-06-14 10:55:19 +08:00
wzj
eb3ed291a2 修复bug 2025-06-14 10:54:16 +08:00
wzj
0abcefe147 修复bug 2025-06-14 10:50:30 +08:00
wzj
9d2c022f1b 修复bug 2025-06-14 10:49:34 +08:00
wzj
ce7a602d63 修复bug 2025-06-14 10:46:08 +08:00
wzj
c4219c2149 修复bug 2025-06-14 10:38:40 +08:00
wzj
e059943699 修复bug 2025-06-14 10:36:04 +08:00
wzj
cf4d571623 修复bug 2025-06-14 10:33:07 +08:00
wzj
95f473d05e 修复bug 2025-06-14 10:31:25 +08:00
wzj
9bcdeed2c2 修复bug 2025-06-14 10:27:14 +08:00
wzj
ff088e83c2 修复bug 2025-06-14 10:18:15 +08:00
wzj
47b3cbe407 修复bug 2025-06-14 10:17:38 +08:00
wzj
0cef097dfa 修复bug 2025-06-14 10:12:03 +08:00
wzj
b041b73766 修复bug 2025-06-14 10:05:11 +08:00
wzj
240409ccf9 修复bug 2025-06-14 10:01:50 +08:00
wzj
649823180d 支持导出ca 2025-06-14 09:46:43 +08:00
wzj
7200cd0de1 支持导出ca 2025-06-14 09:46:15 +08:00
wzj
df91c85d97 支持导出ca 2025-06-14 09:43:08 +08:00
37 changed files with 3054 additions and 585 deletions

5
.env
View File

@ -1,5 +0,0 @@
FLASK_SECRET_KEY=your-secret-key
DB_HOST=192.168.31.11
DB_NAME=cert_manager
DB_USER=root
DB_PASSWORD=Home123#$.

39
.env.example Normal file
View File

@ -0,0 +1,39 @@
# [安全配置]
SECRET_KEY=your-unique-and-secure-secret-key-here # Flask应用加密密钥生产环境必须修改建议使用32位随机字符串
ADMIN_USERNAME=admin # 管理员名称
ADMIN_PASSWORD=123456 # 管理员初始密码(首次部署后必须修改)
# [数据库配置]
DB_HOST=192.168.31.11 # 数据库服务器IP地址或域名
DB_NAME=cert_manager # 数据库名称
DB_USER=certmgr # 数据库用户名
DB_PASSWORD=certmgr123 # 数据库密码(生产环境建议使用强密码)
DB_PORT=3306 # 数据库端口MySQL默认3306
# [应用配置]
APP_HOST=0.0.0.0 # 应用监听地址0.0.0.0表示允许所有IP访问
APP_PORT=5000 # 应用监听端口
DEBUG=False # 调试模式(生产环境必须关闭)
APP_DOMAIN=localhost # 应用对外域名(用于邮件链接生成)
APP_PROTOCOL=http # 应用协议http或https
# [注册配置]
REGISTRATION_OPEN=True # 是否开放用户注册True/False
EMAIL_VERIFICATION_REQUIRED=False # 注册是否需要邮箱验证True/False
# [密码策略]
PASSWORD_MIN_LENGTH=6 # 密码最小长度
PASSWORD_REQUIRE_UPPERCASE=False # 是否要求包含大写字母True/False
PASSWORD_REQUIRE_LOWERCASE=False # 是否要求包含小写字母True/False
PASSWORD_REQUIRE_DIGITS=True # 是否要求包含数字True/False
PASSWORD_REQUIRE_SPECIAL=False # 是否要求包含特殊字符True/False
# [邮件配置]
MAIL_SERVER=smtp.qq.com # SMTP服务器地址QQ邮箱为smtp.qq.com
MAIL_PORT=465 # SMTP端口QQ邮箱SSL端口为465
MAIL_USE_SSL=True # 是否使用SSL加密QQ邮箱必须开启
MAIL_USE_TLS=False # 是否使用TLS加密与SSL二选一
MAIL_USERNAME= # 发件邮箱地址
MAIL_PASSWORD= # SMTP授权码非邮箱密码
MAIL_DEFAULT_SENDER_EMAIL= # 默认发件邮箱
MAIL_DEFAULT_SENDER_NAME="Certificate Manager" # 发件人显示名称

BIN
.gitignore vendored Normal file

Binary file not shown.

8
.idea/.gitignore generated vendored
View File

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/certmgr.iml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

28
.idea/deployment.xml generated
View File

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="root@192.168.31.10:22 password">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="root@192.168.31.10:22 password (2)">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
<paths name="root@192.168.31.11:22 password">
<serverdata>
<mappings>
<mapping local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

View File

@ -1,16 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="APScheduler" />
<item index="1" class="java.lang.String" itemvalue="Flask" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated
View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/certmgr.iml" filepath="$PROJECT_DIR$/.idea/certmgr.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

33
Dockerfile Normal file
View File

@ -0,0 +1,33 @@
# 使用官方 Python 镜像Debian 系)
FROM python:3.8-slim
# 设置时区为北京时间
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 安装系统依赖(仅运行时需要的库)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libjpeg-dev \
zlib1g-dev \
libtiff5-dev \
libfreetype6-dev \
liblcms2-dev \
libwebp-dev \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
# 复制应用代码
COPY . .
# 暴露端口
EXPOSE 5000
# 运行命令
CMD ["python", "app.py"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) [2025] [jeazw]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

131
README.md Normal file
View File

@ -0,0 +1,131 @@
# 自签名证书管理系统
![Python Version](https://img.shields.io/badge/Python-3.7+-blue)
![Flask](https://img.shields.io/badge/Flask-2.0+-green)
<div align="center">
<img src="./static/favicon.svg" alt="qilin SSL Logo" width="150">
<p>一款易用的自签证书管理系统</p>
</div>
一个基于Flask的Web应用程序用于管理自签名证书颁发机构(CA)和证书。
**全部代码由DeepSeek生成**
## 功能特性
- **证书颁发机构管理**
- 创建和管理自签名CA
- 查看CA详情及相关证书
- 生成证书吊销列表(CRL)
- 导出CA捆绑包(证书+私钥)
- **证书管理**
- 颁发由您的CA签名的证书
- 管理主题备用名称(SAN)
- 吊销和续订证书
- 支持多种格式导出(PKCS#12, PEM, CRT+KEY)
- **用户管理**
- 带邮箱验证的用户注册
- 基于角色的访问控制(管理员/普通用户)
- 密码策略强制执行
## 系统截图
<div style="display: flex; justify-content: space-between; margin: 20px 0;">
<img src="screenshots/index.png" alt="首页" style="width: 32%; border: 1px solid #ddd; border-radius: 4px;">
<img src="screenshots/ca_detail.png" alt="CA详情页" style="width: 32%; border: 1px solid #ddd; border-radius: 4px;">
<img src="screenshots/cert_detail.png" alt="证书详情页" style="width: 32%; border: 1px solid #ddd; border-radius: 4px;">
</div>
## demo测试账号
https://ssl.liuyan.wang
admin/123456
## 系统要求
- Python 3.7+
- MySQL/MariaDB数据库
- OpenSSL
- 所需Python包(见`requirements.txt`)
## 安装指南
1. 克隆仓库:
```bash
git clone https://github.com/yourusername/certificate-management-system.git
cd certificate-management-system
```
2. 创建并激活虚拟环境:
```bash
python -m venv venv
source venv/bin/activate # Windows系统: venv\Scripts\activate
```
3. 安装依赖:
```bash
pip install -r requirements.txt
```
4. 基于`.env.example`创建`.env`文件并配置:
```ini
DB_HOST=localhost # 数据库服务器IP地址或域名
DB_PORT=3306 # 数据库端口MySQL默认3306
DB_NAME=cert_management # 数据库名称
DB_USER=root # 数据库用户名
DB_PASSWORD=yourpassword # 数据库密码(生产环境建议使用强密码)
SECRET_KEY=your-secret-key-here # Flask应用加密密钥生产环境必须修改建议使用32位随机字符串
MAIL_SERVER=smtp.example.com # SMTP服务器地址QQ邮箱为smtp.qq.com
MAIL_PORT=587 # SMTP端口QQ邮箱SSL端口为465
MAIL_USE_TLS=True # 是否使用SSL加密QQ邮箱必须开启
MAIL_USERNAME=your-email@example.com # 发件邮箱地址
MAIL_PASSWORD=your-email-password # SMTP授权码非邮箱密码
APP_DOMAIN=localhost # 应用对外域名(用于邮件链接生成)
APP_PROTOCOL=https # 应用协议http或https
```
## 运行应用
```bash
python app.py
```
默认情况下,应用将在`http://localhost:5000`可用。
## 使用说明
1. 注册新账户(若注册开放)或直接登录
2. 管理员可:
- 创建和管理CA
- 颁发证书
- 查看系统所有证书
3. 普通用户可:
- 使用自己创建的CA颁发证书
- 管理自己的证书
## API接口
- `/`: 仪表盘
- `/login`, `/logout`: 认证相关
- `/register`: 用户注册
- `/cas`: CA管理
- `/certificates`: 证书管理
- `/download/<filename>`: 文件下载
## 安全注意事项
- 请始终在安全环境中运行本系统
- 妥善保管您的`.env`文件
- 定期备份证书存储和数据库
- 生产环境建议使用HTTPS
## 开源许可
[MIT License](LICENSE)
## 贡献指南
欢迎提交Pull Request。重大改动请先创建Issue讨论。
## 技术支持
如有问题请提交GitHub仓库Issue。

Binary file not shown.

1479
app.py

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,86 @@
import os
from dotenv import load_dotenv
from pathlib import Path
# 先加载环境变量必须在Config类之前
load_dotenv(Path(__file__).parent / '.env', override=True)
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here'
SQLALCHEMY_DATABASE_URI = 'sqlite:///cert_manager.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
CERTS_ROOT = os.path.join(os.path.dirname(__file__), 'certs')
CAPTCHA_ENABLED = True
# Flask 安全配置
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key') # 生产环境必须覆盖
SESSION_COOKIE_SECURE = True # 只允许HTTPS传输
SESSION_COOKIE_HTTPONLY = True
PERMANENT_SESSION_LIFETIME = 3600 # 1小时会话有效期
# 数据库配置 (从环境变量读取)
DB_CONFIG = {
'host': os.getenv('DB_HOST', 'localhost'),
'database': os.getenv('DB_NAME', 'cert_manager'),
'user': os.getenv('DB_USER', 'certmgr'),
'password': os.getenv('DB_PASSWORD', ''),
'port': int(os.getenv('DB_PORT', '3306')),
'charset': 'utf8mb4',
'collation': 'utf8mb4_general_ci',
'autocommit': True
}
# 证书存储路径 (使用Path更安全)
CERT_STORE = Path(os.getenv('CERT_STORE',
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cert_store')))
# 确保证书存储目录存在
if not CERT_STORE.exists():
CERT_STORE.mkdir(mode=0o700, parents=True) # 设置严格权限
# 管理员配置
ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin')
ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', '') # 生产环境必须设置
ADMIN_EMAIL = os.getenv('ADMIN_EMAIL', 'admin@example.com')
# 应用运行配置
APP_HOST = os.getenv('APP_HOST', '0.0.0.0')
APP_PORT = int(os.getenv('APP_PORT', '9875'))
DEBUG = os.getenv('DEBUG', 'False') == 'True' # 生产环境应为False
# 注册相关配置
REGISTRATION_OPEN = os.getenv('REGISTRATION_OPEN', 'False') == 'True'
EMAIL_VERIFICATION_REQUIRED = os.getenv('EMAIL_VERIFICATION_REQUIRED', 'True') == 'True'
# 密码策略配置
PASSWORD_POLICY = {
'min_length': int(os.getenv('PASSWORD_MIN_LENGTH', '8')),
'require_uppercase': os.getenv('PASSWORD_REQUIRE_UPPERCASE', 'True') == 'True',
'require_lowercase': os.getenv('PASSWORD_REQUIRE_LOWERCASE', 'True') == 'True',
'require_digits': os.getenv('PASSWORD_REQUIRE_DIGITS', 'True') == 'True',
'require_special_chars': os.getenv('PASSWORD_REQUIRE_SPECIAL', 'True') == 'True'
}
# 邮件服务器配置
MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.qq.com')
MAIL_PORT = int(os.getenv('MAIL_PORT', '465'))
MAIL_USE_SSL = os.getenv('MAIL_USE_SSL', 'True') == 'True'
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'False') == 'True'
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = (
os.getenv('MAIL_DEFAULT_SENDER_EMAIL', 'noreply@example.com'),
os.getenv('MAIL_DEFAULT_SENDER_NAME', 'Certificate Manager')
)
# 应用URL配置
APP_DOMAIN = os.getenv('APP_DOMAIN', 'xunxian.liuyan.wang')
APP_PROTOCOL = os.getenv('APP_PROTOCOL', 'https')
SERVER_NAME = os.getenv('SERVER_NAME') # 用于URL生成
# 日志配置
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
LOG_FILE = os.getenv('LOG_FILE', 'app.log')
@property
def SQLALCHEMY_DATABASE_URI(self):
return f"mysql+pymysql://{self.DB_CONFIG['user']}:{self.DB_CONFIG['password']}@" \
f"{self.DB_CONFIG['host']}:{self.DB_CONFIG['port']}/{self.DB_CONFIG['database']}"
@property
def BASE_URL(self):
return f"{self.APP_PROTOCOL}://{self.APP_DOMAIN}"

156
database.py Normal file
View File

@ -0,0 +1,156 @@
# database.py
import mysql.connector
from mysql.connector import Error
from config import Config
def create_database():
"""创建数据库(如果不存在)"""
try:
# 连接到MySQL服务器不带数据库名
conn = mysql.connector.connect(
host=Config.DB_CONFIG['host'],
user=Config.DB_CONFIG['user'],
password=Config.DB_CONFIG['password']
)
cursor = conn.cursor()
# 创建数据库
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {Config.DB_CONFIG['database']}")
print(f"数据库 {Config.DB_CONFIG['database']} 已创建或已存在")
# 切换到新数据库
cursor.execute(f"USE {Config.DB_CONFIG['database']}")
# 创建表
sql_scripts = [
"""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(128) NOT NULL,
email VARCHAR(100),
is_admin BOOLEAN DEFAULT FALSE,
is_active tinyint(1) DEFAULT '1',
verification_token VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS certificate_authorities (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
common_name VARCHAR(100) NOT NULL,
organization VARCHAR(100),
organizational_unit VARCHAR(100),
country VARCHAR(2),
state VARCHAR(100),
locality VARCHAR(100),
key_size INT DEFAULT 2048,
days_valid INT DEFAULT 3650,
cert_path VARCHAR(255),
key_path VARCHAR(255),
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
)
""",
"""
CREATE TABLE IF NOT EXISTS certificates (
id INT AUTO_INCREMENT PRIMARY KEY,
common_name VARCHAR(100) NOT NULL,
san_dns TEXT,
san_ip TEXT,
organization VARCHAR(100),
organizational_unit VARCHAR(100),
country VARCHAR(2),
state VARCHAR(100),
locality VARCHAR(100),
key_size INT DEFAULT 2048,
days_valid INT DEFAULT 365,
cert_path VARCHAR(255),
key_path VARCHAR(255),
csr_path VARCHAR(255),
ca_id INT,
status ENUM('active', 'revoked', 'expired') DEFAULT 'active',
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
revoked_at TIMESTAMP NULL,
revocation_reason VARCHAR(255),
FOREIGN KEY (ca_id) REFERENCES certificate_authorities(id),
FOREIGN KEY (created_by) REFERENCES users(id)
)
""",
"""
CREATE TABLE IF NOT EXISTS certificate_revocation_list (
id INT AUTO_INCREMENT PRIMARY KEY,
ca_id INT NOT NULL,
crl_path VARCHAR(255),
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
next_update TIMESTAMP NULL,
FOREIGN KEY (ca_id) REFERENCES certificate_authorities(id)
)
""",
"""
CREATE TABLE IF NOT EXISTS captcha (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(10) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
]
for script in sql_scripts:
cursor.execute(script)
conn.commit()
print("所有表已创建或已存在")
except Error as e:
print(f"数据库初始化错误: {e}")
raise
finally:
if conn.is_connected():
cursor.close()
conn.close()
def create_admin_user():
"""创建初始管理员用户"""
from werkzeug.security import generate_password_hash
try:
conn = mysql.connector.connect(**Config.DB_CONFIG)
cursor = conn.cursor(dictionary=True)
# 检查管理员是否已存在
cursor.execute("SELECT id FROM users WHERE username = %s", (Config.ADMIN_USERNAME,))
if cursor.fetchone():
print("管理员用户已存在")
return
# 创建管理员
password_hash = generate_password_hash(Config.ADMIN_PASSWORD)
cursor.execute("""
INSERT INTO users (username, password_hash, email, is_admin, is_active)
VALUES (%s, %s, %s, %s, %s)
""", (Config.ADMIN_USERNAME, password_hash, Config.ADMIN_EMAIL, True, True))
conn.commit()
print("管理员用户创建成功")
except Error as e:
print(f"创建管理员用户错误: {e}")
raise
finally:
if conn.is_connected():
cursor.close()
conn.close()
def initialize_database():
"""初始化数据库"""
create_database()
create_admin_user()

13
docker-compose.yml Normal file
View File

@ -0,0 +1,13 @@
version: '3.8'
services:
tiku_bm:
image: certmanager:latest
container_name: certmanager
ports:
- "5002:5000"
environment:
- FLASK_ENV=production
volumes:
- ./data:/app/data
restart: unless-stopped

View File

@ -2,4 +2,7 @@ Flask==2.0.1
Flask-Login==0.5.0
mysql-connector-python==8.0.26
python-dotenv==0.19.0
cryptography==2.0
cryptography==2.0
pypinyin==0.51.0
flask_migrate==4.0.7
flask_mail==0.10.0

BIN
screenshots/ca_detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
screenshots/cert_detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
screenshots/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
static/arial.ttf Normal file

Binary file not shown.

1
static/favicon.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1749894447194" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4365" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M640 768.042667c35.626667 26.794667 80 42.666667 128 42.666666a212.394667 212.394667 0 0 0 128-42.581333v138.496a32 32 0 0 1-46.506667 28.544l-3.84-2.346667L768 878.549333l-77.653333 54.272a32 32 0 0 1-50.005334-21.76l-0.298666-4.437333L640 768z m181.333333-639.872a117.333333 117.333333 0 0 1 117.12 110.208l0.213334 7.125333 0.042666 223.829333a214.442667 214.442667 0 0 0-64-56.746666L874.666667 245.461333a53.333333 53.333333 0 0 0-47.872-53.034666l-5.461334-0.298667H202.666667a53.333333 53.333333 0 0 0-53.077334 47.872l-0.256 5.461333v405.333334c0 27.605333 20.992 50.346667 47.872 53.077333l5.461334 0.256h380.586666c4.266667 7.338667 8.96 14.421333 14.08 21.205333v42.794667H202.666667a117.333333 117.333333 0 0 1-117.12-110.165333L85.333333 650.837333v-405.333333A117.333333 117.333333 0 0 1 195.498667 128.426667l7.168-0.213334h618.666666zM768 426.666667a170.666667 170.666667 0 1 1 0 341.376A170.666667 170.666667 0 0 1 768 426.666667z m-288 106.666666a32 32 0 0 1 4.352 63.701334L480 597.333333h-192a32 32 0 0 1-4.352-63.701333l4.352-0.298667h192z m256-234.666666a32 32 0 0 1 4.352 63.701333l-4.352 0.298667H288a32 32 0 0 1-4.352-63.701334L288 298.666667h448z" fill="#212121" p-id="4366"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -3,53 +3,164 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>证书管理系统 - {% block title %}{% endblock %}</title>
<title>自签证书管理系统 - {% block title %}{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
flex: 1;
padding-top: 20px;
padding-bottom: 40px;
}
.footer {
background-color: #f8f9fa;
padding: 20px 0;
margin-top: auto;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
background-color: rgb(124 146 157) !important;
}
.nav-item.active {
position: relative;
}
.nav-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 50%;
height: 3px;
background-color: #333333;
border-radius: 3px 3px 0 0;
}
.nav-item.active .nav-link {
font-weight: 500;
color: #333333 !important;
}
</style>
{% block styles %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">证书管理系统</a>
<a class="navbar-brand fw-bold" href="{{ url_for('index') }}">
<i class="fas fa-shield-alt me-2"></i>自签证书管理系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('ca_list') }}">CA机构</a>
<li class="nav-item {% if request.path.startswith('/cas') %}active{% endif %}">
<a class="nav-link" href="{{ url_for('ca_list') }}">
<i class="fas fa-shield-alt me-1"></i>CA机构
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('certificate_list') }}">证书</a>
<li class="nav-item {% if request.path.startswith('/certificates') %}active{% endif %}">
<a class="nav-link" href="{{ url_for('certificate_list') }}">
<i class="fas fa-certificate me-1"></i>证书
</a>
</li>
</ul>
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user-circle me-1"></i>{{ current_user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
{% if current_user.is_admin %}
<li><span class="dropdown-item-text text-muted small">管理员</span></li>
{% else %}
<li><span class="dropdown-item-text text-muted small">普通用户</span></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt me-2"></i>退出登录
</a>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<span class="navbar-text me-3">欢迎, {{ current_user.username }}</span>
<a class="nav-link" href="{{ url_for('login') }}">
<i class="fas fa-sign-in-alt me-1"></i>登录
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">退出</a>
<a class="nav-link" href="{{ url_for('register') }}">
<i class="fas fa-user-plus me-1"></i>注册
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
<div class="main-content">
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show mt-3" role="alert">
<div class="d-flex align-items-center">
{% if category == 'success' %}
<i class="fas fa-check-circle me-2"></i>
{% elif category == 'danger' %}
<i class="fas fa-exclamation-circle me-2"></i>
{% elif category == 'warning' %}
<i class="fas fa-exclamation-triangle me-2"></i>
{% else %}
<i class="fas fa-info-circle me-2"></i>
{% endif %}
<div>{{ message }}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
{% block content %}{% endblock %}
</div>
</div>
<footer class="footer">
<div class="container text-center text-muted">
<small>
<p class="mb-1">自签证书管理系统 &copy; {{ now.year }} - 基于Flask构建</p>
<p class="mb-0">版本 7.1.0</p>
</small>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
<script>
// 高亮当前导航项
document.addEventListener('DOMContentLoaded', function() {
// 初始化工具提示
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// 初始化弹出框
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl);
});
});
</script>
</body>
</html>

View File

@ -4,39 +4,67 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>CA机构详情: {{ ca.name }}</h2>
<div>
<a href="{{ url_for('generate_crl_view', ca_id=ca.id) }}" class="btn btn-warning me-2">生成CRL</a>
<h2 class="mb-1">CA机构详情: {{ ca.name }}</h2>
<div class="text-muted fs-6">
<i class="fas fa-folder-open me-1"></i>路径: {{ ca.name|to_pinyin }}
</div>
</div>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('export_ca_view', ca_id=ca.id) }}"
class="btn btn-primary me-1 text-nowrap"
data-bs-toggle="tooltip"
title="导出CA证书(管理员可导出私钥)">
<i class="fas fa-file-export me-1"></i> 导出CA
</a>
<a href="{{ url_for('generate_crl_view', ca_id=ca.id) }}"
class="btn btn-warning me-1 text-nowrap"
data-bs-toggle="tooltip"
title="重新生成证书吊销列表">
<i class="fas fa-sync-alt me-1"></i> 生成CRL
</a>
{% if crl %}
<a href="{{ url_for('download_crl', ca_id=ca.id) }}" class="btn btn-success">下载CRL</a>
<a href="{{ url_for('download_crl', ca_id=ca.id) }}"
class="btn btn-success me-1 text-nowrap"
data-bs-toggle="tooltip"
title="下载当前CRL文件(有效期至 {{ crl.next_update.strftime('%Y-%m-%d') }})">
<i class="fas fa-download me-1"></i> 下载CRL
</a>
{% endif %}
<a href="{{ url_for('delete_ca', ca_id=ca.id) }}"
class="btn btn-danger text-nowrap"
data-bs-toggle="tooltip"
title="删除此CA(谨慎操作)">
<i class="fas fa-trash-alt me-1"></i> 删除CA
</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">基本信息</h5>
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle text-primary me-2"></i> 基本信息
</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">通用名</dt>
<dl class="row mb-0">
<dt class="col-sm-4 text-muted"><i class="fas fa-id-card me-2"></i> 通用名</dt>
<dd class="col-sm-8">{{ ca.common_name }}</dd>
<dt class="col-sm-4">组织</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-building me-2"></i> 组织</dt>
<dd class="col-sm-8">{{ ca.organization }}</dd>
<dt class="col-sm-4">组织单位</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-users me-2"></i> 组织单位</dt>
<dd class="col-sm-8">{{ ca.organizational_unit or 'N/A' }}</dd>
<dt class="col-sm-4">国家</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-globe me-2"></i> 国家</dt>
<dd class="col-sm-8">{{ ca.country }}</dd>
<dt class="col-sm-4">州/省</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-map-marked me-2"></i> 州/省</dt>
<dd class="col-sm-8">{{ ca.state or 'N/A' }}</dd>
<dt class="col-sm-4">城市</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-city me-2"></i> 城市</dt>
<dd class="col-sm-8">{{ ca.locality or 'N/A' }}</dd>
</dl>
</div>
@ -45,80 +73,173 @@
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">技术信息</h5>
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-cogs text-primary me-2"></i> 技术信息
</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">密钥长度</dt>
<dl class="row mb-0">
<dt class="col-sm-4 text-muted"><i class="fas fa-key me-2"></i> 密钥长度</dt>
<dd class="col-sm-8">{{ ca.key_size }}位</dd>
<dt class="col-sm-4">有效期</dt>
<dd class="col-sm-8">{{ ca.days_valid }}天</dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-calendar-check me-2"></i> 有效期</dt>
<dd class="col-sm-8">
{{ ca.days_valid }}天
{% if ca.created_at %}
(至 {{ (ca.created_at + timedelta(days=ca.days_valid)).strftime('%Y-%m-%d') }})
{% endif %}
</dd>
<dt class="col-sm-4">创建者</dt>
<dd class="col-sm-8">{{ ca.created_by }}</dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-user me-2"></i> 创建者</dt>
<dd class="col-sm-8">{{ get_username(ca.created_by) }}</dd>
<dt class="col-sm-4">创建时间</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-clock me-2"></i> 创建时间</dt>
<dd class="col-sm-8">{{ ca.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
<dt class="col-sm-4">证书路径</dt>
<dd class="col-sm-8"><code>{{ ca.cert_path }}</code></dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-certificate me-1"></i> 证书路径</dt>
<dd class="col-sm-8">
<code>{{ ca.cert_path|to_pinyin }}</code>
</dd>
<dt class="col-sm-4">私钥路径</dt>
<dd class="col-sm-8"><code>{{ ca.key_path }}</code></dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-lock me-2"></i> 私钥路径</dt>
<dd class="col-sm-8">
<code>{{ ca.key_path|to_pinyin }}</code>
</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- 证书列表部分 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">颁发的证书</h5>
<a href="{{ url_for('create_certificate_view') }}?ca_id={{ ca.id }}" class="btn btn-sm btn-primary">创建证书</a>
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-certificate text-primary me-2"></i> 颁发的证书
<span class="badge bg-primary rounded-pill ms-2">{{ total }}</span>
</h5>
<div>
<a href="{{ url_for('create_certificate_view') }}?ca_id={{ ca.id }}"
class="btn btn-sm btn-primary"
data-bs-toggle="tooltip"
title="使用此CA颁发新证书">
<i class="fas fa-plus me-1"></i> 创建证书
</a>
</div>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if certificates %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th class="ps-4">ID</th>
<th>通用名</th>
<th>状态</th>
<th>有效期至</th>
<th>创建者</th>
<th>创建时间</th>
<th>操作</th>
<th class="pe-4">操作</th>
</tr>
</thead>
<tbody>
{% for cert in certificates %}
<tr>
<td>{{ cert.id }}</td>
<td>{{ cert.common_name }}</td>
<td class="ps-4">{{ cert.id }}</td>
<td>
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="text-decoration-none">
{{ cert.common_name }}
</a>
</td>
<td>
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
<span class="badge bg-success rounded-pill">
<i class="fas fa-check-circle me-1"></i> 有效
</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
<span class="badge bg-danger rounded-pill">
<i class="fas fa-ban me-1"></i> 已吊销
</span>
{% else %}
<span class="badge bg-secondary">已过期</span>
<span class="badge bg-secondary rounded-pill">
<i class="fas fa-clock me-1"></i> 已过期
</span>
{% endif %}
</td>
<td>{{ cert.expires_at.strftime('%Y-%m-%d') }}</td>
<td>{{ get_username(cert.created_by) }}</td>
<td>{{ cert.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-sm btn-info">详情</a>
<td class="pe-4">
<div class="btn-group btn-group-sm">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}"
class="btn btn-outline-primary"
data-bs-toggle="tooltip"
title="查看证书详情">
<i class="fas fa-eye me-1"></i> 详情
</a>
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}"
class="btn btn-outline-success"
data-bs-toggle="tooltip"
title="导出证书">
<i class="fas fa-download me-1"></i> 导出
</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center">该CA尚未颁发任何证书</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('ca_detail', ca_id=ca.id, page=page-1) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('ca_detail', ca_id=ca.id, page=p) }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('ca_detail', ca_id=ca.id, page=page+1) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-certificate fa-4x text-muted mb-4"></i>
<h5 class="text-muted mb-3">该CA尚未颁发任何证书</h5>
<a href="{{ url_for('create_certificate_view') }}?ca_id={{ ca.id }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> 立即创建证书
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 启用工具提示
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
})
</script>
{% endblock %}

View File

@ -4,44 +4,182 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>CA机构列表</h2>
<a href="{{ url_for('create_ca_view') }}" class="btn btn-primary">创建CA机构</a>
<div>
<h2 class="mb-1">CA机构列表</h2>
<div class="text-muted fs-6">
<i class="fas fa-shield-alt me-1"></i>共 {{ total }} 个CA机构
</div>
</div>
<div>
<button id="batchDeleteBtn" class="btn btn-danger btn-sm me-2" disabled>
<i class="fas fa-trash-alt me-1"></i> 批量删除
</button>
<a href="{{ url_for('create_ca_view') }}" class="btn btn-primary btn-sm text-nowrap">
<i class="fas fa-plus me-1"></i> 创建CA机构
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>名称</th>
<th>通用名</th>
<th>组织</th>
<th>有效期(天)</th>
<th>创建者</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for ca in cas %}
<tr>
<td>{{ ca.id }}</td>
<td>{{ ca.name }}</td>
<td>{{ ca.common_name }}</td>
<td>{{ ca.organization }}</td>
<td>{{ ca.days_valid }}</td>
<td>{{ ca.created_by }}</td>
<td>{{ ca.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}" class="btn btn-sm btn-info">详情</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center">暂无CA机构</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="40" class="ps-4">
<input type="checkbox" id="selectAll" class="form-check-input">
</th>
<th>ID</th>
<th>名称</th>
<th>通用名</th>
<th>组织</th>
<th>有效期</th>
<th>创建者</th>
<th>创建时间</th>
<th class="pe-4">操作</th>
</tr>
</thead>
<tbody>
{% for ca in cas %}
<tr>
<td class="ps-4">
<input type="checkbox" class="form-check-input ca-checkbox" value="{{ ca.id }}">
</td>
<td>{{ ca.id }}</td>
<td>
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}" class="text-decoration-none">
{{ ca.name }}
</a>
</td>
<td>{{ ca.common_name }}</td>
<td>{{ ca.organization }}</td>
<td>{{ ca.days_valid }}天</td>
<td>{{ get_username(ca.created_by) }}</td>
<td>{{ ca.created_at.strftime('%Y-%m-%d') }}</td>
<td class="pe-4">
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="tooltip"
title="查看CA详情">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="9" class="text-center py-4">
<i class="fas fa-shield-alt fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无CA机构记录</p>
<a href="{{ url_for('create_ca_view') }}" class="btn btn-primary btn-sm">
<i class="fas fa-plus me-2"></i> 创建CA机构
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('ca_list', page=page-1) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('ca_list', page=p) }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('ca_list', page=page+1) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 启用工具提示
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// 全选/取消全选
const selectAll = document.getElementById('selectAll')
const checkboxes = document.querySelectorAll('.ca-checkbox')
const batchDeleteBtn = document.getElementById('batchDeleteBtn')
selectAll.addEventListener('change', function() {
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked
})
updateBatchDeleteBtn()
})
// 单个复选框变化时更新全选状态
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
selectAll.checked = [...checkboxes].every(cb => cb.checked)
updateBatchDeleteBtn()
})
})
// 更新批量删除按钮状态
function updateBatchDeleteBtn() {
const checkedCount = document.querySelectorAll('.ca-checkbox:checked').length
batchDeleteBtn.disabled = checkedCount === 0
}
// 批量删除
batchDeleteBtn.addEventListener('click', function() {
const checkedBoxes = document.querySelectorAll('.ca-checkbox:checked')
const ids = Array.from(checkedBoxes).map(checkbox => checkbox.value)
if (ids.length === 0) {
return
}
if (!confirm(`确定要删除选中的 ${ids.length} 个CA机构吗`)) {
return
}
fetch("{{ url_for('batch_delete_cas') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ ids: ids })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message)
window.location.reload()
} else {
alert(data.message)
}
})
.catch(error => {
console.error('Error:', error)
alert('操作失败,请稍后再试')
})
})
})
</script>
{% endblock %}

View File

@ -4,43 +4,75 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>证书详情: {{ cert.common_name }}</h2>
<div>
<h2 class="mb-1">证书详情: {{ cert.common_name }}</h2>
<div class="text-muted fs-6">
<i class="fas fa-folder-open me-1"></i>路径: {{ cert.cert_path|to_pinyin }}
</div>
</div>
<div class="btn-group btn-group-sm">
{% if cert.status == 'active' %}
<a href="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}" class="btn btn-warning me-2">吊销证书</a>
<a href="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}"
class="btn btn-warning me-1 text-nowrap"
data-bs-toggle="tooltip"
title="吊销此证书">
<i class="fas fa-ban me-1"></i> 吊销证书
</a>
{% endif %}
<a href="{{ url_for('renew_certificate_view', cert_id=cert.id) }}" class="btn btn-primary me-2">续期</a>
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}" class="btn btn-success">导出</a>
<a href="{{ url_for('renew_certificate_view', cert_id=cert.id) }}"
class="btn btn-primary me-1 text-nowrap"
data-bs-toggle="tooltip"
title="续期此证书">
<i class="fas fa-sync-alt me-1"></i> 续期
</a>
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}"
class="btn btn-success me-1 text-nowrap"
data-bs-toggle="tooltip"
title="导出证书文件">
<i class="fas fa-file-export me-1"></i> 导出
</a>
<a href="{{ url_for('delete_certificate', cert_id=cert.id) }}"
class="btn btn-danger text-nowrap"
data-bs-toggle="tooltip"
title="删除此证书(谨慎操作)">
<i class="fas fa-trash-alt me-1"></i> 删除
</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">基本信息</h5>
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle text-primary me-2"></i> 基本信息
</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">通用名</dt>
<dl class="row mb-0">
<dt class="col-sm-4 text-muted"><i class="fas fa-id-card me-2"></i> 通用名</dt>
<dd class="col-sm-8">{{ cert.common_name }}</dd>
<dt class="col-sm-4">颁发CA</dt>
<dd class="col-sm-8">{{ ca.name }}</dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-shield-alt me-2"></i> 颁发CA</dt>
<dd class="col-sm-8">
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}" class="text-decoration-none">
{{ ca.name }}
</a>
</dd>
<dt class="col-sm-4">组织</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-building me-2"></i> 组织</dt>
<dd class="col-sm-8">{{ cert.organization }}</dd>
<dt class="col-sm-4">组织单位</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-users me-2"></i> 组织单位</dt>
<dd class="col-sm-8">{{ cert.organizational_unit or 'N/A' }}</dd>
<dt class="col-sm-4">国家</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-globe me-2"></i> 国家</dt>
<dd class="col-sm-8">{{ cert.country }}</dd>
<dt class="col-sm-4">州/省</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-map-marked me-2"></i> 州/省</dt>
<dd class="col-sm-8">{{ cert.state or 'N/A' }}</dd>
<dt class="col-sm-4">城市</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-city me-2"></i> 城市</dt>
<dd class="col-sm-8">{{ cert.locality or 'N/A' }}</dd>
</dl>
</div>
@ -49,44 +81,52 @@
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">技术信息</h5>
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-cogs text-primary me-2"></i> 技术信息
</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">密钥长度</dt>
<dl class="row mb-0">
<dt class="col-sm-4 text-muted"><i class="fas fa-key me-2"></i> 密钥长度</dt>
<dd class="col-sm-8">{{ cert.key_size }}位</dd>
<dt class="col-sm-4">有效期</dt>
<dd class="col-sm-8">{{ cert.days_valid }}天</dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-calendar-check me-2"></i> 有效期</dt>
<dd class="col-sm-8">
{{ cert.days_valid }}天
(至 {{ cert.expires_at.strftime('%Y-%m-%d') }})
</dd>
<dt class="col-sm-4">状态</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-check-circle me-2"></i> 状态</dt>
<dd class="col-sm-8">
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
<span class="badge bg-success rounded-pill">
<i class="fas fa-check-circle me-1"></i> 有效
</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
<span class="badge bg-danger rounded-pill">
<i class="fas fa-ban me-1"></i> 已吊销
</span>
{% else %}
<span class="badge bg-secondary">已过期</span>
<span class="badge bg-secondary rounded-pill">
<i class="fas fa-clock me-1"></i> 已过期
</span>
{% endif %}
</dd>
{% if cert.status == 'revoked' %}
<dt class="col-sm-4">吊销原因</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-comment me-2"></i> 吊销原因</dt>
<dd class="col-sm-8">{{ cert.revocation_reason or '未指定' }}</dd>
<dt class="col-sm-4">吊销时间</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-clock me-2"></i> 吊销时间</dt>
<dd class="col-sm-8">{{ cert.revoked_at.strftime('%Y-%m-%d %H:%M') }}</dd>
{% endif %}
<dt class="col-sm-4">创建者</dt>
<dd class="col-sm-8">{{ cert.created_by }}</dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-user me-2"></i> 创建者</dt>
<dd class="col-sm-8">{{ get_username(cert.created_by) }}</dd>
<dt class="col-sm-4">创建时间</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-clock me-2"></i> 创建时间</dt>
<dd class="col-sm-8">{{ cert.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
<dt class="col-sm-4">过期时间</dt>
<dd class="col-sm-8">{{ cert.expires_at.strftime('%Y-%m-%d %H:%M') }}</dd>
</dl>
</div>
</div>
@ -95,54 +135,71 @@
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">SAN扩展</h5>
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-list-alt text-primary me-2"></i> SAN扩展
</h5>
</div>
<div class="card-body">
{% if cert.san_dns or cert.san_ip %}
{% if cert.san_dns %}
<h6>DNS名称:</h6>
<ul>
<h6><i class="fas fa-globe me-2"></i>DNS名称:</h6>
<ul class="list-unstyled ms-4">
{% for dns in cert.san_dns.split(',') %}
<li>{{ dns }}</li>
<li><i class="fas fa-angle-right text-muted me-2"></i>{{ dns }}</li>
{% endfor %}
</ul>
{% endif %}
{% if cert.san_ip %}
<h6>IP地址:</h6>
<ul>
<h6><i class="fas fa-network-wired me-2"></i>IP地址:</h6>
<ul class="list-unstyled ms-4">
{% for ip in cert.san_ip.split(',') %}
<li>{{ ip }}</li>
<li><i class="fas fa-angle-right text-muted me-2"></i>{{ ip }}</li>
{% endfor %}
</ul>
{% endif %}
{% else %}
<p>未配置SAN扩展</p>
<p class="text-muted mb-0"><i class="fas fa-info-circle me-2"></i>未配置SAN扩展</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">文件路径</h5>
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-folder-open text-primary me-2"></i> 文件路径
</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">证书文件</dt>
<dl class="row mb-0">
<dt class="col-sm-4 text-muted"><i class="fas fa-certificate me-2"></i> 证书文件</dt>
<dd class="col-sm-8"><code>{{ cert.cert_path }}</code></dd>
<dt class="col-sm-4">私钥文件</dt>
<dt class="col-sm-4 text-muted"><i class="fas fa-key me-2"></i> 私钥文件</dt>
<dd class="col-sm-8"><code>{{ cert.key_path }}</code></dd>
<dt class="col-sm-4">CSR文件</dt>
<dd class="col-sm-8"><code>{{ cert.csr_path }}</code></dd>
<dt class="col-sm-4 text-muted"><i class="fas fa-file-signature me-2"></i> CSR文件</dt>
<dd class="col-sm-8"><code>{{ cert.csr_path or 'N/A' }}</code></dd>
</dl>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 启用工具提示
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
})
</script>
{% endblock %}

View File

@ -4,56 +4,212 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>证书列表</h2>
<a href="{{ url_for('create_certificate_view') }}" class="btn btn-primary">创建证书</a>
<div>
<h2 class="mb-1">证书列表</h2>
<div class="text-muted fs-6">
<i class="fas fa-certificate me-1"></i>共 {{ total }} 个证书
</div>
</div>
<div>
<button id="batchDeleteBtn" class="btn btn-danger btn-sm me-2" disabled>
<i class="fas fa-trash-alt me-1"></i> 批量删除
</button>
<a href="{{ url_for('create_certificate_view') }}" class="btn btn-primary btn-sm text-nowrap">
<i class="fas fa-plus me-1"></i> 创建证书
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>通用名</th>
<th>CA机构</th>
<th>状态</th>
<th>有效期至</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for cert in certificates %}
<tr>
<td>{{ cert.id }}</td>
<td>{{ cert.common_name }}</td>
<td>{{ cert.ca_name }}</td>
<td>
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="40" class="ps-4">
<input type="checkbox" id="selectAll" class="form-check-input">
</th>
<th>ID</th>
<th>通用名</th>
<th>CA机构</th>
<th>状态</th>
<th>有效期至</th>
<th>创建者</th>
<th>创建时间</th>
<th class="pe-4">操作</th>
</tr>
</thead>
<tbody>
{% for cert in certificates %}
<tr>
<td class="ps-4">
<input type="checkbox" class="form-check-input cert-checkbox" value="{{ cert.id }}">
</td>
<td>{{ cert.id }}</td>
<td>
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="text-decoration-none">
{{ cert.common_name }}
</a>
</td>
<td>{{ cert.ca_name }}</td>
<td>
{% if cert.status == 'active' %}
<span class="badge bg-success rounded-pill">
<i class="fas fa-check-circle me-1"></i> 有效
</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger rounded-pill">
<i class="fas fa-ban me-1"></i> 已吊销
</span>
{% else %}
<span class="badge bg-secondary rounded-pill">
<i class="fas fa-clock me-1"></i> 已过期
</span>
{% endif %}
</td>
<td>{{ cert.expires_at.strftime('%Y-%m-%d') }}</td>
<td>{{ get_username(cert.created_by) }}</td>
<td>{{ cert.created_at.strftime('%Y-%m-%d') }}</td>
<td class="pe-4">
<div class="btn-group btn-group-sm">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}"
class="btn btn-outline-primary"
data-bs-toggle="tooltip"
title="查看详情">
<i class="fas fa-eye"></i>
</a>
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}"
class="btn btn-outline-success"
data-bs-toggle="tooltip"
title="导出证书">
<i class="fas fa-download"></i>
</a>
{% if cert.status == 'active' %}
<a href="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}"
class="btn btn-outline-warning"
data-bs-toggle="tooltip"
title="吊销证书">
<i class="fas fa-ban"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% else %}
<span class="badge bg-secondary">已过期</span>
{% endif %}
</td>
<td>{{ cert.expires_at.strftime('%Y-%m-%d') }}</td>
<td>{{ cert.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-info">详情</a>
{% if cert.status == 'active' %}
<a href="{{ url_for('revoke_certificate_view', cert_id=cert.id) }}" class="btn btn-warning">吊销</a>
{% endif %}
<a href="{{ url_for('export_certificate_view', cert_id=cert.id) }}" class="btn btn-success">导出</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center">暂无证书</td>
</tr>
{% endfor %}
</tbody>
</table>
<tr>
<td colspan="8" class="text-center py-4">
<i class="fas fa-certificate fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无证书记录</p>
<a href="{{ url_for('create_certificate_view') }}" class="btn btn-primary btn-sm">
<i class="fas fa-plus me-2"></i> 创建证书
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
<div class="card-footer bg-white">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<li class="page-item {% if page == 1 %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('certificate_list', page=page-1) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="{{ url_for('certificate_list', page=p) }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page == total_pages %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('certificate_list', page=page+1) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// 启用工具提示
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// 全选/取消全选
const selectAll = document.getElementById('selectAll')
const checkboxes = document.querySelectorAll('.cert-checkbox')
const batchDeleteBtn = document.getElementById('batchDeleteBtn')
selectAll.addEventListener('change', function() {
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked
})
updateBatchDeleteBtn()
})
// 单个复选框变化时更新全选状态
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
selectAll.checked = [...checkboxes].every(cb => cb.checked)
updateBatchDeleteBtn()
})
})
// 更新批量删除按钮状态
function updateBatchDeleteBtn() {
const checkedCount = document.querySelectorAll('.cert-checkbox:checked').length
batchDeleteBtn.disabled = checkedCount === 0
}
// 批量删除
batchDeleteBtn.addEventListener('click', function() {
const checkedBoxes = document.querySelectorAll('.cert-checkbox:checked')
const ids = Array.from(checkedBoxes).map(checkbox => checkbox.value)
if (ids.length === 0) {
return
}
if (!confirm(`确定要删除选中的 ${ids.length} 个证书吗?`)) {
return
}
fetch("{{ url_for('batch_delete_certificates') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ ids: ids })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message)
window.location.reload()
} else {
alert(data.message)
}
})
.catch(error => {
console.error('Error:', error)
alert('操作失败,请稍后再试')
})
})
})
</script>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}删除CA - {{ ca.name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h4><i class="fas fa-exclamation-triangle me-2"></i> 确认删除CA</h4>
</div>
<div class="card-body">
<h5 class="card-title">您确定要删除以下CA吗</h5>
<div class="alert alert-danger">
<strong>警告:</strong>此操作不可逆所有与此CA相关的文件将被永久删除。
</div>
<div class="mb-4">
<h6>CA信息</h6>
<ul>
<li>名称: {{ ca.name }}</li>
<li>通用名: {{ ca.common_name }}</li>
<li>组织: {{ ca.organization }}</li>
<li>创建时间: {{ ca.created_at.strftime('%Y-%m-%d %H:%M') }}</li>
</ul>
</div>
<form method="POST" action="{{ url_for('delete_ca', ca_id=ca.id) }}">
<div class="d-flex justify-content-between">
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i> 取消
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash-alt me-1"></i> 确认删除
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}删除证书 - {{ cert.common_name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h4><i class="fas fa-exclamation-triangle me-2"></i> 确认删除证书</h4>
</div>
<div class="card-body">
<h5 class="card-title">您确定要删除以下证书吗?</h5>
<div class="alert alert-danger">
<strong>警告:</strong>此操作不可逆!所有与此证书相关的文件将被永久删除。
</div>
<div class="mb-4">
<h6>证书信息:</h6>
<ul>
<li>通用名: {{ cert.common_name }}</li>
<li>颁发CA: {{ cert.ca_name }}</li>
<li>状态:
{% if cert.status == 'active' %}
<span class="badge bg-success">有效</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger">已吊销</span>
{% else %}
<span class="badge bg-secondary">已过期</span>
{% endif %}
</li>
<li>创建时间: {{ cert.created_at.strftime('%Y-%m-%d %H:%M') }}</li>
</ul>
</div>
<form method="POST" action="{{ url_for('delete_certificate', cert_id=cert.id) }}">
<div class="d-flex justify-content-between">
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i> 取消
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash-alt me-1"></i> 确认删除
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -12,12 +12,16 @@
<div class="row mb-3">
<div class="col-md-6">
<label for="ca_name" class="form-label">CA名称</label>
<input type="text" class="form-control" id="ca_name" name="ca_name" required>
<input type="text" class="form-control" id="ca_name" name="ca_name"
required pattern="[a-zA-Z0-9_\-\u4e00-\u9fa5]+"
title="只能包含中文、字母、数字、下划线和短横线,且不能以短横线开头或结尾">
<div class="form-text">CA机构的显示名称</div>
</div>
<div class="col-md-6">
<label for="common_name" class="form-label">通用名(CN)</label>
<input type="text" class="form-control" id="common_name" name="common_name" required>
<input type="text" class="form-control" id="common_name" name="common_name"
required pattern="[a-zA-Z0-9.-]+"
title="只能包含字母、数字、点号和短横线,且不能以点号或短横线开头或结尾">
<div class="form-text">证书的Common Name字段</div>
</div>
</div>

View File

@ -8,11 +8,13 @@
<h4 class="card-title">创建新证书</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('create_certificate_view') }}">
<form method="POST" action="{{ url_for('create_certificate_view') }}" id="certificateForm">
<div class="row mb-3">
<div class="col-md-6">
<label for="common_name" class="form-label">通用名(CN)</label>
<input type="text" class="form-control" id="common_name" name="common_name" required>
<input type="text" class="form-control" id="common_name" name="common_name"
required pattern="[a-zA-Z0-9.-]+"
title="只能包含字母、数字、点号和短横线,且不能以点号或短横线开头或结尾">
<div class="form-text">证书的Common Name字段通常是域名</div>
</div>
<div class="col-md-6">
@ -55,12 +57,24 @@
<div class="row mb-3">
<div class="col-md-6">
<label for="san_dns" class="form-label">SAN DNS (可选)</label>
<label for="san_dns" class="form-label">
SAN DNS (可选)
<span class="text-primary ms-1" data-bs-toggle="tooltip"
title="Subject Alternative Name - 主题备用名称,用于指定证书可用的多个域名">
<i class="fas fa-question-circle"></i>
</span>
</label>
<input type="text" class="form-control" id="san_dns" name="san_dns">
<div class="form-text">多个DNS用逗号分隔如: example.com,www.example.com</div>
</div>
<div class="col-md-6">
<label for="san_ip" class="form-label">SAN IP (可选)</label>
<label for="san_ip" class="form-label">
SAN IP (可选)
<span class="text-primary ms-1" data-bs-toggle="tooltip"
title="Subject Alternative Name - 主题备用名称用于指定证书可用的多个IP地址">
<i class="fas fa-question-circle"></i>
</span>
</label>
<input type="text" class="form-control" id="san_ip" name="san_ip">
<div class="form-text">多个IP用逗号分隔如: 192.168.1.1,10.0.0.1</div>
</div>
@ -88,4 +102,65 @@
</form>
</div>
</div>
<!-- SAN 警告模态框 -->
<div class="modal fade" id="sanWarningModal" tabindex="-1" aria-labelledby="sanWarningModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white"> <!-- 修改了这一行添加了bg-danger和text-white类 -->
<h5 class="modal-title" id="sanWarningModalLabel">缺少SAN扩展警告</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>您没有配置任何SAN(主题备用名称)扩展,这可能导致以下问题:</p>
<ul>
<li>现代浏览器可能不信任没有SAN扩展的证书</li>
<li>证书只能通过Common Name(CN)字段指定的名称访问</li>
<li>不符合现代安全标准</li>
</ul>
<p>建议至少配置一个DNS SAN或IP SAN。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">返回修改</button>
<button type="button" class="btn btn-primary" id="continueWithoutSan">继续创建</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 初始化工具提示
document.addEventListener('DOMContentLoaded', function() {
// 初始化Bootstrap工具提示
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// 表单提交验证
document.getElementById('certificateForm').addEventListener('submit', function(e) {
const sanDns = document.getElementById('san_dns').value.trim()
const sanIp = document.getElementById('san_ip').value.trim()
// 检查是否没有配置任何SAN
if (!sanDns && !sanIp) {
e.preventDefault() // 阻止表单提交
// 显示警告模态框
var sanWarningModal = new bootstrap.Modal(document.getElementById('sanWarningModal'))
sanWarningModal.show()
// 继续创建按钮事件
document.getElementById('continueWithoutSan').addEventListener('click', function() {
sanWarningModal.hide()
document.getElementById('certificateForm').submit()
})
}
})
})
</script>
{% endblock %}

View File

@ -1,40 +1,215 @@
{% extends "base.html" %}
{% block title %}首页{% endblock %}
{% block title %}证书管理系统 - 首页{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">系统概览</h5>
<div class="container-fluid">
<div class="row mb-4">
<!-- CA机构统计卡片 -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
CA机构数量</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ ca_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-shield-alt fa-2x text-gray-300"></i>
</div>
</div>
</div>
<div class="card-footer bg-transparent">
<a href="{{ url_for('ca_list') }}" class="small text-primary stretched-link">
查看所有CA机构 <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
<div class="card-body">
<p>欢迎使用证书管理系统!</p>
<div class="row">
<div class="col-md-6">
<div class="card bg-light mb-3">
<div class="card-body">
<h5 class="card-title">CA机构</h5>
<p class="card-text">
<a href="{{ url_for('ca_list') }}">查看所有CA机构</a>
</p>
</div>
</div>
<!-- 证书统计卡片 -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
证书总数</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ cert_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-certificate fa-2x text-gray-300"></i>
</div>
</div>
<div class="col-md-6">
<div class="card bg-light mb-3">
<div class="card-body">
<h5 class="card-title">证书</h5>
<p class="card-text">
<a href="{{ url_for('certificate_list') }}">查看所有证书</a>
</p>
</div>
</div>
<div class="card-footer bg-transparent">
<a href="{{ url_for('certificate_list') }}" class="small text-success stretched-link">
查看所有证书 <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
<!-- 活跃证书卡片 -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
活跃证书</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ active_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
</div>
</div>
</div>
<div class="card-footer bg-transparent">
<a href="{{ url_for('certificate_list') }}?status=active" class="small text-info stretched-link">
查看活跃证书 <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
<!-- 即将过期证书卡片 -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
即将过期证书 (30天内)</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ expiring_soon_count }}</div>
</div>
<div class="col-auto">
<i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i>
</div>
</div>
</div>
<div class="card-footer bg-transparent">
<a href="{{ url_for('certificate_list') }}?expiring=30" class="small text-warning stretched-link">
查看即将过期证书 <i class="fas fa-arrow-right ms-1"></i>
</a>
</div>
</div>
</div>
</div>
<div class="row">
<!-- 最近CA机构 -->
<div class="col-lg-6 mb-4">
<div class="card shadow">
<div class="card-header bg-light d-flex justify-content-between align-items-center py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-shield-alt me-2"></i> 最近创建的CA机构
</h6>
<a href="{{ url_for('create_ca_view') }}" class="btn btn-sm btn-primary">
<i class="fas fa-plus me-1"></i> 新建CA
</a>
</div>
<div class="card-body">
{% if recent_cas %}
<div class="list-group">
{% for ca in recent_cas %}
<a href="{{ url_for('ca_detail', ca_id=ca.id) }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{{ ca.name }}</h6>
<small class="text-muted">{{ ca.common_name }}</small>
</div>
<span class="badge bg-primary rounded-pill">
{{ ca.created_at.strftime('%Y-%m-%d') }}
</span>
</a>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-shield-alt fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无CA机构记录</p>
<a href="{{ url_for('create_ca_view') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> 创建第一个CA机构
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 最近证书 -->
<div class="col-lg-6 mb-4">
<div class="card shadow">
<div class="card-header bg-light d-flex justify-content-between align-items-center py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-certificate me-2"></i> 最近创建的证书
</h6>
<a href="{{ url_for('create_certificate_view') }}" class="btn btn-sm btn-primary">
<i class="fas fa-plus me-1"></i> 新建证书
</a>
</div>
<div class="card-body">
{% if recent_certs %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>通用名</th>
<th>状态</th>
<th>有效期</th>
</tr>
</thead>
<tbody>
{% for cert in recent_certs %}
<tr>
<td>
<a href="{{ url_for('certificate_detail', cert_id=cert.id) }}"
class="text-decoration-none">
{{ cert.common_name }}
</a>
</td>
<td>
{% if cert.status == 'active' %}
<span class="badge bg-success rounded-pill">有效</span>
{% elif cert.status == 'revoked' %}
<span class="badge bg-danger rounded-pill">已吊销</span>
{% else %}
<span class="badge bg-secondary rounded-pill">已过期</span>
{% endif %}
</td>
<td>{{ cert.expires_at.strftime('%Y-%m-%d') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-certificate fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无证书记录</p>
<a href="{{ url_for('create_certificate_view') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> 创建第一个证书
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
// 初始化工具提示
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
})
</script>
{% endblock %}

View File

@ -3,30 +3,62 @@
{% block title %}登录{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">用户登录</h4>
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-sign-in-alt text-primary me-2"></i>用户登录
</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('login') }}">
<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">
<i class="fas fa-user me-1 text-muted"></i>用户名
</label>
<input type="text" class="form-control" id="username" name="username" required
placeholder="请输入用户名">
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
<label for="password" class="form-label">
<i class="fas fa-lock me-1 text-muted"></i>密码
</label>
<input type="password" class="form-control" id="password" name="password" required
placeholder="请输入密码">
</div>
<div class="mb-3">
<label for="captcha" class="form-label">验证码: <strong>{{ captcha_code }}</strong></label>
<input type="text" class="form-control" id="captcha" name="captcha" required>
<div class="mb-4">
<label for="captcha" class="form-label">
<i class="fas fa-shield-alt me-1 text-muted"></i>验证码
</label>
<div class="input-group">
<input type="text" class="form-control" id="captcha" name="captcha" required
placeholder="请输入验证码">
<img src="{{ url_for('captcha') }}" id="captcha-image"
class="img-thumbnail bg-light"
style="cursor: pointer; width: 100px; height: 38px;"
onclick="refreshCaptcha()"
title="点击刷新验证码">
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sign-in-alt me-1"></i> 登录
</button>
<a href="{{ url_for('register') }}" class="btn btn-link text-decoration-none">
没有账号?立即注册
</a>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
</div>
</div>
</div>
</div>
<script>
function refreshCaptcha() {
var captchaImage = document.getElementById('captcha-image');
captchaImage.src = "{{ url_for('captcha') }}?" + new Date().getTime();
}
</script>
{% endblock %}

View File

@ -3,41 +3,138 @@
{% block title %}用户注册{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">用户注册</h4>
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="card-title mb-0">
<i class="fas fa-user-plus text-primary me-2"></i>用户注册
</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('register') }}">
<form method="POST" action="{{ url_for('register') }}" id="registerForm">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
<div class="form-text">请输入4-20位的字母、数字或下划线</div>
<label for="username" class="form-label">
<i class="fas fa-user me-1 text-muted"></i>用户名
</label>
<input type="text" class="form-control" id="username" name="username" required
pattern="[a-zA-Z0-9_]{4,20}"
title="4-20位字母、数字或下划线">
<small class="form-text text-muted">4-20位字母、数字或下划线</small>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
<div class="form-text">至少8位字符包含字母和数字</div>
<label for="password" class="form-label">
<i class="fas fa-lock me-1 text-muted"></i>密码
</label>
<input type="password" class="form-control" id="password" name="password" required
minlength="{{ config.PASSWORD_POLICY.min_length }}"
pattern="{% if config.PASSWORD_POLICY.require_uppercase %}(?=.*[A-Z]){% endif %}
{% if config.PASSWORD_POLICY.require_lowercase %}(?=.*[a-z]){% endif %}
{% if config.PASSWORD_POLICY.require_digits %}(?=.*\d]){% endif %}
{% if config.PASSWORD_POLICY.require_special_chars %}(?=.*[!@#$%^&*]){% endif %}.*"
title="{% if config.PASSWORD_POLICY.require_uppercase %}必须包含大写字母{% endif %}
{% if config.PASSWORD_POLICY.require_lowercase %}必须包含小写字母{% endif %}
{% if config.PASSWORD_POLICY.require_digits %}必须包含数字{% endif %}
{% if config.PASSWORD_POLICY.require_special_chars %}必须包含特殊字符{% endif %}">
<small class="form-text text-muted">
密码要求:至少{{ config.PASSWORD_POLICY.min_length }}位
{% if config.PASSWORD_POLICY.require_uppercase %},包含大写字母{% endif %}
{% if config.PASSWORD_POLICY.require_lowercase %},包含小写字母{% endif %}
{% if config.PASSWORD_POLICY.require_digits %},包含数字{% endif %}
{% if config.PASSWORD_POLICY.require_special_chars %},包含特殊字符(!@#$%^&*){% endif %}
</small>
</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>
<label for="confirm_password" class="form-label">
<i class="fas fa-lock me-1 text-muted"></i>确认密码
</label>
<input type="password" class="form-control" id="confirm_password"
name="confirm_password" required
oninput="checkPasswordMatch()">
<small id="passwordMatchError" class="text-danger d-none">两次输入的密码不一致</small>
</div>
{% if config.EMAIL_VERIFICATION_REQUIRED %}
<div class="mb-3">
<label for="email" class="form-label">邮箱(可选)</label>
<input type="email" class="form-control" id="email" name="email">
<label for="email" class="form-label">
<i class="fas fa-envelope me-1 text-muted"></i>邮箱
</label>
<input type="email" class="form-control" id="email" name="email" required
placeholder="example@domain.com">
<small class="form-text text-muted">用于接收验证邮件</small>
</div>
{% else %}
<div class="mb-3">
<label for="captcha" class="form-label">验证码: <strong>{{ captcha_code }}</strong></label>
<input type="text" class="form-control" id="captcha" name="captcha" required>
<label for="email" class="form-label">
<i class="fas fa-envelope me-1 text-muted"></i>邮箱(可选)
</label>
<input type="email" class="form-control" id="email" name="email"
placeholder="example@domain.com">
</div>
{% endif %}
<div class="mb-4">
<label for="captcha" class="form-label">
<i class="fas fa-shield-alt me-1 text-muted"></i>验证码
</label>
<div class="input-group">
<input type="text" class="form-control" id="captcha" name="captcha" required
placeholder="请输入验证码">
<img src="{{ url_for('captcha') }}" id="captcha-image"
class="img-thumbnail bg-light"
style="cursor: pointer; width: 100px; height: 38px;"
onclick="refreshCaptcha()"
title="点击刷新验证码">
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus me-1"></i> 注册
</button>
<a href="{{ url_for('login') }}" class="btn btn-link text-decoration-none">
已有账号?去登录
</a>
</div>
<button type="submit" class="btn btn-primary">注册</button>
<a href="{{ url_for('login') }}" class="btn btn-link">已有账号?去登录</a>
</form>
</div>
</div>
</div>
</div>
<script>
function refreshCaptcha() {
document.getElementById('captcha-image').src = "{{ url_for('captcha') }}?" + Date.now();
}
function checkPasswordMatch() {
const password = document.getElementById('password');
const confirmPassword = document.getElementById('confirm_password');
const errorElement = document.getElementById('passwordMatchError');
if (password.value !== confirmPassword.value) {
errorElement.classList.remove('d-none');
confirmPassword.setCustomValidity("密码不匹配");
} else {
errorElement.classList.add('d-none');
confirmPassword.setCustomValidity("");
}
}
// 实时验证密码复杂度
document.getElementById('password').addEventListener('input', function() {
const password = this.value;
const policy = {
minLength: {{ config.PASSWORD_POLICY.min_length }},
requireUpper: {{ config.PASSWORD_POLICY.require_uppercase|lower }},
requireLower: {{ config.PASSWORD_POLICY.require_lowercase|lower }},
requireDigit: {{ config.PASSWORD_POLICY.require_digits|lower }},
requireSpecial: {{ config.PASSWORD_POLICY.require_special_chars|lower }}
};
// 可以在这里添加更复杂的实时验证逻辑
});
</script>
{% endblock %}