| 1 | const fs = require('fs'); |
| 2 | const auth = require('../lib/auth'); |
| 3 | const data = require('../lib/data'); |
| 4 | const git = require('../lib/git'); |
| 5 | const render = require('../lib/render'); |
| 6 | const respond = require('../lib/respond'); |
| 7 | const validate = require('../lib/validate'); |
| 8 | const config = require('../lib/config'); |
| 9 | const limit = require('../lib/limit'); |
| 10 | |
| 11 | const writeLimiter = limit.make( |
| 12 | config.get('limits.write.max'), |
| 13 | config.get('limits.write.windowMin') * 60000 |
| 14 | ); |
| 15 | |
| 16 | function requireOwner(req, res, owner) { |
| 17 | const session = auth.requireAuth(req, res); |
| 18 | if (!session) return null; |
| 19 | if (session.username !== owner) { respond.forbidden(res); return null; } |
| 20 | return session; |
| 21 | } |
| 22 | |
| 23 | function settingsPage(req, res, params) { |
| 24 | const session = auth.requireAuth(req, res); |
| 25 | if (!session) return respond.redirect(res, '/signup'); |
| 26 | if (session.username !== params.owner) return respond.forbidden(res); |
| 27 | const repo = data.repos().get(`${params.owner}/${params.repo}`); |
| 28 | if (!repo) return respond.notFound(res); |
| 29 | const content = render.fill(render.loadView('repo-settings.html'), { |
| 30 | OWNER: render.escape(params.owner), |
| 31 | REPO: render.escape(params.repo), |
| 32 | DESCRIPTION: render.escape(repo.description || ''), |
| 33 | VIS_PUBLIC: repo.visibility === 'public' ? 'checked' : '', |
| 34 | VIS_PRIVATE: repo.visibility === 'private' ? 'checked' : '' |
| 35 | }); |
| 36 | res.end(render.page({ title: `Settings - ${params.owner}/${params.repo}`, content, session, addressUrl: req.url })); |
| 37 | } |
| 38 | |
| 39 | function createRepo(req, res) { |
| 40 | const session = auth.requireAuth(req, res); |
| 41 | if (!session) return; |
| 42 | const ip = respond.clientIp(req); |
| 43 | if (!writeLimiter(ip)) return respond.tooMany(res); |
| 44 | respond.readJson(req, (err, body) => { |
| 45 | if (err) return respond.badRequest(res, err.message); |
| 46 | const name = String(body.name || '').toLowerCase().trim(); |
| 47 | const description = String(body.description || '').slice(0, 256); |
| 48 | const visibility = body.visibility === 'private' && config.get('repos.allowPrivate') |
| 49 | ? 'private' : config.get('repos.defaultVisibility'); |
| 50 | if (!validate.isValidName(name)) return respond.badRequest(res, 'Invalid repository name'); |
| 51 | const owner = session.username; |
| 52 | const key = `${owner}/${name}`; |
| 53 | const repos = data.repos(); |
| 54 | if (repos.has(key)) return respond.json(res, 409, { error: 'Repository name taken' }); |
| 55 | const ownerCount = repos.values().filter(r => r.owner === owner).length; |
| 56 | if (ownerCount >= config.get('repos.maxPerUser')) { |
| 57 | return respond.json(res, 409, { error: 'Repository limit reached' }); |
| 58 | } |
| 59 | const gitDir = git.repoPath(owner, name); |
| 60 | git.init(gitDir) |
| 61 | .then(() => { |
| 62 | repos.set(key, { name, owner, description, visibility, createdAt: Date.now() }); |
| 63 | respond.json(res, 201, { ok: true, owner, name }); |
| 64 | }) |
| 65 | .catch(e => respond.serverError(res, e.message)); |
| 66 | }); |
| 67 | } |
| 68 | |
| 69 | function updateSettings(req, res, params) { |
| 70 | const session = requireOwner(req, res, params.owner); |
| 71 | if (!session) return; |
| 72 | respond.readJson(req, (err, body) => { |
| 73 | if (err) return respond.badRequest(res, err.message); |
| 74 | const key = `${params.owner}/${params.repo}`; |
| 75 | const repos = data.repos(); |
| 76 | const repo = repos.get(key); |
| 77 | if (!repo) return respond.notFound(res); |
| 78 | const updated = { ...repo }; |
| 79 | if (typeof body.description === 'string') updated.description = body.description.slice(0, 256); |
| 80 | if (validate.isVisibility(body.visibility)) { |
| 81 | if (body.visibility === 'private' && !config.get('repos.allowPrivate')) { |
| 82 | return respond.badRequest(res, 'Private repositories are disabled'); |
| 83 | } |
| 84 | updated.visibility = body.visibility; |
| 85 | } |
| 86 | repos.set(key, updated); |
| 87 | respond.json(res, 200, { ok: true }); |
| 88 | }); |
| 89 | } |
| 90 | |
| 91 | function renameRepo(req, res, params) { |
| 92 | const session = requireOwner(req, res, params.owner); |
| 93 | if (!session) return; |
| 94 | respond.readJson(req, (err, body) => { |
| 95 | if (err) return respond.badRequest(res, err.message); |
| 96 | const newName = String(body.name || '').toLowerCase().trim(); |
| 97 | if (!validate.isValidName(newName)) return respond.badRequest(res, 'Invalid name'); |
| 98 | const oldKey = `${params.owner}/${params.repo}`; |
| 99 | const newKey = `${params.owner}/${newName}`; |
| 100 | const repos = data.repos(); |
| 101 | const repo = repos.get(oldKey); |
| 102 | if (!repo) return respond.notFound(res); |
| 103 | if (repos.has(newKey)) return respond.json(res, 409, { error: 'Name already taken' }); |
| 104 | try { |
| 105 | fs.renameSync(git.repoPath(params.owner, params.repo), git.repoPath(params.owner, newName)); |
| 106 | } catch (e) { return respond.serverError(res, 'Could not rename directory'); } |
| 107 | repos.delete(oldKey); |
| 108 | repos.set(newKey, { ...repo, name: newName }); |
| 109 | respond.json(res, 200, { ok: true, owner: params.owner, name: newName }); |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | function deleteRepo(req, res, params) { |
| 114 | const session = requireOwner(req, res, params.owner); |
| 115 | if (!session) return; |
| 116 | const key = `${params.owner}/${params.repo}`; |
| 117 | const repos = data.repos(); |
| 118 | if (!repos.has(key)) return respond.notFound(res); |
| 119 | const gitDir = git.repoPath(params.owner, params.repo); |
| 120 | try { if (fs.existsSync(gitDir)) fs.rmSync(gitDir, { recursive: true, force: true }); } |
| 121 | catch (e) { return respond.serverError(res, 'Could not delete repository'); } |
| 122 | repos.delete(key); |
| 123 | respond.json(res, 200, { ok: true }); |
| 124 | } |
| 125 | |
| 126 | module.exports = function register(router) { |
| 127 | router.get('/:owner/:repo/settings', settingsPage); |
| 128 | router.post('/api/repo/create', createRepo); |
| 129 | router.post('/api/repo/:owner/:repo/settings', updateSettings); |
| 130 | router.post('/api/repo/:owner/:repo/rename', renameRepo); |
| 131 | router.post('/api/repo/:owner/:repo/delete', deleteRepo); |
| 132 | }; |
| 133 | |