diff --git a/app.js b/app.js index 7ba58e3..ed8e52f 100644 --- a/app.js +++ b/app.js @@ -23,6 +23,7 @@ let fs = require('fs'), path = require('path'); global.syzoj = { + rootDir: __dirname, config: require('./config.json'), models: [], modules: [], diff --git a/modules/admin.js b/modules/admin.js new file mode 100644 index 0000000..5ea56e7 --- /dev/null +++ b/modules/admin.js @@ -0,0 +1,347 @@ +/* + * This file is part of SYZOJ. + * + * Copyright (c) 2016 Menci + * + * SYZOJ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * SYZOJ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with SYZOJ. If not, see . + */ + +let Problem = syzoj.model('problem'); +let JudgeState = syzoj.model('judge_state'); +let Article = syzoj.model('article'); +let Contest = syzoj.model('contest'); +let User = syzoj.model('user'); +let UserPrivilege = syzoj.model('user_privilege'); + +let db = syzoj.db; + +app.get('/admin/info', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + let allSubmissionsCount = await JudgeState.count(); + let todaySubmissionsCount = await JudgeState.count({ submit_time: { $gte: syzoj.utils.getCurrentDate(true) } }); + let problemsCount = await Problem.count(); + let articlesCount = await Article.count(); + let contestsCount = await Contest.count(); + let usersCount = await User.count(); + + res.render('admin_info', { + allSubmissionsCount: allSubmissionsCount, + todaySubmissionsCount: todaySubmissionsCount, + problemsCount: problemsCount, + articlesCount: articlesCount, + contestsCount: contestsCount, + usersCount: usersCount + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +let configItems = { + 'title': { name: '站点标题', type: String }, + '邮箱验证': null, + 'register_mail.enabled': { name: '启用', type: Boolean }, + 'register_mail.address': { name: '发件人地址', type: String }, + 'register_mail.key': { name: '密钥', type: String }, + '默认参数': null, + 'default.problem.time_limit': { name: '时间限制(单位:ms)', type: Number }, + 'default.problem.memory_limit': { name: '空间限制(单位:MiB)', type: Number }, + '限制': null, + 'limit.time_limit': { name: '最大时间限制(单位:ms)', type: Number }, + 'limit.memory_limit': { name: '最大空间限制(单位:MiB)', type: Number }, + 'limit.data_size': { name: '所有数据包大小(单位:byte)', type: Number }, + 'limit.testdata': { name: '测试数据大小(单位:byte)', type: Number }, + 'limit.submit_code': { name: '代码长度(单位:byte)', type: Number }, + 'limit.submit_answer': { name: '提交答案题目答案大小(单位:byte)', type: Number }, + 'limit.custom_test_input': { name: '自定义测试输入文件大小(单位:byte)', type: Number }, + 'limit.testdata_filecount': { name: '测试数据文件数量(单位:byte)', type: Number }, + '每页显示数量': null, + 'page.problem': { name: '题库', type: Number }, + 'page.judge_state': { name: '提交记录', type: Number }, + 'page.problem_statistics': { name: '题目统计', type: Number }, + 'page.ranklist': { name: '排行榜', type: Number }, + 'page.discussion': { name: '讨论', type: Number }, + 'page.article_comment': { name: '评论', type: Number }, + 'page.contest': { name: '比赛', type: Number }, + '编译器版本': null, + 'languages.cpp.version': { name: 'C++', type: String }, + 'languages.cpp11.version': { name: 'C++11', type: String }, + 'languages.csharp.version': { name: 'C#', type: String }, + 'languages.c.version': { name: 'C', type: String }, + 'languages.vala.version': { name: 'Vala', type: String }, + 'languages.java.version': { name: 'Java', type: String }, + 'languages.pascal.version': { name: 'Pascal', type: String }, + 'languages.lua.version': { name: 'Lua', type: String }, + 'languages.luajit.version': { name: 'LuaJIT', type: String }, + 'languages.python2.version': { name: 'Python 2', type: String }, + 'languages.python3.version': { name: 'Python 3', type: String }, + 'languages.nodejs.version': { name: 'Node.js', type: String }, + 'languages.ruby.version': { name: 'Ruby', type: String }, + 'languages.haskell.version': { name: 'Haskell', type: String }, + 'languages.ocaml.version': { name: 'OCaml', type: String }, + 'languages.vbnet.version': { name: 'Visual Basic', type: String } +}; + +app.get('/admin/config', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + for (let i in configItems) { + if (!configItems[i]) continue; + configItems[i].val = eval(`syzoj.config.${i}`); + } + + res.render('admin_config', { + items: configItems + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.post('/admin/config', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + for (let i in configItems) { + if (!configItems[i]) continue; + if (req.body[i]) { + let val; + if (configItems[i].type === Boolean) { + val = req.body[i] === 'on'; + } else { + val = req.body[i]; + } + + let f = new Function('val', `syzoj.config.${i} = val`); + f(val); + } + } + + await syzoj.utils.saveConfig(); + + res.redirect(syzoj.utils.makeUrl(['admin', 'config'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.get('/admin/privilege', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + let a = await UserPrivilege.query(); + let users = {}; + for (let p of a) { + if (!users[p.user_id]) { + users[p.user_id] = { + user: await User.fromID(p.user_id), + privileges: [] + }; + } + + users[p.user_id].privileges.push(p.privilege); + } + + res.render('admin_privilege', { + users: Object.values(users) + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.post('/admin/privilege', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + let data = JSON.parse(req.body.data); + for (let id in data) { + let user = await User.fromID(id); + if (!user) throw new ErrorMessage(`不存在 ID 为 ${id} 的用户。`); + await user.setPrivileges(data[id]); + } + + res.redirect(syzoj.utils.makeUrl(['admin', 'privilege'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.get('/admin/rejudge', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + res.render('admin_rejudge', { + form: {}, + count: null + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.post('/admin/rejudge', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + let user = await User.fromName(req.body.submitter || ''); + let where = {}; + if (user) where.user_id = user.id; + else if (req.body.submitter) where.user_id = -1; + + let minID = parseInt(req.body.min_id); + if (isNaN(minID)) minID = 0; + let maxID = parseInt(req.body.max_id); + if (isNaN(maxID)) maxID = 2147483647; + + where.id = { + $and: { + $gte: parseInt(minID), + $lte: parseInt(maxID) + } + }; + + let minScore = parseInt(req.body.min_score); + if (isNaN(minScore)) minScore = 0; + let maxScore = parseInt(req.body.max_score); + if (isNaN(maxScore)) maxScore = 100; + + where.score = { + $and: { + $gte: parseInt(minScore), + $lte: parseInt(maxScore) + } + }; + + let minTime = syzoj.utils.parseDate(req.body.min_time); + if (isNaN(minTime)) minTime = 0; + let maxTime = syzoj.utils.parseDate(req.body.max_time); + if (isNaN(maxTime)) maxTime = 2147483647; + + where.submit_time = { + $and: { + $gte: parseInt(minTime), + $lte: parseInt(maxTime) + } + }; + + if (req.body.language) { + if (req.body.language === 'submit-answer') where.language = ''; + else where.language = req.body.language; + } + if (req.body.status) where.status = { $like: req.body.status + '%' }; + if (req.body.problem_id) where.problem_id = parseInt(req.body.problem_id) || -1; + + let count = await JudgeState.count(where); + if (req.body.type === 'rejudge') { + let submissions = await JudgeState.query(null, where); + for (let submission of submissions) { + await submission.rejudge(); + } + } + + res.render('admin_rejudge', { + form: req.body, + count: count + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.get('/admin/links', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + res.render('admin_links', { + links: syzoj.config.links || [] + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.post('/admin/links', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + syzoj.config.links = JSON.parse(req.body.data); + await syzoj.utils.saveConfig(); + + res.redirect(syzoj.utils.makeUrl(['admin', 'links'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.get('/admin/raw', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + res.render('admin_raw', { + data: JSON.stringify(syzoj.config, null, 2) + }); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); + +app.post('/admin/raw', async (req, res) => { + try { + if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。'); + + syzoj.config = JSON.parse(req.body.data); + await syzoj.utils.saveConfig(); + + res.redirect(syzoj.utils.makeUrl(['admin', 'raw'])); + } catch (e) { + syzoj.log(e); + res.render('error', { + err: e + }) + } +}); diff --git a/static/script.js b/static/script.js index 1aa6db4..ad77f57 100644 --- a/static/script.js +++ b/static/script.js @@ -2,8 +2,21 @@ var addUrlParam = function (url, key, val) { var newParam = encodeURIComponent(key) + '=' + encodeURIComponent(val); url = url.split('#')[0]; - if (url.indexOf('?') === -1) url += '?' + newParam; - else url += '&' + newParam; + var twoPart = url.split('?'), params = {}; + var tmp = twoPart[1] ? twoPart[1].split('&') : []; + for (let i in tmp) { + let a = tmp[i].split('='); + params[a[0]] = a[1]; + } + + params[key] = val; + + url = twoPart[0] + '?'; + for (let key in params) { + url += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'; + } + + url = url.substring(0, url.length - 1); return url; }; diff --git a/utility.js b/utility.js index baf67ae..2f3384f 100644 --- a/utility.js +++ b/utility.js @@ -173,8 +173,15 @@ module.exports = { parseDate(s) { return parseInt(+new Date(s) / 1000); }, - getCurrentDate() { - return parseInt(+new Date / 1000); + getCurrentDate(removeTime) { + let d = new Date; + if (removeTime) { + d.setHours(0); + d.setMinutes(0); + d.setSeconds(0); + d.setMilliseconds(0); + } + return parseInt(+d / 1000); }, makeUrl(req_params, form) { let res = ''; @@ -348,5 +355,9 @@ module.exports = { } catch (e) { return false; } + }, + async saveConfig() { + let fs = require('fs-extra'); + fs.writeFileAsync(syzoj.rootDir + '/config.json', JSON.stringify(syzoj.config, null, 2)); } }; diff --git a/views/admin.ejs b/views/admin.ejs deleted file mode 100644 index d0a60d0..0000000 --- a/views/admin.ejs +++ /dev/null @@ -1,25 +0,0 @@ -<% include header %> -
- -
-
- This is an stretched grid column. This segment will always match the tab height -
-
-
-<% include footer %> diff --git a/views/admin_config.ejs b/views/admin_config.ejs new file mode 100644 index 0000000..826538c --- /dev/null +++ b/views/admin_config.ejs @@ -0,0 +1,54 @@ +<% this.adminPage = 'config'; %> +<% include admin_header %> +
+<% +function showTitle(title) { + %>

<%= title %>

<% +} + +function showStringItem(name, text, val) { + %> +
+ + +
+ <% +} + +function showNumberItem(name, text, val) { + %> +
+ + +
+ <% +} + +function showBooleanItem(name, text, val) { + %> +
+
+ checked<% } %>> + +
+
+ <% +} + +for (let item in items) { + if (items[item] === null) { + showTitle(item); + } else if (items[item].type === String) { + showStringItem(item, items[item].name, items[item].val); + } else if (items[item].type === Number) { + showNumberItem(item, items[item].name, items[item].val); + } else { + showBooleanItem(item, items[item].name, items[item].val); + } +} +%> +
+ +
+
+<% include admin_footer %> diff --git a/views/admin_footer.ejs b/views/admin_footer.ejs new file mode 100644 index 0000000..26e6538 --- /dev/null +++ b/views/admin_footer.ejs @@ -0,0 +1,3 @@ + + +<% include footer %> diff --git a/views/admin_header.ejs b/views/admin_header.ejs new file mode 100644 index 0000000..ccdd354 --- /dev/null +++ b/views/admin_header.ejs @@ -0,0 +1,24 @@ +<% +let items = { + info: '统计信息', + config: '系统配置', + privilege: '权限管理', + rejudge: '一键重测', + links: '友链管理', + raw: '配置文件' +}; +%> +<% this.title = items[this.adminPage] + ' - 后台管理'; %> +<% include header %> +

后台管理

+
+ +
diff --git a/views/admin_info.ejs b/views/admin_info.ejs new file mode 100644 index 0000000..3dc90d5 --- /dev/null +++ b/views/admin_info.ejs @@ -0,0 +1,79 @@ +<% this.adminPage = 'info'; %> +<% include admin_header %> + +
+
+
+ + <%= allSubmissionsCount %> +
+
+ 总评测量 +
+
+
+
+ + <%= todaySubmissionsCount %> +
+
+ 今日评测量 +
+
+
+
+ + <%= problemsCount %> +
+
+ 题目数量 +
+
+
+
+ + <%= articlesCount %> +
+
+ 讨论数量 +
+
+
+
+ + <%= contestsCount %> +
+
+ 比赛数量 +
+
+
+
+ + <%= usersCount %> +
+
+ 用户数量 +
+
+
+<% include admin_footer %> diff --git a/views/admin_links.ejs b/views/admin_links.ejs new file mode 100644 index 0000000..55f4bae --- /dev/null +++ b/views/admin_links.ejs @@ -0,0 +1,78 @@ +<% +this.adminPage = 'links'; +%> +<% include admin_header %> + + + + + + + + + + + <% for (let i = 0; i < links.length; i++) { %> + + + + + + <% } %> + +
名称链接删除
<%= links[i].title %><%= links[i].url %> + + +
+ +
+
+
+ + +
+
+ + +
+
+
添加
+
+ +
+ +
+ + +<% include admin_footer %> diff --git a/views/admin_privilege.ejs b/views/admin_privilege.ejs new file mode 100644 index 0000000..e67f27c --- /dev/null +++ b/views/admin_privilege.ejs @@ -0,0 +1,63 @@ +<% +this.adminPage = 'privilege'; +let privileges = { + manage_problem: '管理题目', + manage_problem_tag: '管理题目标签', + manage_user: '管理用户', +}; +%> +<% include admin_header %> + + + + + + <% for (let privilege in privileges) { %> + + <% } %> + + + + + <% for (let user of users) { %> + + + + <% for (let privilege in privileges) { %> + + <% } %> + + <% } %> + +
ID用户名<%= privileges[privilege] %>
<%= user.user.id %><%= user.user.username %> +
+ checked<% } %>> + +
+
+ +
+ +
+ +
+ +
+ + +<% include admin_footer %> diff --git a/views/admin_raw.ejs b/views/admin_raw.ejs new file mode 100644 index 0000000..3a4093f --- /dev/null +++ b/views/admin_raw.ejs @@ -0,0 +1,34 @@ +<% this.adminPage = 'raw'; %> +<% include admin_header %> +
+ +
<%= data %>
+ + + + + + +
+ +
+
+<% include admin_footer %> diff --git a/views/admin_rejudge.ejs b/views/admin_rejudge.ejs new file mode 100644 index 0000000..2bceb43 --- /dev/null +++ b/views/admin_rejudge.ejs @@ -0,0 +1,108 @@ +<% this.adminPage = 'rejudge'; %> +<% include admin_header %> +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + <% if (form.type === 'rejudge') { %> + + <% } else if (count !== null) { %> + <% if (count === 0) { %>没有符合条件的记录<% } else { %>重测 <%= count %> 条记录<% } %> + + + <% } %> +
+
+ +<% include admin_footer %> diff --git a/views/header.ejs b/views/header.ejs index 2ca8ea7..c3210be 100644 --- a/views/header.ejs +++ b/views/header.ejs @@ -34,6 +34,9 @@ <%= user.username %><% if (user.nameplate) { %><%- user.nameplate %><% } %>