Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f64e7b18b0 | |||
| 9c92be13d9 | |||
| 89cb0af79a | |||
| 9cbb6b022c | |||
| 375816df87 | |||
| 2b36de569d | |||
| 21142a6104 | |||
| f3a2f05b1c | |||
| 722d88a017 | |||
| 179400f9ed | |||
| cc2b18ba80 | |||
| fc3801f0f2 | |||
| c2ca546955 | |||
| f711ca8280 | |||
| 6993679971 | |||
| 2ae605e60b | |||
| 117a7885d7 | |||
| 5aeaab5747 | |||
| 35eea7a803 | |||
| 0001c8f0d3 | |||
| 5c2381c3a8 | |||
| 25add10b35 | |||
| 6f578be53e | |||
| c5e8fd6e3f | |||
| 47f0c572b3 | |||
| afa61f7a04 | |||
| 35165bd58b | |||
| 1ad7b4f642 | |||
| 61d9ba7db9 | |||
| 6e3838b9f0 | |||
| 01e546ab51 | |||
| 5854180c29 | |||
| 1450c6aca6 | |||
| a73d27f687 | |||
| 4d802ba369 | |||
| 1720d69be6 | |||
| ce31a591af |
5
.env
5
.env
@ -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
39
.env.example
Normal 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
BIN
.gitignore
vendored
Normal file
Binary file not shown.
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
.idea/certmgr.iml
generated
8
.idea/certmgr.iml
generated
@ -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
28
.idea/deployment.xml
generated
@ -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>
|
||||
16
.idea/inspectionProfiles/Project_Default.xml
generated
16
.idea/inspectionProfiles/Project_Default.xml
generated
@ -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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@ -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
4
.idea/misc.xml
generated
@ -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
8
.idea/modules.xml
generated
@ -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
6
.idea/vcs.xml
generated
@ -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
33
Dockerfile
Normal 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
21
LICENSE
Normal 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
131
README.md
Normal file
@ -0,0 +1,131 @@
|
||||
# 自签名证书管理系统
|
||||

|
||||

|
||||
|
||||
<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.
88
config.py
88
config.py
@ -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
156
database.py
Normal 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
13
docker-compose.yml
Normal 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
|
||||
@ -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
BIN
screenshots/ca_detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
screenshots/cert_detail.png
Normal file
BIN
screenshots/cert_detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
screenshots/index.png
Normal file
BIN
screenshots/index.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
static/arial.ttf
Normal file
BIN
static/arial.ttf
Normal file
Binary file not shown.
1
static/favicon.svg
Normal file
1
static/favicon.svg
Normal 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 |
@ -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">自签证书管理系统 © {{ 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>
|
||||
@ -4,39 +4,41 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>CA机构详情: {{ ca.name }}
|
||||
<small class="text-muted fs-6">(路径: {{ ca.name|to_pinyin }})</small>
|
||||
</h2>
|
||||
<div class="btn-group">
|
||||
<div>
|
||||
<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-2"
|
||||
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-2"
|
||||
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"
|
||||
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 me-2"
|
||||
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">
|
||||
@ -95,7 +97,7 @@
|
||||
<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 text-muted"><i class="fas fa-file-certificate me-2"></i> 证书路径</dt>
|
||||
<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>
|
||||
@ -110,11 +112,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 证书列表部分 -->
|
||||
<div class="card">
|
||||
<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">{{ certificates|length }}</span>
|
||||
<span class="badge bg-primary rounded-pill ms-2">{{ total }}</span>
|
||||
</h5>
|
||||
<div>
|
||||
<a href="{{ url_for('create_certificate_view') }}?ca_id={{ ca.id }}"
|
||||
@ -135,6 +138,7 @@
|
||||
<th>通用名</th>
|
||||
<th>状态</th>
|
||||
<th>有效期至</th>
|
||||
<th>创建者</th>
|
||||
<th>创建时间</th>
|
||||
<th class="pe-4">操作</th>
|
||||
</tr>
|
||||
@ -164,29 +168,56 @@
|
||||
{% 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 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>
|
||||
<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>
|
||||
{% 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">«</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">»</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>
|
||||
|
||||
@ -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">«</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">»</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 %}
|
||||
@ -4,44 +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('delete_certificate', cert_id=cert.id) }}" class="btn btn-danger">删除</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>
|
||||
@ -50,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">{{ get_username(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(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>
|
||||
@ -96,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 %}
|
||||
@ -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">«</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">»</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 %}
|
||||
@ -8,7 +8,7 @@
|
||||
<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>
|
||||
@ -57,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>
|
||||
@ -90,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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user