全面优化样式,修复注册验证码功能

This commit is contained in:
wzj 2025-06-14 14:11:56 +08:00
parent 1720d69be6
commit 4d802ba369
10 changed files with 911 additions and 229 deletions

209
app.py
View File

@ -14,6 +14,9 @@ import zipfile
import shutil
import re
from pypinyin import pinyin, Style
from flask import send_file
from PIL import Image, ImageDraw, ImageFont
from flask_migrate import Migrate
# 从配置文件中导入配置
@ -22,6 +25,10 @@ from config import Config
app = Flask(__name__)
app.config.from_object(Config)
@app.context_processor
def inject_now():
return {'now': datetime.now()}
# 确保证书存储目录存在
os.makedirs(Config.CERT_STORE, exist_ok=True)
@ -37,6 +44,89 @@ login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
from io import BytesIO
from flask import send_file
import random
import string
from PIL import Image, ImageDraw, ImageFont
def generate_captcha_image():
# 生成4位随机验证码字母和数字
captcha_code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
# 创建图片120x40像素白色背景
image = Image.new('RGB', (120, 40), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
# 尝试加载字体,失败则使用默认字体
try:
font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
# 绘制验证码文本(每个字符随机颜色)
for i, char in enumerate(captcha_code):
text_color = (
random.randint(0, 150), # R
random.randint(0, 150), # G
random.randint(0, 150) # B
)
draw.text(
(10 + i * 25, 5), # 位置
char, # 文本
font=font, # 字体
fill=text_color # 颜色
)
# 添加5条干扰线随机位置和颜色
for _ in range(5):
line_color = (
random.randint(0, 255), # R
random.randint(0, 255), # G
random.randint(0, 255) # B
)
draw.line(
[
random.randint(0, 120), # x1
random.randint(0, 40), # y1
random.randint(0, 120), # x2
random.randint(0, 40) # y2
],
fill=line_color,
width=1
)
# 保存验证码到数据库
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor()
# 清除10分钟前的旧验证码
cursor.execute("DELETE FROM captcha WHERE created_at < NOW() - INTERVAL 10 MINUTE")
# 插入新验证码
cursor.execute("INSERT INTO captcha (code) VALUES (%s)", (captcha_code,))
conn.commit()
except Error as e:
print(f"Database error: {e}")
finally:
if conn.is_connected():
cursor.close()
conn.close()
# 返回图片字节流
img_io = BytesIO()
image.save(img_io, 'PNG')
img_io.seek(0)
return img_io, captcha_code
@app.route('/captcha')
def captcha():
img_io, _ = generate_captcha_image()
return send_file(img_io, mimetype='image/png')
def to_pinyin(text):
"""将中文转换为拼音"""
if not text:
@ -608,9 +698,120 @@ def register():
@app.route('/')
@login_required
def index():
return render_template('index.html')
conn = get_db_connection()
if conn:
try:
cursor = conn.cursor(dictionary=True)
# 获取CA数量
if current_user.is_admin:
cursor.execute("SELECT COUNT(*) as count FROM certificate_authorities")
ca_count = cursor.fetchone()['count']
cursor.fetchall() # 确保清空结果集
cursor.execute("""
SELECT * FROM certificate_authorities
ORDER BY created_at DESC LIMIT 5
""")
else:
cursor.execute("""
SELECT COUNT(*) as count FROM certificate_authorities
WHERE created_by = %s
""", (current_user.id,))
ca_count = cursor.fetchone()['count']
cursor.fetchall() # 确保清空结果集
cursor.execute("""
SELECT * FROM certificate_authorities
WHERE created_by = %s
ORDER BY created_at DESC LIMIT 5
""", (current_user.id,))
recent_cas = cursor.fetchall()
# 获取证书数量
if current_user.is_admin:
cursor.execute("SELECT COUNT(*) as count FROM certificates")
cert_count = cursor.fetchone()['count']
cursor.fetchall() # 确保清空结果集
cursor.execute("""
SELECT * FROM certificates
ORDER BY created_at DESC LIMIT 5
""")
else:
cursor.execute("""
SELECT COUNT(*) as count FROM certificates
WHERE created_by = %s
""", (current_user.id,))
cert_count = cursor.fetchone()['count']
cursor.fetchall() # 确保清空结果集
cursor.execute("""
SELECT * FROM certificates
WHERE created_by = %s
ORDER BY created_at DESC LIMIT 5
""", (current_user.id,))
recent_certs = cursor.fetchall()
# 获取即将过期证书数量30天内
if current_user.is_admin:
cursor.execute("""
SELECT COUNT(*) as count FROM certificates
WHERE expires_at BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 30 DAY)
AND status = 'active'
""")
else:
cursor.execute("""
SELECT COUNT(*) as count FROM certificates
WHERE expires_at BETWEEN NOW() AND DATE_ADD(NOW(), INTERVAL 30 DAY)
AND status = 'active' AND created_by = %s
""", (current_user.id,))
expiring_soon_count = cursor.fetchone()['count']
cursor.fetchall() # 确保清空结果集
# 获取活跃证书数量
if current_user.is_admin:
cursor.execute("""
SELECT COUNT(*) as count FROM certificates
WHERE status = 'active'
""")
else:
cursor.execute("""
SELECT COUNT(*) as count FROM certificates
WHERE status = 'active' AND created_by = %s
""", (current_user.id,))
active_count = cursor.fetchone()['count']
cursor.fetchall() # 确保清空结果集
return render_template('index.html',
ca_count=ca_count,
cert_count=cert_count,
expiring_soon_count=expiring_soon_count,
active_count=active_count,
recent_cas=recent_cas,
recent_certs=recent_certs)
except Error as e:
print(f"Database error: {e}")
flash('获取系统统计信息失败', 'danger')
return render_template('index.html',
ca_count=0,
cert_count=0,
expiring_soon_count=0,
active_count=0,
recent_cas=[],
recent_certs=[])
finally:
if conn.is_connected():
cursor.close()
conn.close()
return render_template('index.html',
ca_count=0,
cert_count=0,
expiring_soon_count=0,
active_count=0,
recent_cas=[],
recent_certs=[])
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
@ -677,7 +878,7 @@ def ca_list():
else:
cursor.execute("SELECT * FROM certificate_authorities WHERE created_by = %s", (current_user.id,))
cas = cursor.fetchall()
return render_template('ca_list.html', cas=cas)
return render_template('ca_list.html', cas=cas, get_username=get_username)
except Error as e:
print(f"Database error: {e}")
flash('获取CA列表失败', 'danger')
@ -899,7 +1100,7 @@ def certificate_list():
ORDER BY c.created_at DESC
""", (current_user.id,))
certificates = cursor.fetchall()
return render_template('certificate_list.html', certificates=certificates)
return render_template('certificate_list.html', certificates=certificates, get_username=get_username)
except Error as e:
print(f"Database error: {e}")
flash('获取证书列表失败', 'danger')
@ -1372,4 +1573,4 @@ def download_file(filename):
if __name__ == '__main__':
app.run(debug=Config.DEBUG, ssl_context='adhoc', host=Config.APP_HOST, port=Config.APP_PORT)
app.run(debug=Config.DEBUG, host=Config.APP_HOST, port=Config.APP_PORT)

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
shutil
flask_migrate

View File

@ -5,51 +5,147 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>证书管理系统 - {% block title %}{% endblock %}</title>
<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;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,.1);
background-color: rgb(124 146 157) !important;
}
.main-content {
flex: 1;
padding-top: 20px;
padding-bottom: 40px;
}
.footer {
background-color: #f8f9fa;
padding: 20px 0;
margin-top: auto;
}
.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">版本 1.0.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,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-folder-open me-1"></i> 证书路径</dt>
<dd class="col-sm-8">
<code>{{ ca.cert_path|to_pinyin }}</code>
</dd>

View File

@ -4,44 +4,70 @@
{% 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>共 {{ cas|length }} 个CA机构
</div>
</div>
<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 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 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 ca in cas %}
<tr>
<td class="ps-4">{{ 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="8" 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>
</div>
</div>
{% endblock %}

View File

@ -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-file-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,111 @@
{% 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>共 {{ certificates|length }} 个证书
</div>
</div>
<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 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 class="ps-4">ID</th>
<th>通用名</th>
<th>CA机构</th>
<th>状态</th>
<th>有效期至</th>
<th>创建时间</th>
<th class="pe-4">操作</th>
</tr>
</thead>
<tbody>
{% for cert in certificates %}
<tr>
<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>{{ 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>{{ 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="7" 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>
</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

@ -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,77 @@
{% 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') }}">
<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
placeholder="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
placeholder="至少8位字符">
<small class="form-text text-muted">至少8位字符包含字母和数字</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
placeholder="再次输入密码">
</div>
<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"
placeholder="example@domain.com">
</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-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() {
var captchaImage = document.getElementById('captcha-image');
captchaImage.src = "{{ url_for('captcha') }}?" + new Date().getTime();
}
</script>
{% endblock %}