🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

skill-base

Package Overview
Dependencies
Maintainers
1
Versions
45
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

skill-base - npm Package Compare versions

Comparing version
2.0.36
to
2.0.37
+89
src/migrations/005...favorites-tags-downloads-super-admin.js
function addColumnIfMissing(db, sql) {
try {
db.exec(sql);
} catch (error) {
if (!String(error && error.message ? error.message : error).includes('duplicate column name')) {
throw error;
}
}
}
function up(db) {
addColumnIfMissing(db, "ALTER TABLE users ADD COLUMN is_super_admin INTEGER NOT NULL DEFAULT 0");
addColumnIfMissing(db, "ALTER TABLE skills ADD COLUMN favorite_count INTEGER NOT NULL DEFAULT 0");
addColumnIfMissing(db, "ALTER TABLE skills ADD COLUMN download_count INTEGER NOT NULL DEFAULT 0");
addColumnIfMissing(db, "ALTER TABLE skill_versions ADD COLUMN download_count INTEGER NOT NULL DEFAULT 0");
db.exec(`
CREATE TABLE IF NOT EXISTS skill_favorites (
user_id INTEGER NOT NULL,
skill_id TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, skill_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER NOT NULL,
updated_by INTEGER NOT NULL,
FOREIGN KEY (created_by) REFERENCES users(id),
FOREIGN KEY (updated_by) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS skill_tags (
skill_id TEXT NOT NULL,
tag_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER NOT NULL,
PRIMARY KEY (skill_id, tag_id),
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_skill_favorites_skill_id ON skill_favorites(skill_id);
CREATE INDEX IF NOT EXISTS idx_skill_favorites_user_id ON skill_favorites(user_id);
CREATE INDEX IF NOT EXISTS idx_skill_tags_tag_id ON skill_tags(tag_id);
CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
`);
db.exec(`
UPDATE skills
SET favorite_count = (
SELECT COUNT(*)
FROM skill_favorites
WHERE skill_favorites.skill_id = skills.id
);
UPDATE skills
SET download_count = COALESCE(download_count, 0);
UPDATE skill_versions
SET download_count = COALESCE(download_count, 0);
`);
const existingSuperAdmin = db.prepare('SELECT id FROM users WHERE is_super_admin = 1 LIMIT 1').get();
if (!existingSuperAdmin) {
db.prepare(`
UPDATE users
SET is_super_admin = 1
WHERE id = (
SELECT id
FROM users
WHERE role = 'admin'
ORDER BY created_at ASC, id ASC
LIMIT 1
)
`).run();
}
}
module.exports = {
version: '005-skill-favorites-tags-downloads-super-admin',
up
};
const db = require('../database');
const modelCache = require('../utils/model-cache');
function syncFavoriteCount(skillId) {
db.prepare(`
UPDATE skills
SET favorite_count = (
SELECT COUNT(*)
FROM skill_favorites
WHERE skill_id = ?
)
WHERE id = ?
`).run(skillId, skillId);
modelCache.invalidateSkill(skillId);
}
const FavoriteModel = {
isFavorited(userId, skillId) {
return !!db.prepare('SELECT 1 FROM skill_favorites WHERE user_id = ? AND skill_id = ?').get(userId, skillId);
},
add(userId, skillId) {
db.transaction(() => {
db.prepare(`
INSERT OR IGNORE INTO skill_favorites (user_id, skill_id)
VALUES (?, ?)
`).run(userId, skillId);
syncFavoriteCount(skillId);
})();
return this.isFavorited(userId, skillId);
},
remove(userId, skillId) {
db.transaction(() => {
db.prepare(`
DELETE FROM skill_favorites
WHERE user_id = ? AND skill_id = ?
`).run(userId, skillId);
syncFavoriteCount(skillId);
})();
return false;
},
countBySkillId(skillId) {
return db.prepare(`
SELECT COUNT(*) AS count
FROM skill_favorites
WHERE skill_id = ?
`).get(skillId).count;
}
};
module.exports = FavoriteModel;
const db = require('../database');
const modelCache = require('../utils/model-cache');
function normalizeName(name) {
return String(name || '').trim();
}
function getTagById(id) {
return db.prepare(`
SELECT id, name, created_at, updated_at, created_by, updated_by
FROM tags
WHERE id = ?
`).get(id);
}
function invalidateSkillTagRefs(skillIds) {
for (const skillId of skillIds || []) {
modelCache.invalidateSkill(skillId);
}
}
const TagModel = {
create({ name, actorId }) {
const normalizedName = normalizeName(name);
const result = db.prepare(`
INSERT INTO tags (name, created_by, updated_by)
VALUES (?, ?, ?)
`).run(normalizedName, actorId, actorId);
return getTagById(result.lastInsertRowid);
},
update(id, { name, actorId }) {
const normalizedName = normalizeName(name);
db.prepare(`
UPDATE tags
SET name = ?, updated_at = CURRENT_TIMESTAMP, updated_by = ?
WHERE id = ?
`).run(normalizedName, actorId, id);
return getTagById(id);
},
delete(id) {
const affectedSkills = db.prepare(`
SELECT skill_id
FROM skill_tags
WHERE tag_id = ?
`).all(id).map((row) => row.skill_id);
const result = db.prepare('DELETE FROM tags WHERE id = ?').run(id);
invalidateSkillTagRefs(affectedSkills);
return result.changes > 0;
},
listSkillTags(skillId) {
return db.prepare(`
SELECT t.id, t.name
FROM skill_tags st
JOIN tags t ON t.id = st.tag_id
WHERE st.skill_id = ?
ORDER BY t.name ASC
`).all(skillId);
},
listAllWithUsage() {
return db.prepare(`
SELECT t.id, t.name, COUNT(st.skill_id) AS usage_count
FROM tags t
LEFT JOIN skill_tags st ON st.tag_id = t.id
GROUP BY t.id, t.name
ORDER BY t.name ASC
`).all();
},
replaceSkillTags(skillId, tagIds, actorId) {
const uniqueTagIds = [...new Set((tagIds || []).map((id) => Number(id)))];
return db.transaction(() => {
if (uniqueTagIds.length > 0) {
const placeholders = uniqueTagIds.map(() => '?').join(', ');
const rows = db.prepare(`SELECT id FROM tags WHERE id IN (${placeholders})`).all(...uniqueTagIds);
if (rows.length !== uniqueTagIds.length) {
throw new Error('One or more tags do not exist');
}
}
db.prepare('DELETE FROM skill_tags WHERE skill_id = ?').run(skillId);
const insert = db.prepare(`
INSERT INTO skill_tags (skill_id, tag_id, created_by)
VALUES (?, ?, ?)
`);
for (const tagId of uniqueTagIds) {
insert.run(skillId, tagId, actorId);
}
modelCache.invalidateSkill(skillId);
return this.listSkillTags(skillId);
})();
}
};
module.exports = TagModel;
const TagModel = require('../models/tag');
const { isSuperAdmin } = require('../utils/permission');
async function tagsRoutes(fastify) {
async function requireSuperAdmin(request, reply) {
if (!isSuperAdmin(request.user)) {
return reply.code(403).send({
ok: false,
error: 'forbidden',
detail: 'Super admin permission required'
});
}
}
fastify.get('/', {
preHandler: [fastify.authenticate]
}, async () => {
return { tags: TagModel.listAllWithUsage() };
});
fastify.post('/', {
preHandler: [fastify.authenticate, requireSuperAdmin]
}, async (request, reply) => {
const name = String(request.body?.name || '').trim();
if (!name) {
return reply.code(400).send({ detail: 'Tag name is required' });
}
const tag = TagModel.create({ name, actorId: request.user.id });
return reply.code(201).send({ ok: true, tag });
});
fastify.patch('/:tag_id', {
preHandler: [fastify.authenticate, requireSuperAdmin]
}, async (request, reply) => {
const tagId = parseInt(request.params.tag_id, 10);
const name = String(request.body?.name || '').trim();
if (!Number.isInteger(tagId) || tagId <= 0) {
return reply.code(400).send({ detail: 'Invalid tag id' });
}
if (!name) {
return reply.code(400).send({ detail: 'Tag name is required' });
}
const tag = TagModel.update(tagId, { name, actorId: request.user.id });
if (!tag) {
return reply.code(404).send({ detail: 'Tag not found' });
}
return { ok: true, tag };
});
fastify.delete('/:tag_id', {
preHandler: [fastify.authenticate, requireSuperAdmin]
}, async (request, reply) => {
const tagId = parseInt(request.params.tag_id, 10);
if (!Number.isInteger(tagId) || tagId <= 0) {
return reply.code(400).send({ detail: 'Invalid tag id' });
}
const deleted = TagModel.delete(tagId);
if (!deleted) {
return reply.code(404).send({ detail: 'Tag not found' });
}
return { ok: true };
});
}
module.exports = tagsRoutes;

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

+2
-0

@@ -62,2 +62,4 @@ # Skill Base

Web 端还支持账号级收藏、下载次数统计,以及由超级管理员维护的全局标签库。详情页内联预览使用独立的 `/view` 接口,避免「浏览文件」计入下载次数。
### 数据结构克制,天然适合 GitOps

@@ -64,0 +66,0 @@

+1
-1
{
"name": "skill-base",
"version": "2.0.36",
"version": "2.0.37",
"description": "Skill Base - A lightweight, privately deployable agent skills distribution hub",

@@ -5,0 +5,0 @@ "main": "src/index.js",

@@ -62,2 +62,4 @@ # Skill Base

The web UI also supports account-based favorites, download counters, and an optional global tag library (managed by the super admin). Inline previews use a dedicated `/view` endpoint so browsing a version ZIP does not inflate download statistics.
### Small data model, GitOps-friendly

@@ -64,0 +66,0 @@

@@ -251,2 +251,29 @@ const { execFileSync } = require('child_process');

const migrations = [
require('./migrations/005-skill-favorites-tags-downloads-super-admin')
];
function ensureSchemaMigrationsTable() {
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
}
function runMigrations() {
ensureSchemaMigrationsTable();
for (const migration of migrations) {
const row = db.prepare('SELECT version FROM schema_migrations WHERE version = ?').get(migration.version);
if (row) continue;
db.transaction(() => {
migration.up(db);
db.prepare('INSERT INTO schema_migrations (version) VALUES (?)').run(migration.version);
})();
}
}
// node-sqlite3-wasm: keep default DELETE journal (WAL + this VFS breaks on many existing DBs).

@@ -362,2 +389,4 @@

runMigrations();
// Data migration: insert skill_collaborators record for existing Skills owners

@@ -364,0 +393,0 @@ const existingSkills = db.prepare('SELECT id, owner_id FROM skills').all();

@@ -164,2 +164,3 @@ const fs = require('fs');

await fastify.register(require('./routes/collaborators'), { prefix: `${API_PREFIX}/skills` });
await fastify.register(require('./routes/tags'), { prefix: `${API_PREFIX}/tags` });
await fastify.register(require('./routes/users'), { prefix: `${API_PREFIX}/users` });

@@ -166,0 +167,0 @@ debugLog({ zh: 'API 路由已注册。', en: 'API routes registered.' });

@@ -83,2 +83,4 @@ const fp = require('fastify-plugin');

async function authPlugin(fastify, options) {
const authUserColumns = 'id, username, name, role, status, is_super_admin';
// Expose sessionStore for routes to use

@@ -96,3 +98,3 @@ fastify.decorate('sessionStore', sessionStore);

if (session) {
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(session.userId);
const user = db.prepare(`SELECT ${authUserColumns} FROM users WHERE id = ?`).get(session.userId);
if (!user || user.status === 'disabled') {

@@ -116,3 +118,3 @@ return reply.code(401).send({

if (pat) {
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(pat.user_id);
const user = db.prepare(`SELECT ${authUserColumns} FROM users WHERE id = ?`).get(pat.user_id);
if (!user || user.status === 'disabled') {

@@ -144,3 +146,3 @@ return reply.code(401).send({

if (session) {
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(session.userId);
const user = db.prepare(`SELECT ${authUserColumns} FROM users WHERE id = ?`).get(session.userId);
if (user && user.status !== 'disabled') { request.user = user; return; }

@@ -154,3 +156,3 @@ }

if (pat) {
const user = db.prepare('SELECT id, username, name, role, status FROM users WHERE id = ?').get(pat.user_id);
const user = db.prepare(`SELECT ${authUserColumns} FROM users WHERE id = ?`).get(pat.user_id);
if (user && user.status !== 'disabled') { request.user = user; return; }

@@ -157,0 +159,0 @@ }

@@ -98,2 +98,11 @@ const db = require('../database');

incrementDownloadCount(id) {
db.prepare(`
UPDATE skills
SET download_count = COALESCE(download_count, 0) + 1
WHERE id = ?
`).run(id);
modelCache.invalidateSkill(id);
},
// Check if Skill exists

@@ -100,0 +109,0 @@ exists(id) {

@@ -5,3 +5,3 @@ const db = require('../database');

function queryById(id) {
return db.prepare('SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE id = ?').get(id);
return db.prepare('SELECT id, username, name, role, status, is_super_admin, created_at, updated_at FROM users WHERE id = ?').get(id);
}

@@ -33,3 +33,3 @@

list({ q, status, page = 1, limit = 20 } = {}) {
let sql = 'SELECT id, username, name, role, status, created_at, updated_at FROM users WHERE 1=1';
let sql = 'SELECT id, username, name, role, status, is_super_admin, created_at, updated_at FROM users WHERE 1=1';
let countSql = 'SELECT COUNT(*) as total FROM users WHERE 1=1';

@@ -85,3 +85,3 @@ const params = [];

update(id, fields) {
const allowed = ['role', 'status', 'username', 'name'];
const allowed = ['role', 'status', 'username', 'name', 'is_super_admin'];
const sets = [];

@@ -120,6 +120,14 @@ const params = [];

delete(id) {
const result = db.prepare('DELETE FROM users WHERE id = ?').run(id);
if (result.changes > 0) {
modelCache.invalidateUser(id);
}
return result.changes > 0;
},
// Find user details (includes creator info)
findByIdWithCreator(id) {
return db.prepare(`
SELECT u.id, u.username, u.name, u.role, u.status, u.created_at, u.updated_at,
SELECT u.id, u.username, u.name, u.role, u.status, u.is_super_admin, u.created_at, u.updated_at,
c.id as creator_id, c.username as creator_username

@@ -132,2 +140,18 @@ FROM users u

countSuperAdmins() {
return db.prepare('SELECT COUNT(*) AS count FROM users WHERE is_super_admin = 1').get().count;
},
canDemoteOrDisableSuperAdmin(id) {
const user = db.prepare('SELECT is_super_admin FROM users WHERE id = ?').get(id);
if (!user?.is_super_admin) {
return true;
}
return this.countSuperAdmins() > 1;
},
canDeleteSuperAdmin(id) {
return this.canDemoteOrDisableSuperAdmin(id);
},
// Update username and name

@@ -134,0 +158,0 @@ updateProfile(id, { username, name }) {

@@ -82,2 +82,12 @@ const db = require('../database');

return this.findById(id);
},
incrementDownloadCount(skillId, version) {
db.prepare(`
UPDATE skill_versions
SET download_count = COALESCE(download_count, 0) + 1
WHERE skill_id = ? AND version = ?
`).run(skillId, version);
modelCache.invalidateSkill(skillId);
return this.findByVersion(skillId, version);
}

@@ -84,0 +94,0 @@ };

@@ -38,3 +38,12 @@ const db = require('../database');

return { ok: true, user: { id: user.id, username: user.username, name: user.name || null, role: user.role } };
return {
ok: true,
user: {
id: user.id,
username: user.username,
name: user.name || null,
role: user.role,
is_super_admin: user.is_super_admin || 0
}
};
});

@@ -41,0 +50,0 @@

@@ -75,3 +75,3 @@ const bcrypt = require('bcryptjs');

const result = db.prepare(
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)'
'INSERT INTO users (username, password_hash, role, is_super_admin) VALUES (?, ?, ?, 1)'
).run(username, passwordHash, 'admin');

@@ -78,0 +78,0 @@

@@ -6,2 +6,4 @@ const fs = require('fs');

const VersionModel = require('../models/version');
const FavoriteModel = require('../models/favorite');
const TagModel = require('../models/tag');
const { getZipPath, resolveZipPath } = require('../utils/zip');

@@ -34,2 +36,5 @@ const { canManageSkill } = require('../utils/permission');

latest_version: skill.latest_version,
favorite_count: skill.favorite_count || 0,
download_count: skill.download_count || 0,
tags: TagModel.listSkillTags(skill.id),
owner: {

@@ -59,2 +64,4 @@ id: skill.owner_id,

result.is_favorited = currentUser ? FavoriteModel.isFavorited(currentUser.id, skill.id) : false;
if (canViewSkillWebhook(currentUser, result.permission)) {

@@ -76,2 +83,3 @@ result.webhook_url = skill.webhook_url || null;

description: version.description,
download_count: version.download_count || 0,
zip_path: version.zip_path,

@@ -88,2 +96,13 @@ uploader: {

async function skillsRoutes(fastify, options) {
function resolveExistingZipPath(skillId, versionRecord) {
const zipPath = resolveZipPath(versionRecord.zip_path, skillId, versionRecord.version);
const fallbackZipPath = getZipPath(skillId, versionRecord.version);
if (!fs.existsSync(zipPath) && !fs.existsSync(fallbackZipPath)) {
return null;
}
return fs.existsSync(zipPath) ? zipPath : fallbackZipPath;
}
// GET / - Get skills list

@@ -149,14 +168,9 @@ fastify.get('/', { preHandler: [fastify.optionalAuth] }, async (request, reply) => {

// Prefer using zip_path from database for backward compatibility; fallback to rule-based path if missing
const zipPath = resolveZipPath(versionRecord.zip_path, skill_id, versionRecord.version);
const fallbackZipPath = getZipPath(skill_id, versionRecord.version);
// Check if file exists
if (!fs.existsSync(zipPath)) {
if (!fs.existsSync(fallbackZipPath)) {
return reply.code(404).send({ detail: 'Version not found' });
}
const finalZipPath = resolveExistingZipPath(skill_id, versionRecord);
if (!finalZipPath) {
return reply.code(404).send({ detail: 'Version not found' });
}
const finalZipPath = fs.existsSync(zipPath) ? zipPath : fallbackZipPath;
SkillModel.incrementDownloadCount(skill_id);
VersionModel.incrementDownloadCount(skill_id, versionRecord.version);

@@ -171,2 +185,91 @@ // Set response headers and return file stream

// GET /:skill_id/versions/:version/view - View version zip file without counting download
fastify.get('/:skill_id/versions/:version/view', async (request, reply) => {
const { skill_id, version } = request.params;
const versionRecord = version === 'latest'
? VersionModel.getLatest(skill_id)
: VersionModel.findByVersion(skill_id, version);
if (!versionRecord) {
return reply.code(404).send({ detail: 'Version not found' });
}
const finalZipPath = resolveExistingZipPath(skill_id, versionRecord);
if (!finalZipPath) {
return reply.code(404).send({ detail: 'Version not found' });
}
const fileName = `${skill_id}-${versionRecord.version}.zip`;
reply.header('Content-Type', 'application/zip');
reply.header('Content-Disposition', `inline; filename="${fileName}"`);
return fs.createReadStream(finalZipPath);
});
// POST /:skill_id/favorite - Favorite skill
fastify.post('/:skill_id/favorite', {
preHandler: [fastify.authenticate]
}, async (request, reply) => {
const { skill_id } = request.params;
if (!SkillModel.exists(skill_id)) {
return reply.code(404).send({ detail: 'Skill not found' });
}
FavoriteModel.add(request.user.id, skill_id);
const skill = SkillModel.findById(skill_id);
return {
ok: true,
skill_id,
favorited: true,
favorite_count: skill.favorite_count || 0
};
});
// DELETE /:skill_id/favorite - Unfavorite skill
fastify.delete('/:skill_id/favorite', {
preHandler: [fastify.authenticate]
}, async (request, reply) => {
const { skill_id } = request.params;
if (!SkillModel.exists(skill_id)) {
return reply.code(404).send({ detail: 'Skill not found' });
}
FavoriteModel.remove(request.user.id, skill_id);
const skill = SkillModel.findById(skill_id);
return {
ok: true,
skill_id,
favorited: false,
favorite_count: skill.favorite_count || 0
};
});
// PUT /:skill_id/tags - Replace skill tags
fastify.put('/:skill_id/tags', {
preHandler: [fastify.authenticate]
}, async (request, reply) => {
const { skill_id } = request.params;
const { tag_ids } = request.body || {};
if (!SkillModel.exists(skill_id)) {
return reply.code(404).send({ detail: 'Skill not found' });
}
if (!canManageSkill(request.user, skill_id)) {
return reply.code(403).send({ ok: false, error: 'forbidden', detail: 'Owner or admin permission required' });
}
if (tag_ids !== undefined && !Array.isArray(tag_ids)) {
return reply.code(400).send({ detail: 'tag_ids must be an array' });
}
const tags = TagModel.replaceSkillTags(skill_id, tag_ids || [], request.user.id);
return { ok: true, skill_id, tags };
});
// PUT /:skill_id - Update skill basic info

@@ -173,0 +276,0 @@ fastify.put('/:skill_id', {

@@ -12,8 +12,16 @@ const UserModel = require('../models/user');

const { q } = request.query;
if (!q || q.trim().length < 1) {
return reply.code(400).send({ ok: false, error: 'invalid_params', detail: 'Search keyword must be at least 1 character' });
const trimmed = q != null ? String(q).trim() : '';
if (!trimmed) {
const users = db.prepare(`
SELECT id, username, name, status
FROM users
WHERE status = 'active'
ORDER BY username ASC
LIMIT 2000
`).all();
return reply.send({ users });
}
const pattern = `%${q.trim()}%`;
const pattern = `%${trimmed}%`;
const users = db.prepare(`

@@ -23,5 +31,6 @@ SELECT id, username, name, status

WHERE (username LIKE ? OR name LIKE ?) AND status = 'active'
LIMIT 10
ORDER BY username ASC
LIMIT 100
`).all(pattern, pattern);
return reply.send({ users });

@@ -92,2 +101,3 @@ });

status: user.status,
is_super_admin: user.is_super_admin || 0,
created_at: user.created_at,

@@ -135,4 +145,10 @@ updated_at: user.updated_at

}
if (status === 'disabled' && !UserModel.canDemoteOrDisableSuperAdmin(userId)) {
return reply.code(400).send({ ok: false, error: 'last_super_admin', detail: 'Cannot disable the last super admin' });
}
fields.status = status;
}
if (role !== undefined && role === 'developer' && !UserModel.canDemoteOrDisableSuperAdmin(userId)) {
return reply.code(400).send({ ok: false, error: 'last_super_admin', detail: 'Cannot downgrade the last super admin' });
}
if (name !== undefined) {

@@ -170,5 +186,25 @@ fields.name = name;

});
// DELETE /:user_id - Delete user
fastify.delete('/:user_id', async (request, reply) => {
const userId = parseInt(request.params.user_id);
const user = UserModel.findById(userId);
if (!user) {
return reply.code(404).send({ ok: false, error: 'not_found', detail: 'User not found' });
}
if (userId === request.user.id) {
return reply.code(400).send({ ok: false, error: 'self_protection', detail: 'Cannot delete your own account' });
}
if (!UserModel.canDeleteSuperAdmin(userId)) {
return reply.code(400).send({ ok: false, error: 'last_super_admin', detail: 'Cannot delete the last super admin' });
}
return reply.send({ ok: UserModel.delete(userId) });
});
});
}
module.exports = usersRoutes;

@@ -41,6 +41,11 @@ const db = require('../database');

function isSuperAdmin(user) {
return !!user && user.role === 'admin' && Number(user.is_super_admin || 0) === 1;
}
module.exports = {
hasSkillPermission,
canPublishSkill,
canManageSkill
canManageSkill,
isSuperAdmin
};

@@ -12,4 +12,4 @@ <!DOCTYPE html>

<title>Skill Base</title>
<script type="module" crossorigin src="./assets/index-HwIh4LJF.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-De_i-3yU.css">
<script type="module" crossorigin src="./assets/index-CXr6bCUN.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-B_WXtVS0.css">
</head>

@@ -16,0 +16,0 @@ <body>

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display