@code2rich/jpage 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +68 -0
- package/.dockerignore +8 -0
- package/.env.example +56 -0
- package/.github/workflows/ci.yml +43 -0
- package/CLAUDE.md +280 -0
- package/Dockerfile +44 -0
- package/LICENSE +21 -0
- package/README.md +433 -0
- package/README_EN.md +399 -0
- package/bin/args.js +64 -0
- package/bin/client.js +93 -0
- package/bin/commands/_shared.js +54 -0
- package/bin/commands/cat.js +23 -0
- package/bin/commands/ls.js +44 -0
- package/bin/commands/mv.js +20 -0
- package/bin/commands/rm.js +22 -0
- package/bin/commands/skills.js +70 -0
- package/bin/commands/star.js +23 -0
- package/bin/commands/tags.js +97 -0
- package/bin/commands/upload.js +84 -0
- package/bin/commands/url.js +25 -0
- package/bin/commands/whoami.js +29 -0
- package/bin/config.js +85 -0
- package/bin/jpage.js +168 -0
- package/build.js +112 -0
- package/docker-compose.yml +26 -0
- package/docs/api.md +438 -0
- package/docs/design/005-custom-modal.md +296 -0
- package/docs/design/013-file-version-history.md +324 -0
- package/docs/design/billing-system.md +600 -0
- package/docs/design/db-index-and-healthcheck.md +176 -0
- package/docs/design/loading-states.md +209 -0
- package/docs/virtual-hosting-feasibility.md +453 -0
- package/eslint.config.mjs +172 -0
- package/lib/auth-state.js +15 -0
- package/lib/categories.js +20 -0
- package/lib/crypto.js +85 -0
- package/lib/csp.js +66 -0
- package/lib/db.js +53 -0
- package/lib/dispatch.js +103 -0
- package/lib/fts.js +81 -0
- package/lib/middleware/auth.js +114 -0
- package/lib/middleware/files.js +42 -0
- package/lib/paths.js +9 -0
- package/lib/render-cache.js +48 -0
- package/lib/render.js +157 -0
- package/lib/templates.js +149 -0
- package/lib/util.js +66 -0
- package/lib/view-counts.js +59 -0
- package/lib/zip.js +192 -0
- package/logger.js +16 -0
- package/mailer.js +34 -0
- package/mcp/constants.js +16 -0
- package/mcp/resources.js +74 -0
- package/mcp/server.js +43 -0
- package/mcp/tools-categories.js +56 -0
- package/mcp/tools-content-templates.js +59 -0
- package/mcp/tools-files.js +245 -0
- package/mcp/tools-tags.js +41 -0
- package/mcp/tools-versions.js +57 -0
- package/mcp/transport.js +183 -0
- package/mcp/util.js +63 -0
- package/mcp-server.js +20 -0
- package/migrations/001_init_schema.js +25 -0
- package/migrations/002_add_share_key.js +33 -0
- package/migrations/003_add_roles_and_tokens.js +28 -0
- package/migrations/004_add_version_history.js +32 -0
- package/migrations/005_tags_starred_categories.js +49 -0
- package/migrations/006_zip_bundle.js +17 -0
- package/migrations/007_add_file_type_uploaded_by_indexes.js +7 -0
- package/migrations/008_add_fts5.js +6 -0
- package/migrations/009_add_link_visits.js +20 -0
- package/migrations/010_add_templates_system.js +34 -0
- package/migrations/011_content_templates.js +233 -0
- package/migrations/012_add_email_and_verification.js +35 -0
- package/migrations/013_add_token_encrypted.js +14 -0
- package/migrations.js +65 -0
- package/package.json +63 -0
- package/public/css/style.css +2915 -0
- package/public/index.html +855 -0
- package/public/js/api.js +22 -0
- package/public/js/app.js +94 -0
- package/public/js/components/dialog.js +106 -0
- package/public/js/components/toast.js +13 -0
- package/public/js/pages/content-templates.js +330 -0
- package/public/js/pages/home.js +1903 -0
- package/public/js/pages/landing.js +158 -0
- package/public/js/pages/login.js +175 -0
- package/public/js/pages/preview.js +713 -0
- package/public/js/theme.js +44 -0
- package/public/js/utils.js +67 -0
- package/routes/admin.js +136 -0
- package/routes/auth.js +365 -0
- package/routes/categories.js +90 -0
- package/routes/content-templates.js +215 -0
- package/routes/files/_shared.js +112 -0
- package/routes/files/associations.js +94 -0
- package/routes/files/crud.js +139 -0
- package/routes/files/detail-serve.js +178 -0
- package/routes/files/index.js +38 -0
- package/routes/files/list.js +200 -0
- package/routes/files/overwrite.js +114 -0
- package/routes/files/upload.js +204 -0
- package/routes/files/versions.js +166 -0
- package/routes/files.js +16 -0
- package/routes/skills.js +93 -0
- package/routes/tags.js +65 -0
- package/routes/tokens.js +110 -0
- package/routes/users.js +120 -0
- package/server.js +372 -0
- package/skills/jpage-content-template/SKILL.md +98 -0
- package/skills/jpage-upload/SKILL.md +247 -0
- package/skills-registry.js +135 -0
- package/templates/academic.html +41 -0
- package/templates/dark-pro.html +41 -0
- package/templates/default.html +56 -0
- package/templates/github.html +67 -0
- package/test/browser-harness.js +125 -0
- package/test/dispatch-bench.js +74 -0
- package/test/helpers/setup.js +45 -0
- package/test/integration/admin.test.js +108 -0
- package/test/integration/auth.test.js +93 -0
- package/test/integration/categories.test.js +103 -0
- package/test/integration/cli.test.js +310 -0
- package/test/integration/content-templates.test.js +147 -0
- package/test/integration/files-security.test.js +248 -0
- package/test/integration/files.test.js +139 -0
- package/test/integration/share.test.js +79 -0
- package/test/integration/skills.test.js +104 -0
- package/test/integration/tags.test.js +84 -0
- package/test/integration/tokens.test.js +89 -0
- package/test/integration/users.test.js +138 -0
- package/test/mcp-harness.js +152 -0
- package/test/perf-bench.js +108 -0
- package/test/perf-harness.js +198 -0
- package/test/run-server.sh +15 -0
- package/test/unit/cli-args.test.js +88 -0
- package/test/unit/cli-config.test.js +89 -0
- package/test/unit/crypto.test.js +100 -0
- package/test/unit/fts.test.js +52 -0
- package/test/unit/render-cache.test.js +76 -0
- package/test/unit/util.test.js +81 -0
- package/test/unit/zip.test.js +164 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const BUILTIN_TEMPLATES = [
|
|
2
|
+
{
|
|
3
|
+
title: '深色数据仪表板',
|
|
4
|
+
description: '深色主题的数据仪表板,使用 CSS Grid 布局,包含统计卡片和图表区域。适合数据可视化、监控面板。',
|
|
5
|
+
file_type: 'html',
|
|
6
|
+
scene: 'dashboard',
|
|
7
|
+
style_tags: 'dark,grid,chart,card',
|
|
8
|
+
content: `<!DOCTYPE html>
|
|
9
|
+
<html lang="zh-CN">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>数据仪表板</title>
|
|
14
|
+
<style>
|
|
15
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
16
|
+
body { font-family: -apple-system, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; padding: 24px; }
|
|
17
|
+
h1 { font-size: 24px; font-weight: 600; margin-bottom: 24px; color: #f1f5f9; }
|
|
18
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
19
|
+
.stat-card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; }
|
|
20
|
+
.stat-card .label { font-size: 13px; color: #94a3b8; margin-bottom: 8px; }
|
|
21
|
+
.stat-card .value { font-size: 28px; font-weight: 700; color: #f1f5f9; }
|
|
22
|
+
.stat-card .change { font-size: 12px; margin-top: 4px; }
|
|
23
|
+
.stat-card .change.up { color: #34d399; }
|
|
24
|
+
.stat-card .change.down { color: #f87171; }
|
|
25
|
+
.chart-area { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
|
|
26
|
+
.chart-card { background: #1e293b; border-radius: 12px; padding: 20px; border: 1px solid #334155; min-height: 300px; }
|
|
27
|
+
.chart-card h3 { font-size: 14px; color: #94a3b8; margin-bottom: 16px; font-weight: 500; }
|
|
28
|
+
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 200px; padding-top: 16px; }
|
|
29
|
+
.bar { flex: 1; border-radius: 4px 4px 0 0; min-height: 20px; transition: opacity .2s; }
|
|
30
|
+
.bar:hover { opacity: 0.8; }
|
|
31
|
+
.legend { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; }
|
|
32
|
+
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #cbd5e1; }
|
|
33
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; }
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<h1>数据仪表板</h1>
|
|
38
|
+
<div class="stats">
|
|
39
|
+
<div class="stat-card">
|
|
40
|
+
<div class="label">总用户数</div>
|
|
41
|
+
<div class="value">12,847</div>
|
|
42
|
+
<div class="change up">+12.5%</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="stat-card">
|
|
45
|
+
<div class="label">日活跃用户</div>
|
|
46
|
+
<div class="value">3,421</div>
|
|
47
|
+
<div class="change up">+8.3%</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="stat-card">
|
|
50
|
+
<div class="label">平均会话时长</div>
|
|
51
|
+
<div class="value">4m 32s</div>
|
|
52
|
+
<div class="change down">-2.1%</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="stat-card">
|
|
55
|
+
<div class="label">转化率</div>
|
|
56
|
+
<div class="value">6.8%</div>
|
|
57
|
+
<div class="change up">+0.5%</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="chart-area">
|
|
61
|
+
<div class="chart-card">
|
|
62
|
+
<h3>月度趋势</h3>
|
|
63
|
+
<div class="bar-chart">
|
|
64
|
+
<div class="bar" style="height:60%;background:#6366f1"></div>
|
|
65
|
+
<div class="bar" style="height:75%;background:#6366f1"></div>
|
|
66
|
+
<div class="bar" style="height:50%;background:#6366f1"></div>
|
|
67
|
+
<div class="bar" style="height:90%;background:#8b5cf6"></div>
|
|
68
|
+
<div class="bar" style="height:70%;background:#6366f1"></div>
|
|
69
|
+
<div class="bar" style="height:85%;background:#8b5cf6"></div>
|
|
70
|
+
<div class="bar" style="height:65%;background:#6366f1"></div>
|
|
71
|
+
<div class="bar" style="height:95%;background:#a78bfa"></div>
|
|
72
|
+
<div class="bar" style="height:80%;background:#8b5cf6"></div>
|
|
73
|
+
<div class="bar" style="height:100%;background:#a78bfa"></div>
|
|
74
|
+
<div class="bar" style="height:88%;background:#8b5cf6"></div>
|
|
75
|
+
<div class="bar" style="height:92%;background:#a78bfa"></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="chart-card">
|
|
79
|
+
<h3>来源分布</h3>
|
|
80
|
+
<div class="legend">
|
|
81
|
+
<div class="legend-item"><span class="legend-dot" style="background:#6366f1"></span>直接访问 35%</div>
|
|
82
|
+
<div class="legend-item"><span class="legend-dot" style="background:#8b5cf6"></span>搜索引擎 28%</div>
|
|
83
|
+
<div class="legend-item"><span class="legend-dot" style="background:#a78bfa"></span>社交媒体 22%</div>
|
|
84
|
+
<div class="legend-item"><span class="legend-dot" style="background:#c4b5fd"></span>推荐链接 15%</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</body>
|
|
89
|
+
</html>`
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: '项目周报',
|
|
93
|
+
description: '结构清晰的项目周报 Markdown 模板,包含本周进展、风险问题、下周计划等标准章节。',
|
|
94
|
+
file_type: 'markdown',
|
|
95
|
+
scene: 'report',
|
|
96
|
+
style_tags: 'structured,weekly,team',
|
|
97
|
+
content: `# 项目周报 — 第 24 周
|
|
98
|
+
|
|
99
|
+
> 2026-06-08 ~ 2026-06-14
|
|
100
|
+
|
|
101
|
+
## 本周概要
|
|
102
|
+
|
|
103
|
+
| 指标 | 目标 | 实际 | 状态 |
|
|
104
|
+
|------|------|------|------|
|
|
105
|
+
| 需求交付 | 5 | 4 | 🟡 进行中 |
|
|
106
|
+
| 缺陷修复 | 8 | 8 | ✅ 完成 |
|
|
107
|
+
| 代码审查 | 10 | 12 | ✅ 超额 |
|
|
108
|
+
|
|
109
|
+
## 重点工作进展
|
|
110
|
+
|
|
111
|
+
### 1. 用户认证模块重构
|
|
112
|
+
- **负责人**:张三
|
|
113
|
+
- **进度**:80%
|
|
114
|
+
- **完成项**:JWT 令牌签发、刷新机制、单元测试
|
|
115
|
+
- **待完成**:OAuth 第三方登录对接
|
|
116
|
+
|
|
117
|
+
### 2. 数据导出功能
|
|
118
|
+
- **负责人**:李四
|
|
119
|
+
- **进度**:100%
|
|
120
|
+
- **完成项**:CSV/Excel 导出、异步任务队列、进度通知
|
|
121
|
+
|
|
122
|
+
## 风险与问题
|
|
123
|
+
|
|
124
|
+
| # | 描述 | 影响 | 应对措施 | 负责人 |
|
|
125
|
+
|---|------|------|----------|--------|
|
|
126
|
+
| 1 | 第三方 API 响应变慢 | 中 | 增加超时重试和本地缓存 | 王五 |
|
|
127
|
+
| 2 | 测试环境磁盘空间不足 | 低 | 清理历史数据,申请扩容 | 运维 |
|
|
128
|
+
|
|
129
|
+
## 下周计划
|
|
130
|
+
|
|
131
|
+
1. 完成用户认证模块重构(含 OAuth)
|
|
132
|
+
2. 启动报表可视化需求开发
|
|
133
|
+
3. 组织代码规范评审会议
|
|
134
|
+
|
|
135
|
+
## 备注
|
|
136
|
+
|
|
137
|
+
本周三进行了全员安全培训,周五发布了 v1.3.0 版本。`
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title: '极简落地页',
|
|
141
|
+
description: '现代极简风格的产品落地页,包含 Hero 区域、特性展示和 CTA 按钮。适合产品介绍、活动推广。',
|
|
142
|
+
file_type: 'html',
|
|
143
|
+
scene: 'landing',
|
|
144
|
+
style_tags: 'minimal,gradient,modern,cta',
|
|
145
|
+
content: `<!DOCTYPE html>
|
|
146
|
+
<html lang="zh-CN">
|
|
147
|
+
<head>
|
|
148
|
+
<meta charset="UTF-8">
|
|
149
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
150
|
+
<title>产品落地页</title>
|
|
151
|
+
<style>
|
|
152
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
153
|
+
body { font-family: -apple-system, 'Segoe UI', sans-serif; color: #1a1a2e; background: #fff; }
|
|
154
|
+
.hero { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 40px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
|
|
155
|
+
.hero h1 { font-size: clamp(32px, 5vw, 56px); font-weight: 800; line-height: 1.2; margin-bottom: 16px; }
|
|
156
|
+
.hero p { font-size: 18px; opacity: 0.9; max-width: 560px; line-height: 1.6; margin-bottom: 32px; }
|
|
157
|
+
.btn-group { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
|
|
158
|
+
.btn { display: inline-block; padding: 14px 32px; border-radius: 8px; font-size: 16px; font-weight: 600; text-decoration: none; transition: transform .2s, box-shadow .2s; cursor: pointer; border: none; }
|
|
159
|
+
.btn:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.15); }
|
|
160
|
+
.btn-primary { background: #fff; color: #667eea; }
|
|
161
|
+
.btn-outline { background: transparent; color: #fff; border: 2px solid rgba(255,255,255,0.6); }
|
|
162
|
+
.features { padding: 80px 24px; max-width: 960px; margin: 0 auto; }
|
|
163
|
+
.features h2 { text-align: center; font-size: 32px; margin-bottom: 48px; color: #1a1a2e; }
|
|
164
|
+
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 32px; }
|
|
165
|
+
.feature-card { text-align: center; padding: 32px 24px; border-radius: 12px; background: #f8f9ff; }
|
|
166
|
+
.feature-icon { font-size: 40px; margin-bottom: 16px; }
|
|
167
|
+
.feature-card h3 { font-size: 18px; margin-bottom: 8px; }
|
|
168
|
+
.feature-card p { font-size: 14px; color: #64748b; line-height: 1.6; }
|
|
169
|
+
footer { text-align: center; padding: 40px 24px; color: #94a3b8; font-size: 14px; border-top: 1px solid #e2e8f0; }
|
|
170
|
+
</style>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<section class="hero">
|
|
174
|
+
<h1>让创作回归简单</h1>
|
|
175
|
+
<p>一站式内容管理与分享平台,拖入文件即可获得预览链接。支持 HTML、Markdown,与 AI 深度集成。</p>
|
|
176
|
+
<div class="btn-group">
|
|
177
|
+
<a class="btn btn-primary" href="#">立即开始</a>
|
|
178
|
+
<a class="btn btn-outline" href="#">了解更多</a>
|
|
179
|
+
</div>
|
|
180
|
+
</section>
|
|
181
|
+
<section class="features">
|
|
182
|
+
<h2>核心特性</h2>
|
|
183
|
+
<div class="feature-grid">
|
|
184
|
+
<div class="feature-card">
|
|
185
|
+
<div class="feature-icon">⚡</div>
|
|
186
|
+
<h3>即时预览</h3>
|
|
187
|
+
<p>上传即生成预览链接,零配置,无需构建工具</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="feature-card">
|
|
190
|
+
<div class="feature-icon">🎨</div>
|
|
191
|
+
<h3>多套模板</h3>
|
|
192
|
+
<p>内置多种渲染风格,一键切换外观</p>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="feature-card">
|
|
195
|
+
<div class="feature-icon">🤖</div>
|
|
196
|
+
<h3>AI 集成</h3>
|
|
197
|
+
<p>MCP 协议原生支持,AI 直接上传和分享内容</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</section>
|
|
201
|
+
<footer>© 2026 即页 — 让创作回归简单</footer>
|
|
202
|
+
</body>
|
|
203
|
+
</html>`
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
name: 'add_content_templates',
|
|
209
|
+
async up(db, { dbRun, dbGet, dbAll }) {
|
|
210
|
+
await dbRun(db, `CREATE TABLE IF NOT EXISTS content_templates (
|
|
211
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
212
|
+
title TEXT NOT NULL,
|
|
213
|
+
description TEXT,
|
|
214
|
+
file_type TEXT NOT NULL DEFAULT 'html',
|
|
215
|
+
scene TEXT,
|
|
216
|
+
style_tags TEXT,
|
|
217
|
+
content TEXT NOT NULL,
|
|
218
|
+
uploaded_by INTEGER,
|
|
219
|
+
use_count INTEGER NOT NULL DEFAULT 0,
|
|
220
|
+
is_public INTEGER NOT NULL DEFAULT 1,
|
|
221
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
222
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
223
|
+
)`);
|
|
224
|
+
|
|
225
|
+
await dbRun(db, `CREATE INDEX IF NOT EXISTS idx_ct_scene ON content_templates(scene)`);
|
|
226
|
+
await dbRun(db, `CREATE INDEX IF NOT EXISTS idx_ct_use_count ON content_templates(use_count DESC)`);
|
|
227
|
+
|
|
228
|
+
for (const t of BUILTIN_TEMPLATES) {
|
|
229
|
+
await dbRun(db, `INSERT INTO content_templates (title, description, file_type, scene, style_tags, content, use_count, is_public) VALUES (?, ?, ?, ?, ?, ?, 0, 1)`,
|
|
230
|
+
[t.title, t.description, t.file_type, t.scene, t.style_tags, t.content]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
name: 'add_email_and_verification',
|
|
3
|
+
async up(db, { dbRun, dbGet, dbAll }) {
|
|
4
|
+
const cols = await dbAll(db, 'PRAGMA table_info(users)');
|
|
5
|
+
const colNames = new Set(cols.map(c => c.name));
|
|
6
|
+
|
|
7
|
+
if (!colNames.has('email')) {
|
|
8
|
+
await dbRun(db, 'ALTER TABLE users ADD COLUMN email TEXT');
|
|
9
|
+
}
|
|
10
|
+
if (!colNames.has('email_verified')) {
|
|
11
|
+
await dbRun(db, 'ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 邮箱唯一索引(排除 NULL)
|
|
15
|
+
await dbRun(db, 'CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email) WHERE email IS NOT NULL');
|
|
16
|
+
|
|
17
|
+
// 回填:现有用户名看起来像邮箱的用户
|
|
18
|
+
await dbRun(db, "UPDATE users SET email = username, email_verified = 1 WHERE email IS NULL AND username LIKE '%@%'");
|
|
19
|
+
|
|
20
|
+
// 邮箱验证 token 表
|
|
21
|
+
await dbRun(db, `CREATE TABLE IF NOT EXISTS email_verifications (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
user_id INTEGER NOT NULL,
|
|
24
|
+
token_hash TEXT NOT NULL,
|
|
25
|
+
token_prefix TEXT NOT NULL,
|
|
26
|
+
type TEXT NOT NULL DEFAULT 'verify_email',
|
|
27
|
+
new_email TEXT,
|
|
28
|
+
expires_at DATETIME NOT NULL,
|
|
29
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
31
|
+
)`);
|
|
32
|
+
await dbRun(db, 'CREATE INDEX IF NOT EXISTS idx_email_verifications_user ON email_verifications(user_id)');
|
|
33
|
+
await dbRun(db, 'CREATE UNIQUE INDEX IF NOT EXISTS idx_email_verifications_hash ON email_verifications(token_hash)');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
name: 'add_token_encrypted',
|
|
3
|
+
async up(db, { dbRun, dbAll }) {
|
|
4
|
+
// 幂等:先检查列是否已存在
|
|
5
|
+
const cols = await dbAll(db, 'PRAGMA table_info(tokens)');
|
|
6
|
+
const colNames = new Set(cols.map(c => c.name));
|
|
7
|
+
|
|
8
|
+
// 新增 token_enc 列:存 AES-256-GCM 密文,可逆,使明文可后续查看/复制。
|
|
9
|
+
// 旧 token 留 NULL(不可查看,但鉴权不受影响)。
|
|
10
|
+
if (!colNames.has('token_enc')) {
|
|
11
|
+
await dbRun(db, 'ALTER TABLE tokens ADD COLUMN token_enc TEXT');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
package/migrations.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const logger = require('./logger');
|
|
4
|
+
|
|
5
|
+
const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
|
|
6
|
+
|
|
7
|
+
function dbRun(db, sql, params = []) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
db.run(sql, params, function (err) {
|
|
10
|
+
if (err) reject(err);
|
|
11
|
+
else resolve({ lastID: this.lastID, changes: this.changes });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function dbGet(db, sql, params = []) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
db.get(sql, params, (err, row) => {
|
|
19
|
+
if (err) reject(err);
|
|
20
|
+
else resolve(row);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function dbAll(db, sql, params = []) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
db.all(sql, params, (err, rows) => {
|
|
28
|
+
if (err) reject(err);
|
|
29
|
+
else resolve(rows);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runMigrations(db) {
|
|
35
|
+
// 创建 _migrations 版本表
|
|
36
|
+
await dbRun(db, `CREATE TABLE IF NOT EXISTS _migrations (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
name TEXT UNIQUE NOT NULL,
|
|
39
|
+
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
40
|
+
)`);
|
|
41
|
+
|
|
42
|
+
// 获取已执行的 migration
|
|
43
|
+
const applied = await dbAll(db, 'SELECT name FROM _migrations');
|
|
44
|
+
const appliedNames = new Set(applied.map(r => r.name));
|
|
45
|
+
|
|
46
|
+
// 读取并排序 migration 文件
|
|
47
|
+
const files = fs.readdirSync(MIGRATIONS_DIR)
|
|
48
|
+
.filter(f => f.endsWith('.js'))
|
|
49
|
+
.sort();
|
|
50
|
+
|
|
51
|
+
const helpers = { dbRun, dbGet, dbAll };
|
|
52
|
+
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const migration = require(path.join(MIGRATIONS_DIR, file));
|
|
55
|
+
|
|
56
|
+
if (appliedNames.has(migration.name)) continue;
|
|
57
|
+
|
|
58
|
+
logger.info({ type: 'migration', message: `Running: ${migration.name}` });
|
|
59
|
+
await migration.up(db, helpers);
|
|
60
|
+
await dbRun(db, 'INSERT INTO _migrations (name) VALUES (?)', [migration.name]);
|
|
61
|
+
logger.info({ type: 'migration', message: `Done: ${migration.name}` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { runMigrations, dbRun, dbGet, dbAll };
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@code2rich/jpage",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "即页 — 零配置 HTML/Markdown 即时预览与分享工具",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jpage": "bin/jpage.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node server.js",
|
|
17
|
+
"dev": "nodemon server.js",
|
|
18
|
+
"build": "node build.js",
|
|
19
|
+
"build:dev": "node build.js --dev",
|
|
20
|
+
"lint": "eslint .",
|
|
21
|
+
"lint:fix": "eslint . --fix",
|
|
22
|
+
"test": "node --test \"test/unit/**/*.test.js\" \"test/integration/**/*.test.js\"",
|
|
23
|
+
"test:unit": "node --test \"test/unit/**/*.test.js\"",
|
|
24
|
+
"test:integration": "node --test \"test/integration/**/*.test.js\""
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"archiver": "^7.0.1",
|
|
29
|
+
"bcryptjs": "^2.4.3",
|
|
30
|
+
"connect-sqlite3": "^0.9.15",
|
|
31
|
+
"docx": "^9.7.1",
|
|
32
|
+
"express": "^4.18.2",
|
|
33
|
+
"express-rate-limit": "^7.1.5",
|
|
34
|
+
"express-session": "^1.18.0",
|
|
35
|
+
"helmet": "^7.1.0",
|
|
36
|
+
"highlight.js": "^11.11.1",
|
|
37
|
+
"jszip": "^3.10.1",
|
|
38
|
+
"katex": "^0.16.47",
|
|
39
|
+
"marked": "^12.0.0",
|
|
40
|
+
"marked-highlight": "^2.2.4",
|
|
41
|
+
"mermaid": "^11.15.0",
|
|
42
|
+
"morgan": "^1.11.0",
|
|
43
|
+
"multer": "^1.4.5-lts.1",
|
|
44
|
+
"node-cron": "^4.2.1",
|
|
45
|
+
"nodemailer": "^9.0.1",
|
|
46
|
+
"sqlite3": "^6.0.1",
|
|
47
|
+
"zod": "^3.25.76"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^10.0.1",
|
|
51
|
+
"esbuild": "^0.28.1",
|
|
52
|
+
"eslint": "^10.5.0",
|
|
53
|
+
"nodemon": "^3.0.3",
|
|
54
|
+
"supertest": "^7.2.2"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"tar": "^7.5.16",
|
|
58
|
+
"node-gyp": "^11.0.0",
|
|
59
|
+
"make-fetch-happen": "^14.0.1",
|
|
60
|
+
"http-proxy-agent": "^7.0.2",
|
|
61
|
+
"@tootallnate/once": "^2.0.1"
|
|
62
|
+
}
|
|
63
|
+
}
|