skill-base 2.0.16 → 2.0.18
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/README.md +8 -2
- package/bin/skill-base.js +73 -28
- package/package.json +2 -2
- package/src/cappy.js +162 -50
- package/src/database.js +17 -17
- package/src/index.js +75 -22
- package/src/middleware/admin.js +3 -3
- package/src/middleware/auth.js +22 -22
- package/src/middleware/error.js +4 -4
- package/src/models/skill.js +6 -6
- package/src/models/user.js +10 -10
- package/src/models/version.js +6 -6
- package/src/routes/auth.js +17 -17
- package/src/routes/collaborators.js +28 -28
- package/src/routes/init.js +7 -7
- package/src/routes/publish.js +15 -15
- package/src/routes/skills.js +13 -13
- package/src/routes/users.js +9 -9
- package/src/utils/crypto.js +6 -6
- package/src/utils/detect-language.js +56 -0
- package/src/utils/permission.js +7 -7
- package/src/utils/zip.js +6 -6
- package/static/assets/{index-BHB0vddE.js → index-BVgsNsqr.js} +43 -43
- package/static/assets/index-ByONPaqz.css +1 -0
- package/static/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/static/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/static/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/static/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/static/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/static/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/static/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/static/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/static/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/static/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/static/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/static/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/static/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/static/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/static/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/static/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/static/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/static/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/static/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/static/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/static/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/static/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/static/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/static/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/static/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/static/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/static/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/static/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/static/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/static/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/static/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/static/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/static/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/static/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/static/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/static/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/static/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/static/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/static/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/static/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/static/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/static/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/static/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/static/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/static/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/static/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/static/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/static/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/static/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/static/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/static/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/static/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/static/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/static/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/static/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/static/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/static/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/static/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/static/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/static/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/static/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/static/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/static/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/static/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/static/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/static/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/static/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/static/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/static/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/static/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/static/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/static/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/static/index.html +2 -5
- package/static/assets/index-EVWfLxoq.css +0 -1
package/src/index.js
CHANGED
|
@@ -13,13 +13,36 @@ const fastify = require('fastify')({
|
|
|
13
13
|
bodyLimit: 100 * 1024 * 1024
|
|
14
14
|
});
|
|
15
15
|
const CappyMascot = require('./cappy');
|
|
16
|
+
const appLanguage = CappyMascot.detectSystemLanguage();
|
|
17
|
+
|
|
18
|
+
function pickMessage(message) {
|
|
19
|
+
if (typeof message === 'string') return message;
|
|
20
|
+
if (!message || typeof message !== 'object') return '';
|
|
21
|
+
return message[appLanguage] || message.en || message.zh || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function infoLog(message, ...args) {
|
|
25
|
+
console.log(pickMessage(message), ...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function debugLog(message, ...args) {
|
|
29
|
+
if (!isDebug) return;
|
|
30
|
+
console.log(`DEBUG: ${pickMessage(message)}`, ...args);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function errorLog(message, ...args) {
|
|
34
|
+
console.error(pickMessage(message), ...args);
|
|
35
|
+
}
|
|
16
36
|
|
|
17
37
|
if (isDebug) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
debugLog({
|
|
39
|
+
zh: '调试模式已启用。',
|
|
40
|
+
en: 'Debug mode is enabled.'
|
|
41
|
+
});
|
|
42
|
+
debugLog({ zh: '端口:', en: 'PORT:' }, process.env.PORT);
|
|
43
|
+
debugLog({ zh: '主机:', en: 'HOST:' }, process.env.HOST);
|
|
44
|
+
debugLog({ zh: '应用基础路径:', en: 'APP_BASE_PATH:' }, process.env.APP_BASE_PATH);
|
|
45
|
+
debugLog({ zh: '缓存上限(MB):', en: 'CACHE_MAX_MB:' }, process.env.CACHE_MAX_MB);
|
|
23
46
|
}
|
|
24
47
|
|
|
25
48
|
// 1. Normalize deploy prefix (APP_BASE_PATH)
|
|
@@ -47,18 +70,21 @@ function renderSpaHtml() {
|
|
|
47
70
|
// Main bootstrap
|
|
48
71
|
async function start() {
|
|
49
72
|
try {
|
|
50
|
-
|
|
73
|
+
debugLog({
|
|
74
|
+
zh: '正在注册核心插件...',
|
|
75
|
+
en: 'Registering core plugins...'
|
|
76
|
+
});
|
|
51
77
|
// 1. Plugins
|
|
52
78
|
// @fastify/cors — CORS
|
|
53
79
|
await fastify.register(require('@fastify/cors'), {
|
|
54
80
|
origin: true,
|
|
55
81
|
credentials: true
|
|
56
82
|
});
|
|
57
|
-
|
|
83
|
+
debugLog({ zh: '已注册 @fastify/cors', en: 'Registered @fastify/cors' });
|
|
58
84
|
|
|
59
85
|
// @fastify/cookie
|
|
60
86
|
await fastify.register(require('@fastify/cookie'));
|
|
61
|
-
|
|
87
|
+
debugLog({ zh: '已注册 @fastify/cookie', en: 'Registered @fastify/cookie' });
|
|
62
88
|
|
|
63
89
|
// @fastify/multipart — uploads
|
|
64
90
|
await fastify.register(require('@fastify/multipart'), {
|
|
@@ -66,7 +92,7 @@ async function start() {
|
|
|
66
92
|
fileSize: 100 * 1024 * 1024 // 100MB
|
|
67
93
|
}
|
|
68
94
|
});
|
|
69
|
-
|
|
95
|
+
debugLog({ zh: '已注册 @fastify/multipart', en: 'Registered @fastify/multipart' });
|
|
70
96
|
|
|
71
97
|
// Before static: with index:false, directory + trailing slash can 403 in send and skip notFoundHandler
|
|
72
98
|
fastify.route({
|
|
@@ -88,21 +114,33 @@ async function start() {
|
|
|
88
114
|
wildcard: true,
|
|
89
115
|
index: false
|
|
90
116
|
});
|
|
91
|
-
|
|
117
|
+
debugLog(
|
|
118
|
+
{ zh: '已注册 @fastify/static,目录为', en: 'Registered @fastify/static at' },
|
|
119
|
+
STATIC_ROOT
|
|
120
|
+
);
|
|
92
121
|
|
|
93
122
|
// 2. Custom middleware
|
|
94
|
-
|
|
123
|
+
debugLog({
|
|
124
|
+
zh: '正在注册自定义中间件...',
|
|
125
|
+
en: 'Registering custom middlewares...'
|
|
126
|
+
});
|
|
95
127
|
// Errors
|
|
96
128
|
await fastify.register(require('./middleware/error'));
|
|
97
129
|
// Auth (authenticate, createSession, …)
|
|
98
130
|
await fastify.register(require('./middleware/auth'));
|
|
99
131
|
// Admin (requireAdmin)
|
|
100
132
|
await fastify.register(require('./middleware/admin'));
|
|
101
|
-
|
|
133
|
+
debugLog({
|
|
134
|
+
zh: '自定义中间件已注册。',
|
|
135
|
+
en: 'Custom middlewares registered.'
|
|
136
|
+
});
|
|
102
137
|
|
|
103
138
|
// 3. API routes
|
|
104
139
|
const API_PREFIX = (APP_BASE_PATH + 'api/v1').replace(/\/+/g, '/');
|
|
105
|
-
|
|
140
|
+
debugLog(
|
|
141
|
+
{ zh: '正在注册 API 路由,前缀为:', en: 'Registering API routes with prefix:' },
|
|
142
|
+
API_PREFIX
|
|
143
|
+
);
|
|
106
144
|
|
|
107
145
|
// Health
|
|
108
146
|
fastify.get(`${API_PREFIX}/health`, async () => {
|
|
@@ -123,7 +161,7 @@ async function start() {
|
|
|
123
161
|
await fastify.register(require('./routes/publish'), { prefix: `${API_PREFIX}/skills` });
|
|
124
162
|
await fastify.register(require('./routes/collaborators'), { prefix: `${API_PREFIX}/skills` });
|
|
125
163
|
await fastify.register(require('./routes/users'), { prefix: `${API_PREFIX}/users` });
|
|
126
|
-
|
|
164
|
+
debugLog({ zh: 'API 路由已注册。', en: 'API routes registered.' });
|
|
127
165
|
|
|
128
166
|
// 4. SPA fallback for non-API routes
|
|
129
167
|
fastify.setNotFoundHandler(async (request, reply) => {
|
|
@@ -158,9 +196,9 @@ async function start() {
|
|
|
158
196
|
let cappy = null;
|
|
159
197
|
|
|
160
198
|
if (enableCappy) {
|
|
161
|
-
|
|
199
|
+
debugLog({ zh: 'CappyMascot 已启用。', en: 'CappyMascot is enabled.' });
|
|
162
200
|
// Cappy (decorate before listen)
|
|
163
|
-
cappy = new CappyMascot(PORT, APP_BASE_PATH);
|
|
201
|
+
cappy = new CappyMascot(PORT, APP_BASE_PATH, appLanguage);
|
|
164
202
|
fastify.decorate('cappy', cappy);
|
|
165
203
|
|
|
166
204
|
// Drive Cappy from onResponse; no route changes
|
|
@@ -171,29 +209,44 @@ async function start() {
|
|
|
171
209
|
const url = request.url.split('?')[0];
|
|
172
210
|
|
|
173
211
|
if (method === 'POST' && url === `${API_PREFIX}/users`) {
|
|
174
|
-
cappy.action(
|
|
212
|
+
cappy.action({
|
|
213
|
+
zh: '新用户已添加。又多一个人干活,系统依旧稳定。',
|
|
214
|
+
en: 'New user added. Another worker on the roster; the system stays steady.'
|
|
215
|
+
});
|
|
175
216
|
} else if (method === 'POST' && url === `${API_PREFIX}/skills/publish`) {
|
|
176
|
-
cappy.action(
|
|
217
|
+
cappy.action({
|
|
218
|
+
zh: '新的 Skill 或版本已发布。希望代码继续保持简单。',
|
|
219
|
+
en: 'New skill or version published. Hope the code stays simple.'
|
|
220
|
+
});
|
|
177
221
|
} else if (method === 'GET' && url.match(new RegExp(`^${API_PREFIX}/skills/[^/]+/versions/[^/]+/download/?$`))) {
|
|
178
|
-
cappy.action(
|
|
222
|
+
cappy.action({
|
|
223
|
+
zh: '有人下载了一个 Skill。代码开始流动了,Cappy 认可。',
|
|
224
|
+
en: 'Someone downloaded a skill. Code is moving—Cappy approves.'
|
|
225
|
+
});
|
|
179
226
|
}
|
|
180
227
|
}
|
|
181
228
|
done();
|
|
182
229
|
});
|
|
183
230
|
} else {
|
|
184
|
-
|
|
231
|
+
debugLog({ zh: 'CappyMascot 已禁用。', en: 'CappyMascot is disabled.' });
|
|
185
232
|
fastify.decorate('cappy', { action: () => {} });
|
|
186
233
|
}
|
|
187
234
|
|
|
188
235
|
await fastify.listen({ port: PORT, host: HOST });
|
|
189
|
-
|
|
236
|
+
infoLog({
|
|
237
|
+
zh: `\n📦 Skill Base 引擎已启动: http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}${APP_BASE_PATH}\n`,
|
|
238
|
+
en: `\n📦 Skill Base Engine Initialized at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}${APP_BASE_PATH}\n`
|
|
239
|
+
});
|
|
190
240
|
|
|
191
241
|
if (enableCappy && cappy) {
|
|
192
242
|
// Start Cappy loop
|
|
193
243
|
cappy.start();
|
|
194
244
|
}
|
|
195
245
|
} catch (err) {
|
|
196
|
-
|
|
246
|
+
errorLog(
|
|
247
|
+
{ zh: 'Skill Base 启动失败。', en: 'Skill Base failed to start.' },
|
|
248
|
+
err
|
|
249
|
+
);
|
|
197
250
|
process.exit(1);
|
|
198
251
|
}
|
|
199
252
|
}
|
package/src/middleware/admin.js
CHANGED
|
@@ -2,16 +2,16 @@ const fp = require('fastify-plugin');
|
|
|
2
2
|
|
|
3
3
|
async function adminPlugin(fastify, options) {
|
|
4
4
|
fastify.decorate('requireAdmin', async function(request, reply) {
|
|
5
|
-
// 1.
|
|
5
|
+
// 1. First call authenticate to ensure logged in
|
|
6
6
|
await fastify.authenticate(request, reply);
|
|
7
7
|
if (reply.sent) return;
|
|
8
8
|
|
|
9
|
-
// 2.
|
|
9
|
+
// 2. Check admin role
|
|
10
10
|
if (request.user.role !== 'admin') {
|
|
11
11
|
return reply.code(403).send({
|
|
12
12
|
ok: false,
|
|
13
13
|
error: 'forbidden',
|
|
14
|
-
detail: '
|
|
14
|
+
detail: 'Admin permission required'
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
});
|
package/src/middleware/auth.js
CHANGED
|
@@ -2,12 +2,12 @@ const fp = require('fastify-plugin');
|
|
|
2
2
|
const db = require('../database');
|
|
3
3
|
const { generateSessionId } = require('../utils/crypto');
|
|
4
4
|
|
|
5
|
-
// Session
|
|
5
|
+
// Session storage mode: 'memory' | 'sqlite', configured via environment variable
|
|
6
6
|
const SESSION_STORE = process.env.SESSION_STORE || 'memory';
|
|
7
|
-
// Session
|
|
7
|
+
// Session expiration time (default 7 days)
|
|
8
8
|
const SESSION_EXPIRES_DAYS = parseInt(process.env.SESSION_EXPIRES_DAYS || '7', 10);
|
|
9
9
|
|
|
10
|
-
// ============
|
|
10
|
+
// ============ Memory storage implementation ============
|
|
11
11
|
const memorySessions = new Map();
|
|
12
12
|
|
|
13
13
|
const memoryStore = {
|
|
@@ -20,7 +20,7 @@ const memoryStore = {
|
|
|
20
20
|
get(sessionId) {
|
|
21
21
|
const session = memorySessions.get(sessionId);
|
|
22
22
|
if (!session) return null;
|
|
23
|
-
//
|
|
23
|
+
// Check expiration
|
|
24
24
|
if (Date.now() > session.expiresAt) {
|
|
25
25
|
memorySessions.delete(sessionId);
|
|
26
26
|
return null;
|
|
@@ -30,7 +30,7 @@ const memoryStore = {
|
|
|
30
30
|
destroy(sessionId) {
|
|
31
31
|
memorySessions.delete(sessionId);
|
|
32
32
|
},
|
|
33
|
-
//
|
|
33
|
+
// Clean up expired sessions
|
|
34
34
|
cleanup() {
|
|
35
35
|
const now = Date.now();
|
|
36
36
|
for (const [id, session] of memorySessions) {
|
|
@@ -41,7 +41,7 @@ const memoryStore = {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
// ============ SQLite
|
|
44
|
+
// ============ SQLite storage implementation ============
|
|
45
45
|
const sqliteStore = {
|
|
46
46
|
create(userId) {
|
|
47
47
|
const sessionId = generateSessionId();
|
|
@@ -62,32 +62,32 @@ const sqliteStore = {
|
|
|
62
62
|
destroy(sessionId) {
|
|
63
63
|
db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId);
|
|
64
64
|
},
|
|
65
|
-
//
|
|
65
|
+
// Clean up expired sessions
|
|
66
66
|
cleanup() {
|
|
67
67
|
db.prepare("DELETE FROM sessions WHERE expires_at <= datetime('now')").run();
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// Select storage implementation based on configuration
|
|
72
72
|
const sessionStore = SESSION_STORE === 'sqlite' ? sqliteStore : memoryStore;
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Clean up expired sessions on startup
|
|
75
75
|
sessionStore.cleanup();
|
|
76
76
|
|
|
77
|
-
//
|
|
77
|
+
// Periodically clean up expired sessions (every hour)
|
|
78
78
|
setInterval(() => sessionStore.cleanup(), 60 * 60 * 1000);
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
80
|
+
// Authentication middleware decorator - registered as Fastify's decorate + preHandler
|
|
81
|
+
// Usage: use via { preHandler: [fastify.authenticate] } in routes
|
|
82
82
|
async function authPlugin(fastify, options) {
|
|
83
|
-
//
|
|
83
|
+
// Expose sessionStore for routes to use
|
|
84
84
|
fastify.decorate('sessionStore', sessionStore);
|
|
85
85
|
fastify.decorate('createSession', (userId) => sessionStore.create(userId));
|
|
86
86
|
fastify.decorate('destroySession', (sessionId) => sessionStore.destroy(sessionId));
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// Authentication decorator
|
|
89
89
|
fastify.decorate('authenticate', async function(request, reply) {
|
|
90
|
-
// 1.
|
|
90
|
+
// 1. Try Cookie Session first
|
|
91
91
|
const sessionId = request.cookies?.session_id;
|
|
92
92
|
if (sessionId) {
|
|
93
93
|
const session = sessionStore.get(sessionId);
|
|
@@ -97,7 +97,7 @@ async function authPlugin(fastify, options) {
|
|
|
97
97
|
return reply.code(401).send({
|
|
98
98
|
ok: false,
|
|
99
99
|
error: 'account_disabled',
|
|
100
|
-
detail: '
|
|
100
|
+
detail: 'Account has been disabled'
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
request.user = user;
|
|
@@ -105,7 +105,7 @@ async function authPlugin(fastify, options) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// 2.
|
|
108
|
+
// 2. Then try Bearer Token (PAT)
|
|
109
109
|
const authHeader = request.headers.authorization;
|
|
110
110
|
if (authHeader?.startsWith('Bearer ')) {
|
|
111
111
|
const token = authHeader.slice(7);
|
|
@@ -116,24 +116,24 @@ async function authPlugin(fastify, options) {
|
|
|
116
116
|
return reply.code(401).send({
|
|
117
117
|
ok: false,
|
|
118
118
|
error: 'account_disabled',
|
|
119
|
-
detail: '
|
|
119
|
+
detail: 'Account has been disabled'
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
//
|
|
122
|
+
// Update last_used_at
|
|
123
123
|
db.prepare('UPDATE personal_access_tokens SET last_used_at = CURRENT_TIMESTAMP WHERE token = ?').run(token);
|
|
124
124
|
request.user = user;
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// 3.
|
|
129
|
+
// 3. Not authenticated
|
|
130
130
|
reply.code(401).send({ detail: 'Authentication required' });
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
//
|
|
133
|
+
// Optional authentication (not enforced, parse if present)
|
|
134
134
|
fastify.decorate('optionalAuth', async function(request, reply) {
|
|
135
135
|
try {
|
|
136
|
-
//
|
|
136
|
+
// Reuse authenticate logic but don't throw error
|
|
137
137
|
const sessionId = request.cookies?.session_id;
|
|
138
138
|
if (sessionId) {
|
|
139
139
|
const session = sessionStore.get(sessionId);
|
package/src/middleware/error.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Fastify
|
|
1
|
+
// Fastify error handler plugin
|
|
2
2
|
async function errorHandler(fastify, options) {
|
|
3
3
|
fastify.setErrorHandler(function (error, request, reply) {
|
|
4
4
|
const statusCode = error.statusCode || 500;
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Log error
|
|
7
7
|
if (statusCode >= 500) {
|
|
8
8
|
request.log.error(error);
|
|
9
9
|
} else {
|
|
@@ -16,8 +16,8 @@ async function errorHandler(fastify, options) {
|
|
|
16
16
|
});
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
19
|
+
// Note: 404 handling is unified in setNotFoundHandler in index.js
|
|
20
|
+
// Includes logic for returning JSON 404 for API routes and corresponding HTML for page routes
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
module.exports = errorHandler;
|
package/src/models/skill.js
CHANGED
|
@@ -11,7 +11,7 @@ function queryById(id) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const SkillModel = {
|
|
14
|
-
//
|
|
14
|
+
// Find Skill by ID (includes owner info)
|
|
15
15
|
findById(id) {
|
|
16
16
|
return modelCache.remember(
|
|
17
17
|
modelCache.keys.skill(id),
|
|
@@ -20,7 +20,7 @@ const SkillModel = {
|
|
|
20
20
|
);
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Search/List Skills (supports keyword search on name or description)
|
|
24
24
|
search(query) {
|
|
25
25
|
const normalizedQuery = query ? String(query) : '';
|
|
26
26
|
return modelCache.remember(
|
|
@@ -48,7 +48,7 @@ const SkillModel = {
|
|
|
48
48
|
);
|
|
49
49
|
},
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// Create new Skill
|
|
52
52
|
create(id, name, description, ownerId) {
|
|
53
53
|
db.prepare(`
|
|
54
54
|
INSERT INTO skills (id, name, description, owner_id)
|
|
@@ -58,7 +58,7 @@ const SkillModel = {
|
|
|
58
58
|
return queryById(id);
|
|
59
59
|
},
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Update Skill
|
|
62
62
|
update(id, name, description) {
|
|
63
63
|
const fields = [];
|
|
64
64
|
const values = [];
|
|
@@ -83,7 +83,7 @@ const SkillModel = {
|
|
|
83
83
|
return queryById(id);
|
|
84
84
|
},
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// Update latest_version and updated_at
|
|
87
87
|
updateLatestVersion(id, version) {
|
|
88
88
|
db.prepare(`
|
|
89
89
|
UPDATE skills SET latest_version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
|
@@ -91,7 +91,7 @@ const SkillModel = {
|
|
|
91
91
|
modelCache.invalidateSkill(id);
|
|
92
92
|
},
|
|
93
93
|
|
|
94
|
-
//
|
|
94
|
+
// Check if Skill exists
|
|
95
95
|
exists(id) {
|
|
96
96
|
return modelCache.remember(
|
|
97
97
|
modelCache.keys.skillExists(id),
|
package/src/models/user.js
CHANGED
|
@@ -6,7 +6,7 @@ function queryById(id) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const UserModel = {
|
|
9
|
-
//
|
|
9
|
+
// Find user by ID
|
|
10
10
|
findById(id) {
|
|
11
11
|
return modelCache.remember(
|
|
12
12
|
modelCache.keys.userBasic(id),
|
|
@@ -15,19 +15,19 @@ const UserModel = {
|
|
|
15
15
|
);
|
|
16
16
|
},
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Find user by username (includes password_hash for login verification)
|
|
19
19
|
findByUsername(username) {
|
|
20
20
|
return db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Create user
|
|
24
24
|
create(username, passwordHash, role = 'developer') {
|
|
25
25
|
const result = db.prepare('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)').run(username, passwordHash, role);
|
|
26
26
|
modelCache.invalidateUser(result.lastInsertRowid);
|
|
27
27
|
return queryById(result.lastInsertRowid);
|
|
28
28
|
},
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// List users (supports pagination and search)
|
|
31
31
|
list({ q, status, page = 1, limit = 20 } = {}) {
|
|
32
32
|
let sql = 'SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE 1=1';
|
|
33
33
|
let countSql = 'SELECT COUNT(*) as total FROM users WHERE 1=1';
|
|
@@ -56,7 +56,7 @@ const UserModel = {
|
|
|
56
56
|
return { users, total, page, limit };
|
|
57
57
|
},
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Update username
|
|
60
60
|
updateUsername(id, username) {
|
|
61
61
|
const result = db.prepare(
|
|
62
62
|
"UPDATE users SET username = ?, updated_at = datetime('now') WHERE id = ?"
|
|
@@ -67,7 +67,7 @@ const UserModel = {
|
|
|
67
67
|
return result.changes > 0;
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// Update password
|
|
71
71
|
updatePassword(id, passwordHash) {
|
|
72
72
|
const result = db.prepare(
|
|
73
73
|
"UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?"
|
|
@@ -78,7 +78,7 @@ const UserModel = {
|
|
|
78
78
|
return result.changes > 0;
|
|
79
79
|
},
|
|
80
80
|
|
|
81
|
-
//
|
|
81
|
+
// Update user (for admin use)
|
|
82
82
|
update(id, fields) {
|
|
83
83
|
const allowed = ['role', 'status', 'username', 'name'];
|
|
84
84
|
const sets = [];
|
|
@@ -103,7 +103,7 @@ const UserModel = {
|
|
|
103
103
|
return result.changes > 0;
|
|
104
104
|
},
|
|
105
105
|
|
|
106
|
-
//
|
|
106
|
+
// Reset password (for admin use)
|
|
107
107
|
resetPassword(id, passwordHash) {
|
|
108
108
|
const result = db.prepare(
|
|
109
109
|
"UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?"
|
|
@@ -114,7 +114,7 @@ const UserModel = {
|
|
|
114
114
|
return result.changes > 0;
|
|
115
115
|
},
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Find user details (includes creator info)
|
|
118
118
|
findByIdWithCreator(id) {
|
|
119
119
|
return db.prepare(`
|
|
120
120
|
SELECT u.id, u.username, u.name, u.role, u.status, u.created_at, u.updated_at,
|
|
@@ -125,7 +125,7 @@ const UserModel = {
|
|
|
125
125
|
`).get(id);
|
|
126
126
|
},
|
|
127
127
|
|
|
128
|
-
//
|
|
128
|
+
// Update username and name
|
|
129
129
|
updateProfile(id, { username, name }) {
|
|
130
130
|
const sets = [];
|
|
131
131
|
const params = [];
|
package/src/models/version.js
CHANGED
|
@@ -2,7 +2,7 @@ const db = require('../database');
|
|
|
2
2
|
const modelCache = require('../utils/model-cache');
|
|
3
3
|
|
|
4
4
|
const VersionModel = {
|
|
5
|
-
//
|
|
5
|
+
// Create new version
|
|
6
6
|
create(skillId, version, changelog, zipPath, uploaderId, description) {
|
|
7
7
|
const result = db.prepare(`
|
|
8
8
|
INSERT INTO skill_versions (skill_id, version, changelog, zip_path, uploader_id, description)
|
|
@@ -12,7 +12,7 @@ const VersionModel = {
|
|
|
12
12
|
return this.findById(result.lastInsertRowid);
|
|
13
13
|
},
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// Find version by ID
|
|
16
16
|
findById(id) {
|
|
17
17
|
return db.prepare(`
|
|
18
18
|
SELECT sv.*, u.username as uploader_username, u.name as uploader_name
|
|
@@ -22,7 +22,7 @@ const VersionModel = {
|
|
|
22
22
|
`).get(id);
|
|
23
23
|
},
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Find by skill_id and version
|
|
26
26
|
findByVersion(skillId, version) {
|
|
27
27
|
return modelCache.remember(
|
|
28
28
|
modelCache.keys.skillVersion(skillId, version),
|
|
@@ -36,7 +36,7 @@ const VersionModel = {
|
|
|
36
36
|
);
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// List all versions of a Skill (sorted by created_at descending)
|
|
40
40
|
listBySkillId(skillId) {
|
|
41
41
|
return modelCache.remember(
|
|
42
42
|
modelCache.keys.skillVersions(skillId),
|
|
@@ -51,7 +51,7 @@ const VersionModel = {
|
|
|
51
51
|
);
|
|
52
52
|
},
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// Get the latest version of a Skill
|
|
55
55
|
getLatest(skillId) {
|
|
56
56
|
return modelCache.remember(
|
|
57
57
|
modelCache.keys.skillLatest(skillId),
|
|
@@ -67,7 +67,7 @@ const VersionModel = {
|
|
|
67
67
|
);
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
-
//
|
|
70
|
+
// Update version description and changelog
|
|
71
71
|
update(id, description, changelog) {
|
|
72
72
|
const existing = this.findById(id);
|
|
73
73
|
db.prepare(`
|