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/routes/auth.js
CHANGED
|
@@ -3,7 +3,7 @@ const UserModel = require('../models/user');
|
|
|
3
3
|
const { verifyPassword, hashPassword, generateCliCode, generatePAT } = require('../utils/crypto');
|
|
4
4
|
|
|
5
5
|
async function authRoutes(fastify, options) {
|
|
6
|
-
// POST /login -
|
|
6
|
+
// POST /login - User login
|
|
7
7
|
fastify.post('/login', async (request, reply) => {
|
|
8
8
|
const { username, password } = request.body || {};
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ async function authRoutes(fastify, options) {
|
|
|
11
11
|
return reply.code(400).send({ detail: 'Username and password are required' });
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Find user
|
|
15
15
|
const user = UserModel.findByUsername(username);
|
|
16
16
|
if (!user) {
|
|
17
17
|
return reply.code(401).send({ detail: 'Invalid username or password' });
|
|
@@ -38,7 +38,7 @@ async function authRoutes(fastify, options) {
|
|
|
38
38
|
return { ok: true, user: { id: user.id, username: user.username, name: user.name || null, role: user.role } };
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// POST /logout -
|
|
41
|
+
// POST /logout - User logout
|
|
42
42
|
fastify.post('/logout', async (request, reply) => {
|
|
43
43
|
const sessionId = request.cookies?.session_id;
|
|
44
44
|
if (sessionId) {
|
|
@@ -48,14 +48,14 @@ async function authRoutes(fastify, options) {
|
|
|
48
48
|
return { ok: true };
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
// POST /cli-code/generate -
|
|
51
|
+
// POST /cli-code/generate - Generate CLI verification code
|
|
52
52
|
fastify.post('/cli-code/generate', {
|
|
53
53
|
preHandler: [fastify.authenticate]
|
|
54
54
|
}, async (request, reply) => {
|
|
55
55
|
const code = generateCliCode();
|
|
56
56
|
const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Write to cli_auth_codes table
|
|
59
59
|
db.prepare(`
|
|
60
60
|
INSERT INTO cli_auth_codes (code, user_id, expires_at, used)
|
|
61
61
|
VALUES (?, ?, ?, FALSE)
|
|
@@ -64,7 +64,7 @@ async function authRoutes(fastify, options) {
|
|
|
64
64
|
return { ok: true, code, expires_at: expiresAt };
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
// POST /cli-code/verify -
|
|
67
|
+
// POST /cli-code/verify - Verify CLI verification code
|
|
68
68
|
fastify.post('/cli-code/verify', async (request, reply) => {
|
|
69
69
|
const { code } = request.body || {};
|
|
70
70
|
|
|
@@ -72,7 +72,7 @@ async function authRoutes(fastify, options) {
|
|
|
72
72
|
return reply.code(400).send({ detail: 'Code is required' });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// Find verification code: unused and not expired
|
|
76
76
|
const codeRecord = db.prepare(`
|
|
77
77
|
SELECT * FROM cli_auth_codes
|
|
78
78
|
WHERE code = ? AND used = FALSE AND expires_at > datetime('now')
|
|
@@ -82,17 +82,17 @@ async function authRoutes(fastify, options) {
|
|
|
82
82
|
return reply.code(401).send({ detail: 'Invalid or expired code' });
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// Mark as used
|
|
86
86
|
db.prepare('UPDATE cli_auth_codes SET used = TRUE WHERE code = ?').run(code);
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// Generate PAT
|
|
89
89
|
const token = generatePAT();
|
|
90
90
|
db.prepare(`
|
|
91
91
|
INSERT INTO personal_access_tokens (token, user_id, description)
|
|
92
92
|
VALUES (?, ?, ?)
|
|
93
93
|
`).run(token, codeRecord.user_id, 'CLI generated token');
|
|
94
94
|
|
|
95
|
-
//
|
|
95
|
+
// Get user info
|
|
96
96
|
const user = UserModel.findById(codeRecord.user_id);
|
|
97
97
|
|
|
98
98
|
return {
|
|
@@ -102,31 +102,31 @@ async function authRoutes(fastify, options) {
|
|
|
102
102
|
};
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
// GET /me -
|
|
105
|
+
// GET /me - Get current user info
|
|
106
106
|
fastify.get('/me', {
|
|
107
107
|
preHandler: [fastify.authenticate]
|
|
108
108
|
}, async (request, reply) => {
|
|
109
109
|
return request.user;
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
// PATCH /me -
|
|
112
|
+
// PATCH /me - Update personal info (username and name)
|
|
113
113
|
fastify.patch('/me', {
|
|
114
114
|
preHandler: [fastify.authenticate]
|
|
115
115
|
}, async (request, reply) => {
|
|
116
116
|
const { username, name } = request.body || {};
|
|
117
117
|
|
|
118
|
-
//
|
|
118
|
+
// At least one field must be provided
|
|
119
119
|
if (username === undefined && name === undefined) {
|
|
120
120
|
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'At least one field must be provided' });
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
//
|
|
123
|
+
// Validate username
|
|
124
124
|
if (username !== undefined) {
|
|
125
125
|
if (typeof username !== 'string' || username.trim().length === 0) {
|
|
126
126
|
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Username cannot be empty' });
|
|
127
127
|
}
|
|
128
128
|
const trimmed = username.trim();
|
|
129
|
-
//
|
|
129
|
+
// Check if username already exists (exclude self)
|
|
130
130
|
const existing = UserModel.findByUsername(trimmed);
|
|
131
131
|
if (existing && existing.id !== request.user.id) {
|
|
132
132
|
return reply.code(400).send({ ok: false, error: 'username_exists', detail: 'Username already exists' });
|
|
@@ -142,7 +142,7 @@ async function authRoutes(fastify, options) {
|
|
|
142
142
|
return reply.send({ ok: true, user: updated });
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
// POST /me/change-password -
|
|
145
|
+
// POST /me/change-password - Change password
|
|
146
146
|
fastify.post('/me/change-password', {
|
|
147
147
|
preHandler: [fastify.authenticate]
|
|
148
148
|
}, async (request, reply) => {
|
|
@@ -156,7 +156,7 @@ async function authRoutes(fastify, options) {
|
|
|
156
156
|
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'New password must be at least 6 characters' });
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// Verify old password - need to get user info with password
|
|
160
160
|
const user = UserModel.findByUsername(request.user.username);
|
|
161
161
|
|
|
162
162
|
if (!verifyPassword(old_password, user.password_hash)) {
|
|
@@ -5,11 +5,11 @@ const { invalidateSkill } = require('../utils/model-cache');
|
|
|
5
5
|
|
|
6
6
|
async function collaboratorsRoutes(fastify, options) {
|
|
7
7
|
|
|
8
|
-
// GET /:skill_id/collaborators -
|
|
8
|
+
// GET /:skill_id/collaborators - Get collaborators list (public)
|
|
9
9
|
fastify.get('/:skill_id/collaborators', async (request, reply) => {
|
|
10
10
|
const { skill_id } = request.params;
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// Check if Skill exists
|
|
13
13
|
const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
|
|
14
14
|
if (!skill) {
|
|
15
15
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
|
|
@@ -42,25 +42,25 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
42
42
|
return reply.send({ skill_id, collaborators: result });
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
// POST /:skill_id/collaborators -
|
|
45
|
+
// POST /:skill_id/collaborators - Add collaborator (owner/admin)
|
|
46
46
|
fastify.post('/:skill_id/collaborators', {
|
|
47
47
|
preHandler: [fastify.authenticate]
|
|
48
48
|
}, async (request, reply) => {
|
|
49
49
|
const { skill_id } = request.params;
|
|
50
50
|
const { user_id, username } = request.body || {};
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// Check if Skill exists
|
|
53
53
|
const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
|
|
54
54
|
if (!skill) {
|
|
55
55
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Permission check
|
|
59
59
|
if (!canManageSkill(request.user, skill_id)) {
|
|
60
60
|
return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// Find target user
|
|
64
64
|
let targetUser;
|
|
65
65
|
if (user_id) {
|
|
66
66
|
targetUser = UserModel.findById(parseInt(user_id));
|
|
@@ -72,7 +72,7 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
72
72
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// Check if already a collaborator
|
|
76
76
|
const existing = db.prepare(
|
|
77
77
|
'SELECT id FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
|
|
78
78
|
).get(skill_id, targetUser.id);
|
|
@@ -81,7 +81,7 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
81
81
|
return reply.code(400).send({ ok: false, error: 'already_collaborator', detail: 'User is already a collaborator' });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// Add collaborator
|
|
85
85
|
const result = db.prepare(
|
|
86
86
|
'INSERT INTO skill_collaborators (skill_id, user_id, role, created_by) VALUES (?, ?, ?, ?)'
|
|
87
87
|
).run(skill_id, targetUser.id, 'collaborator', request.user.id);
|
|
@@ -98,24 +98,24 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
// DELETE /:skill_id/collaborators/:user_id -
|
|
101
|
+
// DELETE /:skill_id/collaborators/:user_id - Remove collaborator (owner/admin)
|
|
102
102
|
fastify.delete('/:skill_id/collaborators/:user_id', {
|
|
103
103
|
preHandler: [fastify.authenticate]
|
|
104
104
|
}, async (request, reply) => {
|
|
105
105
|
const { skill_id, user_id } = request.params;
|
|
106
106
|
|
|
107
|
-
//
|
|
107
|
+
// Check if Skill exists
|
|
108
108
|
const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
|
|
109
109
|
if (!skill) {
|
|
110
110
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
//
|
|
113
|
+
// Permission check
|
|
114
114
|
if (!canManageSkill(request.user, skill_id)) {
|
|
115
115
|
return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
118
|
+
// Check collaborator record
|
|
119
119
|
const collaborator = db.prepare(
|
|
120
120
|
'SELECT id, role FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
|
|
121
121
|
).get(skill_id, parseInt(user_id));
|
|
@@ -124,7 +124,7 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
124
124
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Collaborator not found' });
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
//
|
|
127
|
+
// Cannot remove owner
|
|
128
128
|
if (collaborator.role === 'owner') {
|
|
129
129
|
return reply.code(400).send({ ok: false, error: 'cannot_remove_owner', detail: 'Cannot remove the owner' });
|
|
130
130
|
}
|
|
@@ -136,7 +136,7 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
136
136
|
return reply.send({ ok: true, message: 'Collaborator removed' });
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
// POST /:skill_id/transfer-ownership -
|
|
139
|
+
// POST /:skill_id/transfer-ownership - Transfer ownership (owner/admin, transaction)
|
|
140
140
|
fastify.post('/:skill_id/transfer-ownership', {
|
|
141
141
|
preHandler: [fastify.authenticate]
|
|
142
142
|
}, async (request, reply) => {
|
|
@@ -147,13 +147,13 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
147
147
|
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'New owner must be specified' });
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
//
|
|
150
|
+
// Check Skill and get current owner
|
|
151
151
|
const skill = db.prepare('SELECT id, owner_id FROM skills WHERE id = ?').get(skill_id);
|
|
152
152
|
if (!skill) {
|
|
153
153
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
//
|
|
156
|
+
// Permission check
|
|
157
157
|
if (!canManageSkill(request.user, skill_id)) {
|
|
158
158
|
return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
|
|
159
159
|
}
|
|
@@ -164,23 +164,23 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
164
164
|
return reply.code(400).send({ ok: false, error: 'same_owner', detail: 'New owner is the same as current owner' });
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
//
|
|
167
|
+
// Check if new owner exists
|
|
168
168
|
const newOwner = UserModel.findById(newOwnerId);
|
|
169
169
|
if (!newOwner) {
|
|
170
170
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
//
|
|
173
|
+
// Transaction operation
|
|
174
174
|
const transferTx = db.transaction(() => {
|
|
175
|
-
// 1.
|
|
175
|
+
// 1. Update skills table owner_id
|
|
176
176
|
db.prepare('UPDATE skills SET owner_id = ?, updated_at = datetime("now") WHERE id = ?')
|
|
177
177
|
.run(newOwnerId, skill_id);
|
|
178
178
|
|
|
179
|
-
// 2.
|
|
179
|
+
// 2. Demote original owner to collaborator
|
|
180
180
|
db.prepare('UPDATE skill_collaborators SET role = "collaborator" WHERE skill_id = ? AND user_id = ?')
|
|
181
181
|
.run(skill_id, skill.owner_id);
|
|
182
182
|
|
|
183
|
-
// 3.
|
|
183
|
+
// 3. Promote new owner to owner (insert if not exists)
|
|
184
184
|
const existing = db.prepare(
|
|
185
185
|
'SELECT id FROM skill_collaborators WHERE skill_id = ? AND user_id = ?'
|
|
186
186
|
).get(skill_id, newOwnerId);
|
|
@@ -204,14 +204,14 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
// DELETE /:skill_id -
|
|
207
|
+
// DELETE /:skill_id - Delete Skill (owner/admin, requires confirm parameter, transaction)
|
|
208
208
|
fastify.delete('/:skill_id', {
|
|
209
209
|
preHandler: [fastify.authenticate]
|
|
210
210
|
}, async (request, reply) => {
|
|
211
211
|
const { skill_id } = request.params;
|
|
212
212
|
const { confirm } = request.query;
|
|
213
213
|
|
|
214
|
-
//
|
|
214
|
+
// Confirm parameter validation
|
|
215
215
|
if (confirm !== skill_id) {
|
|
216
216
|
return reply.code(400).send({
|
|
217
217
|
ok: false,
|
|
@@ -220,22 +220,22 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// Check if Skill exists
|
|
224
224
|
const skill = db.prepare('SELECT id FROM skills WHERE id = ?').get(skill_id);
|
|
225
225
|
if (!skill) {
|
|
226
226
|
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'Skill not found' });
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
//
|
|
229
|
+
// Permission check
|
|
230
230
|
if (!canManageSkill(request.user, skill_id)) {
|
|
231
231
|
return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
//
|
|
234
|
+
// Get versions count (for response)
|
|
235
235
|
const versionsCount = db.prepare('SELECT COUNT(*) as count FROM skill_versions WHERE skill_id = ?')
|
|
236
236
|
.get(skill_id).count;
|
|
237
237
|
|
|
238
|
-
//
|
|
238
|
+
// Transaction delete
|
|
239
239
|
const deleteSkillTx = db.transaction(() => {
|
|
240
240
|
db.prepare('DELETE FROM skill_versions WHERE skill_id = ?').run(skill_id);
|
|
241
241
|
db.prepare('DELETE FROM skill_collaborators WHERE skill_id = ?').run(skill_id);
|
|
@@ -245,7 +245,7 @@ async function collaboratorsRoutes(fastify, options) {
|
|
|
245
245
|
deleteSkillTx();
|
|
246
246
|
invalidateSkill(skill_id);
|
|
247
247
|
|
|
248
|
-
//
|
|
248
|
+
// Delete files from filesystem
|
|
249
249
|
const fs = require('fs');
|
|
250
250
|
const path = require('path');
|
|
251
251
|
const { getDataDir } = require('../utils/zip');
|
package/src/routes/init.js
CHANGED
|
@@ -2,13 +2,13 @@ const bcrypt = require('bcryptjs');
|
|
|
2
2
|
const db = require('../database');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* System initialization routes
|
|
6
6
|
* @param {import('fastify').FastifyInstance} fastify
|
|
7
7
|
*/
|
|
8
8
|
async function initRoutes(fastify) {
|
|
9
9
|
/**
|
|
10
10
|
* GET /api/v1/init/status
|
|
11
|
-
*
|
|
11
|
+
* Check if system needs initialization (whether admin user exists)
|
|
12
12
|
*/
|
|
13
13
|
fastify.get('/status', async (request, reply) => {
|
|
14
14
|
const adminCount = db.prepare(
|
|
@@ -22,11 +22,11 @@ async function initRoutes(fastify) {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* POST /api/v1/init/setup
|
|
25
|
-
*
|
|
25
|
+
* Initialize system admin account
|
|
26
26
|
* Body: { username, password }
|
|
27
27
|
*/
|
|
28
28
|
fastify.post('/setup', async (request, reply) => {
|
|
29
|
-
//
|
|
29
|
+
// Check if already initialized
|
|
30
30
|
const adminCount = db.prepare(
|
|
31
31
|
"SELECT COUNT(*) as count FROM users WHERE role = 'admin'"
|
|
32
32
|
).get();
|
|
@@ -39,7 +39,7 @@ async function initRoutes(fastify) {
|
|
|
39
39
|
|
|
40
40
|
const { username, password } = request.body || {};
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Validate input
|
|
43
43
|
if (!username || !password) {
|
|
44
44
|
return reply.code(400).send({
|
|
45
45
|
error: 'Username and password are required'
|
|
@@ -58,7 +58,7 @@ async function initRoutes(fastify) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Check if username already exists
|
|
62
62
|
const existingUser = db.prepare(
|
|
63
63
|
'SELECT id FROM users WHERE username = ?'
|
|
64
64
|
).get(username);
|
|
@@ -69,7 +69,7 @@ async function initRoutes(fastify) {
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Create admin account
|
|
73
73
|
const passwordHash = bcrypt.hashSync(password, 10);
|
|
74
74
|
const result = db.prepare(
|
|
75
75
|
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
|
package/src/routes/publish.js
CHANGED
|
@@ -7,7 +7,7 @@ const { ensureSkillDir, generateVersionNumber, getZipPath, getZipRelativePath }
|
|
|
7
7
|
const { canPublishSkill } = require('../utils/permission');
|
|
8
8
|
|
|
9
9
|
async function publishRoutes(fastify, options) {
|
|
10
|
-
// POST /publish -
|
|
10
|
+
// POST /publish - Publish new version
|
|
11
11
|
fastify.post('/publish', {
|
|
12
12
|
preHandler: [fastify.authenticate]
|
|
13
13
|
}, async (request, reply) => {
|
|
@@ -15,12 +15,12 @@ async function publishRoutes(fastify, options) {
|
|
|
15
15
|
let zipBuffer = null;
|
|
16
16
|
let zipFilename = null;
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Parse multipart data
|
|
19
19
|
const parts = request.parts();
|
|
20
20
|
for await (const part of parts) {
|
|
21
21
|
if (part.type === 'file') {
|
|
22
22
|
if (part.fieldname === 'zip_file') {
|
|
23
|
-
//
|
|
23
|
+
// Read file into buffer
|
|
24
24
|
const chunks = [];
|
|
25
25
|
for await (const chunk of part.file) {
|
|
26
26
|
chunks.push(chunk);
|
|
@@ -33,19 +33,19 @@ async function publishRoutes(fastify, options) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Check required zip file
|
|
37
37
|
if (!zipBuffer) {
|
|
38
38
|
return reply.code(400).send({ detail: 'zip_file is required' });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const { skill_id, name, description, changelog } = fields;
|
|
42
42
|
|
|
43
|
-
//
|
|
43
|
+
// Check skill_id
|
|
44
44
|
if (!skill_id) {
|
|
45
45
|
return reply.code(400).send({ detail: 'skill_id is required' });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Check publish permission
|
|
49
49
|
if (!canPublishSkill(request.user, skill_id)) {
|
|
50
50
|
return reply.code(403).send({
|
|
51
51
|
ok: false,
|
|
@@ -54,15 +54,15 @@ async function publishRoutes(fastify, options) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// Check if skill exists
|
|
58
58
|
const skillExists = SkillModel.exists(skill_id);
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// If skill does not exist, name field is required to create new skill
|
|
61
61
|
if (!skillExists) {
|
|
62
62
|
if (!name) {
|
|
63
63
|
return reply.code(400).send({ detail: 'name is required for new skill' });
|
|
64
64
|
}
|
|
65
|
-
//
|
|
65
|
+
// Use transaction to create new skill and add owner collaborator record
|
|
66
66
|
const createSkillTx = db.transaction(() => {
|
|
67
67
|
SkillModel.create(skill_id, name, description || '', request.user.id);
|
|
68
68
|
db.prepare(
|
|
@@ -72,20 +72,20 @@ async function publishRoutes(fastify, options) {
|
|
|
72
72
|
createSkillTx();
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// Generate version number
|
|
76
76
|
const version = generateVersionNumber();
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Ensure directory exists
|
|
79
79
|
ensureSkillDir(skill_id);
|
|
80
80
|
|
|
81
|
-
//
|
|
81
|
+
// Write zip file
|
|
82
82
|
const zipPath = getZipPath(skill_id, version);
|
|
83
83
|
fs.writeFileSync(zipPath, zipBuffer);
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// Get relative path (stored in database)
|
|
86
86
|
const zipRelativePath = getZipRelativePath(skill_id, version);
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// Create version record
|
|
89
89
|
const versionRecord = VersionModel.create(
|
|
90
90
|
skill_id,
|
|
91
91
|
version,
|
|
@@ -95,7 +95,7 @@ async function publishRoutes(fastify, options) {
|
|
|
95
95
|
description || ''
|
|
96
96
|
);
|
|
97
97
|
|
|
98
|
-
//
|
|
98
|
+
// Update skill's latest version
|
|
99
99
|
SkillModel.updateLatestVersion(skill_id, version);
|
|
100
100
|
invalidateSkill(skill_id);
|
|
101
101
|
|
package/src/routes/skills.js
CHANGED
|
@@ -5,7 +5,7 @@ const VersionModel = require('../models/version');
|
|
|
5
5
|
const { getZipPath, resolveZipPath } = require('../utils/zip');
|
|
6
6
|
const { canManageSkill } = require('../utils/permission');
|
|
7
7
|
|
|
8
|
-
//
|
|
8
|
+
// Format skill, convert owner to object
|
|
9
9
|
function formatSkill(skill, currentUser) {
|
|
10
10
|
if (!skill) return null;
|
|
11
11
|
const result = {
|
|
@@ -41,7 +41,7 @@ function formatSkill(skill, currentUser) {
|
|
|
41
41
|
return result;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// Format version, convert uploader to object
|
|
45
45
|
function formatVersion(version) {
|
|
46
46
|
if (!version) return null;
|
|
47
47
|
return {
|
|
@@ -61,7 +61,7 @@ function formatVersion(version) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
async function skillsRoutes(fastify, options) {
|
|
64
|
-
// GET / -
|
|
64
|
+
// GET / - Get skills list
|
|
65
65
|
fastify.get('/', { preHandler: [fastify.optionalAuth] }, async (request, reply) => {
|
|
66
66
|
const { q } = request.query;
|
|
67
67
|
const skills = SkillModel.search(q);
|
|
@@ -73,7 +73,7 @@ async function skillsRoutes(fastify, options) {
|
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
// GET /:skill_id -
|
|
76
|
+
// GET /:skill_id - Get single skill
|
|
77
77
|
fastify.get('/:skill_id', { preHandler: [fastify.optionalAuth] }, async (request, reply) => {
|
|
78
78
|
const { skill_id } = request.params;
|
|
79
79
|
const skill = SkillModel.findById(skill_id);
|
|
@@ -85,11 +85,11 @@ async function skillsRoutes(fastify, options) {
|
|
|
85
85
|
return formatSkill(skill, request.user);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
// GET /:skill_id/versions -
|
|
88
|
+
// GET /:skill_id/versions - Get all versions of a skill
|
|
89
89
|
fastify.get('/:skill_id/versions', async (request, reply) => {
|
|
90
90
|
const { skill_id } = request.params;
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// First check if skill exists
|
|
93
93
|
if (!SkillModel.exists(skill_id)) {
|
|
94
94
|
return reply.code(404).send({ detail: 'Skill not found' });
|
|
95
95
|
}
|
|
@@ -103,7 +103,7 @@ async function skillsRoutes(fastify, options) {
|
|
|
103
103
|
};
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
// GET /:skill_id/versions/:version/download -
|
|
106
|
+
// GET /:skill_id/versions/:version/download - Download version zip file
|
|
107
107
|
fastify.get('/:skill_id/versions/:version/download', async (request, reply) => {
|
|
108
108
|
const { skill_id, version } = request.params;
|
|
109
109
|
|
|
@@ -119,11 +119,11 @@ async function skillsRoutes(fastify, options) {
|
|
|
119
119
|
return reply.code(404).send({ detail: 'Version not found' });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
//
|
|
122
|
+
// Prefer using zip_path from database for backward compatibility; fallback to rule-based path if missing
|
|
123
123
|
const zipPath = resolveZipPath(versionRecord.zip_path, skill_id, versionRecord.version);
|
|
124
124
|
const fallbackZipPath = getZipPath(skill_id, versionRecord.version);
|
|
125
125
|
|
|
126
|
-
//
|
|
126
|
+
// Check if file exists
|
|
127
127
|
if (!fs.existsSync(zipPath)) {
|
|
128
128
|
if (!fs.existsSync(fallbackZipPath)) {
|
|
129
129
|
return reply.code(404).send({ detail: 'Version not found' });
|
|
@@ -132,7 +132,7 @@ async function skillsRoutes(fastify, options) {
|
|
|
132
132
|
|
|
133
133
|
const finalZipPath = fs.existsSync(zipPath) ? zipPath : fallbackZipPath;
|
|
134
134
|
|
|
135
|
-
//
|
|
135
|
+
// Set response headers and return file stream
|
|
136
136
|
const fileName = `${skill_id}-${versionRecord.version}.zip`;
|
|
137
137
|
reply.header('Content-Type', 'application/zip');
|
|
138
138
|
reply.header('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
@@ -140,7 +140,7 @@ async function skillsRoutes(fastify, options) {
|
|
|
140
140
|
return fs.createReadStream(finalZipPath);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
// PUT /:skill_id -
|
|
143
|
+
// PUT /:skill_id - Update skill basic info
|
|
144
144
|
fastify.put('/:skill_id', {
|
|
145
145
|
preHandler: [fastify.authenticate]
|
|
146
146
|
}, async (request, reply) => {
|
|
@@ -159,7 +159,7 @@ async function skillsRoutes(fastify, options) {
|
|
|
159
159
|
return formatSkill(updated, request.user);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
-
// PUT /:skill_id/head -
|
|
162
|
+
// PUT /:skill_id/head - Set skill's latest version (Head pointer)
|
|
163
163
|
fastify.put('/:skill_id/head', {
|
|
164
164
|
preHandler: [fastify.authenticate]
|
|
165
165
|
}, async (request, reply) => {
|
|
@@ -187,7 +187,7 @@ async function skillsRoutes(fastify, options) {
|
|
|
187
187
|
return { ok: true, skill_id, latest_version: version };
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
// PATCH /:skill_id/versions/:version -
|
|
190
|
+
// PATCH /:skill_id/versions/:version - Update description and changelog for specified version
|
|
191
191
|
fastify.patch('/:skill_id/versions/:version', {
|
|
192
192
|
preHandler: [fastify.authenticate]
|
|
193
193
|
}, async (request, reply) => {
|
package/src/routes/users.js
CHANGED
|
@@ -3,8 +3,8 @@ const { hashPassword } = require('../utils/crypto');
|
|
|
3
3
|
const db = require('../database');
|
|
4
4
|
|
|
5
5
|
async function usersRoutes(fastify, options) {
|
|
6
|
-
// GET /search -
|
|
7
|
-
//
|
|
6
|
+
// GET /search - User search (login required only, no admin permission needed)
|
|
7
|
+
// Note: Must be registered before /:user_id
|
|
8
8
|
fastify.get('/search', {
|
|
9
9
|
preHandler: [fastify.authenticate]
|
|
10
10
|
}, async (request, reply) => {
|
|
@@ -25,11 +25,11 @@ async function usersRoutes(fastify, options) {
|
|
|
25
25
|
return reply.send({ users });
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// Routes below require admin permission
|
|
29
29
|
fastify.register(async function adminRoutes(fastify) {
|
|
30
30
|
fastify.addHook('preHandler', fastify.requireAdmin);
|
|
31
31
|
|
|
32
|
-
// GET / -
|
|
32
|
+
// GET / - User list
|
|
33
33
|
fastify.get('/', async (request, reply) => {
|
|
34
34
|
const { q, status, page = 1, limit = 20 } = request.query;
|
|
35
35
|
const result = UserModel.list({
|
|
@@ -41,7 +41,7 @@ async function usersRoutes(fastify, options) {
|
|
|
41
41
|
return reply.send(result);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
// POST / -
|
|
44
|
+
// POST / - Create user
|
|
45
45
|
fastify.post('/', async (request, reply) => {
|
|
46
46
|
const { username, password, role = 'developer', name } = request.body || {};
|
|
47
47
|
|
|
@@ -62,7 +62,7 @@ async function usersRoutes(fastify, options) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const passwordHash = await hashPassword(password);
|
|
65
|
-
//
|
|
65
|
+
// Record created_by when creating user
|
|
66
66
|
const result = db.prepare(
|
|
67
67
|
"INSERT INTO users (username, password_hash, role, name, status, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, datetime('now'), datetime('now'))"
|
|
68
68
|
).run(username.trim(), passwordHash, role, name || null, request.user.id);
|
|
@@ -71,7 +71,7 @@ async function usersRoutes(fastify, options) {
|
|
|
71
71
|
return reply.code(201).send({ ok: true, user });
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
// GET /:user_id -
|
|
74
|
+
// GET /:user_id - User details
|
|
75
75
|
fastify.get('/:user_id', async (request, reply) => {
|
|
76
76
|
const { user_id } = request.params;
|
|
77
77
|
const user = UserModel.findByIdWithCreator(parseInt(user_id));
|
|
@@ -97,7 +97,7 @@ async function usersRoutes(fastify, options) {
|
|
|
97
97
|
return reply.send(result);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
// PATCH /:user_id -
|
|
100
|
+
// PATCH /:user_id - Update user
|
|
101
101
|
fastify.patch('/:user_id', async (request, reply) => {
|
|
102
102
|
const userId = parseInt(request.params.user_id);
|
|
103
103
|
const { role, status, name } = request.body || {};
|
|
@@ -144,7 +144,7 @@ async function usersRoutes(fastify, options) {
|
|
|
144
144
|
return reply.send({ ok: true, user: updated });
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
// POST /:user_id/reset-password -
|
|
147
|
+
// POST /:user_id/reset-password - Reset password
|
|
148
148
|
fastify.post('/:user_id/reset-password', async (request, reply) => {
|
|
149
149
|
const userId = parseInt(request.params.user_id);
|
|
150
150
|
const { new_password } = request.body || {};
|