├── database ├── temp.sql ├── cleanup.sql ├── init.js ├── patch0323.sql ├── init.sh ├── triggers.sql ├── db.js ├── views.sql ├── initialize.sql ├── functions.sql └── structure.sql ├── api ├── test.js ├── admin │ ├── admin_email.js │ ├── admin_problem.js │ ├── admin_upload.js │ ├── admin.js │ ├── admin_message.js │ ├── admin_problem_spj.js │ ├── admin_problem_data.js │ ├── admin_post.js │ ├── admin_role.js │ ├── admin_report.js │ ├── admin_problem_add.js │ ├── admin_problem_update.js │ ├── admin_user.js │ └── admin_contest.js ├── user │ ├── user_contest.js │ ├── user_problems.js │ ├── user_check.js │ ├── user_login.js │ ├── user.js │ ├── user_api.js │ ├── user_update.js │ └── user_register.js ├── captcha.js ├── danmaku.js ├── simple_picture.js ├── contests.js ├── code.js ├── rank.js ├── avatar.js ├── posts.js ├── video.js ├── problems.js ├── api.js ├── version.js ├── problem.js ├── message.js ├── contest.js ├── post.js └── status.js ├── init ├── public │ └── ic_fix.png ├── defaults │ └── avatars │ │ └── default.png ├── session_template.js ├── db_template.js ├── mail_template.js ├── path_template.js ├── reason.js ├── rsa_template.js ├── init.js └── init.ejs ├── lib ├── md5.js ├── message.js ├── redis.js ├── problem.js ├── extension.js ├── apikey.js ├── spawn.js ├── rsa.js ├── prototype.js ├── rate-limit.js ├── session.js ├── captcha.js ├── permission.js ├── judge.js ├── mail.js ├── git.js ├── chat.js └── form-check.js ├── config ├── redis.js ├── basic.js └── mailTemplate.js ├── .gitignore ├── readme.md ├── .github └── workflows │ ├── node.js.yml │ └── codeql-analysis.yml ├── package.json ├── LICENSE ├── nginx └── default ├── app.js └── bin └── init /database/temp.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/test.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | module.exports = router 4 | -------------------------------------------------------------------------------- /init/public/ic_fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NankaiACM/NKOJ-Back-End/HEAD/init/public/ic_fix.png -------------------------------------------------------------------------------- /api/admin/admin_email.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | module.exports = router 4 | -------------------------------------------------------------------------------- /api/user/user_contest.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | module.exports = router 4 | -------------------------------------------------------------------------------- /database/cleanup.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | DROP SCHEMA IF EXISTS public CASCADE; 3 | CREATE SCHEMA public; 4 | COMMIT; 5 | -------------------------------------------------------------------------------- /init/defaults/avatars/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NankaiACM/NKOJ-Back-End/HEAD/init/defaults/avatars/default.png -------------------------------------------------------------------------------- /init/session_template.js: -------------------------------------------------------------------------------- 1 | // Template File 2 | module.exports = { 3 | name: 'oj.sid', 4 | secret: 'something secret' 5 | } 6 | -------------------------------------------------------------------------------- /lib/md5.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const md5 = (string) => { 3 | 'use strict' 4 | return crypto.createHash('md5').update(string).digest('hex') 5 | } 6 | 7 | module.exports = md5 8 | -------------------------------------------------------------------------------- /config/redis.js: -------------------------------------------------------------------------------- 1 | // TODO: move to 1 database 2 | module.exports = { 3 | DB_BASIC: 0, 4 | DB_USER: 1, 5 | DB_EMAIL_VERIFY: 2, 6 | DB_REDIS: 4, 7 | DB_CONTEST: 5, 8 | DB_RATE_LIMIT: 6, 9 | DB_SESSION_STORE: 9 10 | } 11 | -------------------------------------------------------------------------------- /init/db_template.js: -------------------------------------------------------------------------------- 1 | // Template File 2 | module.exports = { 3 | user: 'ojadmin', 4 | // password: '', // if postgres pg_hba set to 'trust', don't need this field 5 | host: 'localhost', 6 | port: 5432, 7 | database: 'onlinejudge' 8 | } 9 | -------------------------------------------------------------------------------- /api/captcha.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const captcha = require('../lib/captcha') 3 | 4 | router.get('/:type*?', (req, res) => { 5 | 'use strict' 6 | return captcha.middleware(req.params.type)(req, res) 7 | }) 8 | 9 | module.exports = router 10 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | const db = require('../database/db') 2 | const sendMessage = async function (to, title, content) { 3 | return await db.query('INSERT INTO messages (a, b, title, content) VALUES (NULL, $1, $2, $3) RETURNING *', [to, title, content]) 4 | } 5 | module.exports = { 6 | sendMessage 7 | } 8 | -------------------------------------------------------------------------------- /lib/redis.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis') 2 | const bluebird = require('bluebird') 3 | bluebird.promisifyAll(redis.RedisClient.prototype) 4 | bluebird.promisifyAll(redis.Multi.prototype) 5 | module.exports = (database) => { 6 | const client = redis.createClient() 7 | client.select(database) 8 | return client 9 | } 10 | -------------------------------------------------------------------------------- /api/danmaku.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | 4 | router.get('/', async (req, res) => { 5 | 'use strict' 6 | const ret = await db.query('SELECT * FROM user_danmaku ORDER BY "when" desc LIMIT 100') 7 | res.ok(ret.rows.reverse()) 8 | }) 9 | 10 | module.exports = router 11 | -------------------------------------------------------------------------------- /database/init.js: -------------------------------------------------------------------------------- 1 | const {Pool} = require('pg') 2 | const config = require('../config/postgres') 3 | 4 | const pool = new Pool(config) 5 | 6 | pool.query('SELECT NOW() as now', (err, res) => { 7 | if (err) console.log('Database connected error.', err) 8 | else console.log(`Database connected at ${res.rows[0].now}`) 9 | }) 10 | 11 | module.exports = pool 12 | -------------------------------------------------------------------------------- /api/admin/admin_problem.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const add = require('./admin_problem_add') 3 | const update = require('./admin_problem_update') 4 | const data = require('./admin_problem_data') 5 | const spj = require('./admin_problem_spj') 6 | 7 | router.use('/add', add) 8 | router.use('/update', update) 9 | router.use('/data', data) 10 | router.use('/spj', spj) 11 | 12 | module.exports = router 13 | -------------------------------------------------------------------------------- /api/simple_picture.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const fs = require('fs') 3 | const {PICTURE_PATH} = require('../config/basic') 4 | 5 | router.get('/:name', async (req, res) => { 6 | 'use strict' 7 | const file = req.params.name 8 | if (fs.existsSync(`${PICTURE_PATH}/${file}`)) { 9 | res.sendFile(`${PICTURE_PATH}/${file}`) 10 | } else { 11 | res.fatal(404) 12 | } 13 | }) 14 | 15 | module.exports = router 16 | -------------------------------------------------------------------------------- /init/mail_template.js: -------------------------------------------------------------------------------- 1 | // Template File 2 | module.exports = { 3 | transporter: { 4 | host: 'smtp.yourhost.com', 5 | secureConnection: true, // use SSL 6 | port: 465, 7 | secure: true, // secure:true for port 465, secure:false for port 587 8 | auth: { 9 | user: 'some user', 10 | pass: 'some pass' 11 | } 12 | }, 13 | regMailOptions: { 14 | from: '"Display Name" ', 15 | subject: 'Your Subject' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/problem.js: -------------------------------------------------------------------------------- 1 | const generateFileString = (json) => { 2 | const strArr = [] 3 | Object.keys(json).forEach((k, v) => { 4 | strArr.push(`$$${k}$$\n${json[k]}\n$$${k}$$`) 5 | }) 6 | return strArr.join() 7 | } 8 | 9 | const splitFileString = (str) => { 10 | const o = {} 11 | const r = /\$\$(.+?)\$\$(?:\n)([\s\S]+?)(?:\$\$\1\$\$)/g 12 | while (item = r.exec(str)) o[item[1]] = item[2] 13 | return o 14 | } 15 | 16 | module.exports = { 17 | generateFileString, 18 | splitFileString 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | 19 | /node_modules/ 20 | 21 | # Private Configurations 22 | /config/postgres.js 23 | /config/path.js 24 | /config/mail.js 25 | /config/rsa.js 26 | /config/session.js 27 | 28 | # Temp files 29 | /database/temp.sql 30 | -------------------------------------------------------------------------------- /init/path_template.js: -------------------------------------------------------------------------------- 1 | // Template File 2 | module.exports = { 3 | BASE_URL: 'http://localhost', 4 | DATA_BASE: '/var/www/data', 5 | AVATAR_PATH: 'avatars', 6 | PICTURE: 'pictures', 7 | VIDEO: 'videos', 8 | DIST_PATH: 'dist', 9 | PROBLEM_PATH: 'problems', 10 | PROBLEM_DATA_PATH: 'problem_data', 11 | PROBLEM_SPJ_PATH: 'problem_spj', 12 | PUBLIC_PATH: 'public', 13 | TEMP_PATH: 'temp', 14 | CONTEST_PATH: 'contests', 15 | SOLUTION_PATH: 'solutions', 16 | FRONT_END_PATH: '../alpha' 17 | } 18 | -------------------------------------------------------------------------------- /lib/extension.js: -------------------------------------------------------------------------------- 1 | const language_ext = { 2 | c: 'c', 3 | cpp: 'c++', 4 | py: 'python', 5 | js: 'javascript', 6 | go: 'go', 7 | txt: 'text', 8 | pypy3: 'pypy3', 9 | [0]: 'c', 10 | [1]: 'cpp', 11 | [2]: 'js', 12 | [3]: 'py', 13 | [4]: 'go', 14 | [5]: 'txt', 15 | [6]: 'pypy3', 16 | } 17 | 18 | const handler = { 19 | get (target, name) { 20 | return name in target ? target[name] : null 21 | } 22 | } 23 | 24 | const p = new Proxy(language_ext, handler) 25 | 26 | module.exports = p 27 | -------------------------------------------------------------------------------- /init/reason.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | REASON_DATABASE_CONFIG: 'Database Configuration File is Missing or Failed to Load', 3 | REASON_DATABASE_CONNECT: 'Database Connection Failed', 4 | REASON_PATH_CONFIG: 'Data Path Configuration File is Missing or Failed to Load', 5 | REASON_INACCESSIBLE_PATH: 'The Data Path is Improper Set', 6 | REASON_REDIS_ERROR: 'Redis Server Connection Failed', 7 | REASON_RSA_CONFIG: 'Rsa Configuration is Missing', 8 | REASON_RSA_Decrypt: 'Rsa Key Could not Decrypt Encrypted Message' 9 | } 10 | -------------------------------------------------------------------------------- /api/admin/admin_upload.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {PUBLIC_PATH} = require('../../config/basic') 3 | const multer = require('multer') 4 | const path = require('path') 5 | 6 | const upload = multer({ 7 | storage: multer.diskStorage({ 8 | destination: (req, file, cb) => { 9 | cb(null, PUBLIC_PATH) 10 | }, 11 | filename: (req, file, cb) => { 12 | cb(null, `${Date.now().toString(36)}${Math.floor(Math.random() * 100).toString(36)}${path.extname(file.originalname)}`) 13 | } 14 | }) 15 | }) 16 | 17 | router.post('/', upload.single('file'), (req, res) => { 18 | if (!req.file) return res.fail(1) 19 | res.ok([`/public/${req.file.filename}`], {errno: 0}) 20 | }) 21 | 22 | module.exports = router 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### How To Install 2 | 3 | + install node 10.0.0+ 4 | + run `npm install` on this folder 5 | + run `npm install pm2 -g` to install pm2 and save it globally 6 | + run `pm2 start bin/init --name api --watch` on this folder 7 | + run `pm2 monit` or `pm2 log api` to monitor its state 8 | 9 | ### Other DEPENDENCY This Project May Use 10 | 11 | + postgres 10.3 12 | + run `./database/init.sh` to init the database 13 | + redis 4.0.8+ (Deprecated Dependency) 14 | + gcc 6.4.0+ (Configure it at `/config`) 15 | 16 | ### 在windows上的部署细节 17 | 18 | + 执行完install的命令后,还需要安装postgresql和redis,可以去官网查看安装配置方法 19 | + config中主要需要对postgresql进行配置,填写设置好的密码,其他都是默认设置,可以不用更改 20 | + 使用webstorm可以下载sql插件,执行create.sql文件建立数据库 21 | 22 | 23 | ### 已经实现的接口 24 | 25 | + 待补充... 26 | -------------------------------------------------------------------------------- /api/contests.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | const fc = require('../lib/form-check') 4 | 5 | const listContest = async (req, res) => { 6 | let form = req.fcResult 7 | let offset = form.l || 0 8 | let requested = form.r || 20 9 | let limit = requested > 50 ? 50 : requested 10 | let result = await db.query('SELECT * FROM user_contests order by contest_id desc limit $1 offset $2', [limit, offset]) 11 | return res.ok({ 12 | requested: requested, 13 | served: result.rows.length, 14 | is_end: result.rows.length < limit, 15 | list: result.rows 16 | }) 17 | } 18 | 19 | router.get('/:l(\\d+)?/:r(\\d+)?', fc.all(['l', 'r']), listContest) 20 | router.get('/list/:l(\\d+)?/:r(\\d+)?', fc.all(['l', 'r']), listContest) 21 | 22 | module.exports = router 23 | -------------------------------------------------------------------------------- /api/code.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const fc = require('../lib/form-check') 3 | const fs = require('fs') 4 | const db = require('../database/db') 5 | const {getSolutionStructure} = require('../lib/judge') 6 | 7 | router.get('/:pid', fc.all(['pid']), async (req, res, next) => { 8 | const pid = req.fcResult.pid 9 | const ret = await db.query('SELECT solution_id FROM solutions WHERE status_id = 107 AND problem_id = $1 LIMIT 1', [pid]) 10 | if (ret.rows.length === 0) return res.fail(404) 11 | 12 | const sid = ret.rows[0].solution_id 13 | const struct = getSolutionStructure(sid) 14 | 15 | const codeFile = struct.file.code_base + 'cpp' 16 | try { 17 | const code = fs.readFileSync(codeFile, 'utf8') 18 | res.ok({code}) 19 | } catch (e) { 20 | next(e) 21 | } 22 | 23 | }) 24 | 25 | module.exports = router 26 | -------------------------------------------------------------------------------- /api/user/user_problems.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../../database/db') 3 | const fc = require('../../lib/form-check') 4 | 5 | router.get('/list', async (req, res) => { 6 | 'use strict' 7 | const values = [req.session.user] 8 | const queryString = 'SELECT * FROM solutions WHERE user_id = $1' 9 | const result = await db.query(queryString, values) 10 | return res.ok(result.rows) 11 | }) 12 | 13 | router.post('/star_problem', fc.all('problem_id'), async (req, res) => { 14 | const values = [req.session.user, req.fcResult.problem_id] 15 | try { 16 | const queryString = 'INSERT INTO user_favo (user_id,problem_id) VALUES ($1,$2)' 17 | await db.query(queryString, values) 18 | } 19 | catch (err) { 20 | return res.fail(520, err) 21 | } 22 | return res.ok() 23 | }) 24 | module.exports = router 25 | -------------------------------------------------------------------------------- /lib/apikey.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const db = require('../database/db') 3 | const session = require('./session') 4 | 5 | module.exports = (req, res, next) => { 6 | const key = req.body.akey || req.query.akey || undefined 7 | if (!key) return session(req, res, next) 8 | 9 | const secret = req.body.asecret || req.query.asecret || undefined 10 | if (!secret) return session(req, res, next) 11 | 12 | const hash = crypto.createHash('sha256') 13 | hash.update(`${key}${secret}`) 14 | const hashed_key = hash.digest('hex') 15 | 16 | db.query('SELECT * FROM user_api WHERE api_key = $1 and api_hashed = $2', [key, hashed_key]).then(function (ret) { 17 | if (!ret.rows.length) return res.fail(401, 'api key is not recognized') 18 | req.session = {} 19 | req.session.user = ret.rows[0].user_id 20 | next() 21 | }).catch(function (e) { 22 | next(e) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /lib/spawn.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | function spawnAsync(command, args, options) { 4 | const child = spawn(command, args, options); 5 | return new Promise(((resolve, reject) => { 6 | let stderr = ''; 7 | let stdout = ''; 8 | const _reject = (reason) => { 9 | console.error(command, args, 'rejected', reason); 10 | return reject(reason); 11 | }; 12 | child.stdout.on('data', (data) => { 13 | stdout += data; 14 | }); 15 | child.stderr.on('data', (data) => { 16 | stderr += data; 17 | }); 18 | child.addListener('error', reject); 19 | child.addListener('exit', (code) => { 20 | if (code === 0) { 21 | resolve({code, stdout, stderr}); 22 | } else { 23 | _reject({code, stdout, stderr}); 24 | } 25 | }); 26 | })); 27 | } 28 | 29 | module.exports = { 30 | spawn: spawnAsync, 31 | }; 32 | -------------------------------------------------------------------------------- /api/rank.js: -------------------------------------------------------------------------------- 1 | const db = require('../database/db') 2 | const router = require('express').Router() 3 | 4 | router.get('/', async (req, res) => { 5 | const time = [] 6 | const myDate = new Date() 7 | const myres = [] 8 | for (let i = 0; i <= 5; i++) { 9 | time[i] = (myDate.getFullYear() - i).toString() + '-09-01' 10 | } 11 | for (let i = 0; i < 5; i++) { 12 | const query = 'SELECT user_nick.nickname,user_info.submit_ac,user_info.submit_all ' + 13 | 'FROM user_info,user_nick WHERE user_nick.nick_id = user_info.nick_id AND join_time < timestamp $1 AND join_time >=timestamp $2 ' + 14 | 'ORDER BY submit_ac,submit_all DESC LIMIT 100' 15 | const result = await db.query(query, [time[i], time[i + 1]]) 16 | myres[i] = {} 17 | myres[i].data = result.rows 18 | myres[i].key = i 19 | myres[i].text = time[i] 20 | } 21 | res.ok(myres) 22 | }) 23 | 24 | module.exports = router 25 | -------------------------------------------------------------------------------- /api/user/user_check.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | const db = require('../../database/db') 4 | const validator = require('validator') 5 | 6 | router.get('/check/:type/:what', async (req, res, next) => { 7 | 'use strict' 8 | const type = req.params.type 9 | const value = req.params.what 10 | if (type === 'email') { 11 | if (validator.isEmail(value)) { 12 | const result = await db.checkEmail(value) 13 | if (result) return res.fail(422, result) 14 | return res.ok() 15 | } 16 | } else if (type === 'nickname') { 17 | // TODO: merge with fc 18 | if (value.length >= 3 && value.length <= 20 && !validator.isNumeric(value) && !validator.isEmail(value)) { 19 | const result = await db.checkName(value) 20 | if (result) return res.fail(422, result) 21 | return res.ok() 22 | } 23 | } 24 | return res.fail(422) 25 | }) 26 | 27 | module.exports = router 28 | -------------------------------------------------------------------------------- /database/patch0323.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | ALTER TABLE problems ALTER COLUMN detail_judge DROP DEFAULT; 4 | ALTER TABLE problems ALTER detail_judge TYPE smallint USING CASE WHEN detail_judge=TRUE THEN 1 ELSE 0 END; 5 | ALTER TABLE problems ALTER COLUMN detail_judge SET DEFAULT 0; 6 | 7 | ALTER TABLE problems ALTER COLUMN detail_judge DROP DEFAULT; 8 | ALTER TABLE problems ALTER detail_judge TYPE boolean USING CASE WHEN detail_judge=1 THEN TRUE ELSE FALSE END; 9 | ALTER TABLE problems ALTER COLUMN detail_judge SET DEFAULT FALSE; 10 | 11 | ALTER TABLE problems ALTER COLUMN special_judge DROP DEFAULT; 12 | ALTER TABLE problems ALTER special_judge TYPE smallint USING CASE WHEN detail_judge=TRUE THEN 1 ELSE 0 END; 13 | ALTER TABLE problems ALTER COLUMN special_judge SET DEFAULT 0; 14 | 15 | ALTER TABLE solutions ADD COLUMN detail jsonb DEFAULT '{}'::jsonb; 16 | ALTER TABLE solutions ADD COLUMN compile_info text DEFAULT ''; 17 | 18 | commit; -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /api/avatar.js: -------------------------------------------------------------------------------- 1 | const {DB_USER} = require('../config/redis') 2 | const redis = require('../lib/redis')(DB_USER) 3 | const db = require('../database/db') 4 | const router = require('express').Router() 5 | const fs = require('fs') 6 | const {AVATAR_PATH} = require('../config/basic') 7 | const avatar = async (key) => { 8 | 'use strict' 9 | if (Number.isInteger(Number(key))) 10 | return await redis.getAsync(`avatar:${key}`) 11 | else { 12 | key = await db.query('SELECT user_id FROM users WHERE nickname = $1', [key]) 13 | if (key.rows.length) return await redis.getAsync(`avatar:${key.rows[0].user_id}`) 14 | } 15 | return 'default.png' 16 | } 17 | 18 | router.get('/:key', async (req, res) => { 19 | 'use strict' 20 | const key = req.params.key 21 | const file = await avatar(key) 22 | if (fs.existsSync(`${AVATAR_PATH}/${file}`)) { 23 | res.sendFile(`${AVATAR_PATH}/${file}`) 24 | } else { 25 | res.sendFile(`${AVATAR_PATH}/default.png`) 26 | } 27 | }) 28 | 29 | module.exports = router 30 | -------------------------------------------------------------------------------- /config/basic.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { 3 | DIST_PATH, PUBLIC_PATH, TEMP_PATH, PROBLEM_SPJ_PATH, 4 | AVATAR_PATH, CONTEST_PATH, DATA_BASE, 5 | PROBLEM_DATA_PATH, PROBLEM_PATH, SOLUTION_PATH, FRONT_END_PATH, PICTURE,VIDEO 6 | } = require('./path') 7 | 8 | module.exports = { 9 | DATA_BASE: DATA_BASE, 10 | PICTURE_PATH: path.resolve(PICTURE), 11 | VIDEO_PATH: path.resolve(VIDEO), 12 | AVATAR_PATH: path.resolve(DATA_BASE, AVATAR_PATH), 13 | DIST_PATH: path.resolve(DATA_BASE, DIST_PATH), 14 | PROBLEM_PATH: path.resolve(DATA_BASE, PROBLEM_PATH), 15 | PROBLEM_DATA_PATH: path.resolve(DATA_BASE, PROBLEM_DATA_PATH), 16 | PROBLEM_SPJ_PATH: path.resolve(DATA_BASE, PROBLEM_SPJ_PATH), 17 | PUBLIC_PATH: path.resolve(DATA_BASE, PUBLIC_PATH), 18 | TEMP_PATH: path.resolve(DATA_BASE, TEMP_PATH), 19 | CONTEST_PATH: path.resolve(DATA_BASE, CONTEST_PATH), 20 | SOLUTION_PATH: path.resolve(DATA_BASE, SOLUTION_PATH), 21 | FRONT_END_PATH: path.resolve(DATA_BASE, FRONT_END_PATH), 22 | BACK_END_PATH: path.resolve(__dirname, '..') 23 | } 24 | -------------------------------------------------------------------------------- /api/admin/admin.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const contest = require('./admin_contest') 3 | const email = require('./admin_email') 4 | const message = require('./admin_message') 5 | const role = require('./admin_role') 6 | const user = require('./admin_user') 7 | const problem = require('./admin_problem') 8 | const upload = require('./admin_upload') 9 | const rank = require('../rank') 10 | const post = require('./admin_post') 11 | const report = require('./admin_report') 12 | const {require_perm, MANAGE_ROLE, SUPER_ADMIN, EDIT_CONTEST_ALL} = require('../../lib/permission') 13 | 14 | router.use('/contest', require_perm(EDIT_CONTEST_ALL), contest) 15 | router.use('/email', require_perm(SUPER_ADMIN), email) 16 | router.use('/message', require_perm(SUPER_ADMIN), message) 17 | router.use('/role', require_perm(MANAGE_ROLE), role) 18 | router.use('/user', require_perm(MANAGE_ROLE), user) 19 | router.use('/rank', rank) 20 | router.use('/problem', problem) 21 | router.use('/post', post) 22 | router.use('/report', report) 23 | router.use('/upload', upload) 24 | 25 | module.exports = router 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nkoj-api", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "./bin/init", 6 | "scripts": { 7 | "start": "node ./bin/init" 8 | }, 9 | "author": "SunriseFox", 10 | "license": "ISC", 11 | "dependencies": { 12 | "adm-zip": "^0.4.7", 13 | "bluebird": "^3.5.1", 14 | "body-parser": "^1.18.2", 15 | "connect-redis": "^3.3.3", 16 | "diff2html": "^2.3.3", 17 | "ejs": "^2.6.1", 18 | "express": "^4.16.3", 19 | "express-session": "^1.15.6", 20 | "express-validator": "^5.2.0", 21 | "http-proxy-middleware": "^0.19.1", 22 | "jsdecrypt": "^1.0.4", 23 | "multer": "^1.3.0", 24 | "node-forge": "^0.10.0", 25 | "nodemailer": "^6.4.16", 26 | "pg": "^7.4.3", 27 | "redis": "^2.8.0", 28 | "sprintf-js": "^1.1.1", 29 | "striptags": "latest", 30 | "svg-captcha": "^1.3.11", 31 | "validator": "^10.2.0", 32 | "ws": "^6.2.0", 33 | "xss": "^1.0.6" 34 | }, 35 | "devDependencies": { 36 | "debug": "^4.1.1", 37 | "morgan": "^1.9.0" 38 | }, 39 | "optionalDependencies": { 40 | "sharp": "^0.20.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nankai ACM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/posts.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | const fc = require('../lib/form-check') 4 | 5 | const listDiscuss = async (req, res) => { 6 | 'use strict' 7 | const form = req.fcResult 8 | const offset = form.l || 0 9 | const requested = form.r || 20 10 | const limit = requested > 50 ? 50 : requested 11 | 12 | const pid = form.pid 13 | 14 | // TODO: 帖子列表 15 | 16 | const result = await db.query( 17 | 'SELECT post_id, user_id, nickname, title, since, last_active_date, last_active_user, positive, negative' + 18 | ' FROM post WHERE removed_date IS NULL AND parent_id IS NULL AND (CASE $1::int WHEN 0 THEN (problem_id IS NULL) ELSE (problem_id = $1) END)' + 19 | ' ORDER BY last_active_date DESC LIMIT $2 OFFSET $3' 20 | , [pid, limit, offset] 21 | ) 22 | return res.ok({ 23 | requested: requested, 24 | served: result.rows.length, 25 | is_end: result.rows.length < limit, 26 | list: result.rows 27 | }) 28 | } 29 | 30 | router.get('/:pid/:l(\\d+)?/:r(\\d+)?', fc.all(['pid', 'l', 'r']), listDiscuss) 31 | router.get('/list/:pid/:l(\\d+)?/:r(\\d+)?', fc.all(['pid', 'l', 'r']), listDiscuss) 32 | 33 | module.exports = router 34 | -------------------------------------------------------------------------------- /api/video.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const fs = require('fs') 3 | const {VIDEO_PATH} = require('../config/basic') 4 | const join=require('path').join 5 | 6 | function findSync(startPath) { 7 | let result=[]; 8 | function finder(path) { 9 | let files=fs.readdirSync(path); 10 | files.forEach((val,index) => { 11 | let fPath=join(path,val); 12 | let stats=fs.statSync(fPath); 13 | if(stats.isDirectory()) finder(fPath); 14 | if(stats.isFile()) result.push(fPath); 15 | }); 16 | } 17 | finder(startPath); 18 | return result; 19 | } 20 | 21 | router.get('/list',async (req,res)=>{ 22 | 'use strict' 23 | let files=findSync(VIDEO_PATH) 24 | 25 | for(let i in files){ 26 | files[i]=files[i].replace(VIDEO_PATH,"") 27 | } 28 | console.log(files) 29 | res.ok(files) 30 | }) 31 | 32 | router.get('/:name', async (req, res) => { 33 | 'use strict' 34 | const file = req.params.name 35 | if (fs.existsSync(`${VIDEO_PATH}/${file}`)) { 36 | res.sendFile(`${VIDEO_PATH}/${file}`) 37 | } else { 38 | res.fatal(404) 39 | } 40 | }) 41 | 42 | module.exports = router 43 | -------------------------------------------------------------------------------- /api/admin/admin_message.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {sendMessage} = require('../../lib/message') 3 | const db = require('../../database/db') 4 | const fc = require('../../lib/form-check') 5 | // 广播私信 6 | 7 | router.post('/:uid', async (req, res) => { 8 | const title = req.body.title 9 | const message = req.body.message 10 | 11 | if (!title) return res.gen422('title', 'is required') 12 | if (!message) return res.gen422('message', 'is required') 13 | 14 | if (title.length > 60) 15 | return res.gen422('title', 'length should <= 60') 16 | 17 | let uid = req.params.uid 18 | if (uid === 'all') { 19 | await sendMessage(undefined, title, message) 20 | return res.ok() 21 | } 22 | 23 | uid = Number(uid) 24 | 25 | if (!Number.isInteger(uid)) return res.fail(422) 26 | 27 | try { 28 | sendMessage(uid, title, message) 29 | } catch (e) { 30 | return res.fail(500, e.stack || e) 31 | } 32 | 33 | res.ok() 34 | }) 35 | 36 | router.get('/withdraw/:mid', fc.all(['mid']), async (req, res) => { 37 | const ret = await db.query('DELETE FROM messages WHERE message_id = $1 AND a IS NULL', [req.fcResult.mid]) 38 | 39 | res.ok({affected: ret.rowCount}) 40 | }) 41 | 42 | // TODO: 删除一定时间前的所有公告 43 | 44 | module.exports = router 45 | -------------------------------------------------------------------------------- /api/user/user_login.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | const db = require('../../database/db') 4 | const fc = require('../../lib/form-check') 5 | 6 | const redis = require('redis') 7 | const session_client = redis.createClient() 8 | const {DB_SESSION_STORE} = require('../../config/redis') 9 | session_client.select(DB_SESSION_STORE) 10 | 11 | const captcha = require('../../lib/captcha') 12 | 13 | router.get('/logout', async (req, res) => { 14 | 'use strict' 15 | delete req.session 16 | res.ok() 17 | }) 18 | 19 | router.post('/login', captcha.check('login'), fc.all(['user', 'password']), async (req, res) => { 20 | 'use strict' 21 | 22 | const errArr = [422, [{name: 'name', message: 'might be wrong'}, { 23 | name: 'password', 24 | message: 'might be wrong' 25 | }], 'login failed'] 26 | 27 | const query = 'SELECT * FROM users WHERE (lower(nickname) = lower($1) OR email = $1) AND password = hash_password($2) LIMIT 1' 28 | 29 | let result = await db.query(query, [req.fcResult.user, req.fcResult.password]) 30 | if (result.rows.length > 0) 31 | db.postLogin(result.rows[0], req, res) 32 | else 33 | res.fail(...errArr) 34 | }) 35 | 36 | router.get('/list/login', async (req, res) => { 37 | 'use strict' 38 | session_client.hgetall(`session:${req.session.user}`, function (err, ret) { 39 | res.ok(ret) 40 | }) 41 | }) 42 | 43 | module.exports = router 44 | -------------------------------------------------------------------------------- /lib/rsa.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const jsdecrypt = require('jsdecrypt') 3 | const forge = require('node-forge') 4 | const key_public = require('../config/rsa').key_public 5 | const key_private = require('../config/rsa').key_private 6 | const key_public2 = require('../config/rsa').key_public2 7 | 8 | const decrypt = function (message) { 9 | try { 10 | return jsdecrypt.dec(key_private, message) 11 | } catch (e) { 12 | try { 13 | return crypto.privateDecrypt({ 14 | key: key_private 15 | }, Buffer.from(message, 'base64')).toString() 16 | } catch (e) { 17 | return false 18 | } 19 | } 20 | } 21 | 22 | const encrypt = function (message) { 23 | try { 24 | return crypto.publicEncrypt({ 25 | key: key_public 26 | }, Buffer.from(message)).toString('base64') 27 | } catch (e) { 28 | return false 29 | } 30 | } 31 | 32 | const genRawStr = function (len) { 33 | if (!Number.isFinite(len)) { 34 | throw new TypeError('Expected a finite number'); 35 | } 36 | return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').slice(0, len); 37 | } 38 | 39 | const getPass = function (raw) { 40 | const publicKey = forge.pki.publicKeyFromPem(key_public2) 41 | const encrypted = publicKey.encrypt(raw) 42 | return forge.util.encode64(encrypted) 43 | } 44 | 45 | module.exports = { 46 | decrypt, 47 | encrypt, 48 | genRawStr, 49 | getPass 50 | } 51 | -------------------------------------------------------------------------------- /nginx/default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | 5 | server_name _; 6 | 7 | location /old { 8 | charset gb2312; 9 | proxy_set_header Host $host; 10 | proxy_pass http://220.113.20.2/old; 11 | proxy_http_version 1.1; 12 | proxy_set_header X-Real-IP $remote_addr; 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_cache_bypass $http_upgrade; 15 | } 16 | 17 | location /nkcoj { 18 | proxy_set_header Host $host; 19 | proxy_pass http://220.113.20.2/nkcoj; 20 | proxy_http_version 1.1; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_cache_bypass $http_upgrade; 24 | } 25 | 26 | location / { 27 | proxy_set_header Upgrade $http_upgrade; 28 | proxy_set_header Connection $http_connection; 29 | proxy_pass http://localhost:3000; 30 | proxy_http_version 1.1; 31 | proxy_set_header X-Real-IP $remote_addr; 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | proxy_cache_bypass $http_upgrade; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/prototype.js: -------------------------------------------------------------------------------- 1 | const error_string = { 2 | 0: 'ok', 3 | 1: 'common error', 4 | 233: 'captcha required', 5 | 234: 'captcha not match', 6 | 400: 'bad request', 7 | 401: 'auth required', 8 | 403: 'access denied', 9 | 404: 'not found', 10 | 422: 'unprocessable entity', 11 | 429: 'too many requests', 12 | 500: 'internal server error', 13 | 501: 'not implemented', 14 | 520: 'database query error' 15 | } 16 | 17 | const ends = (res, status, code, message, assign) => { 18 | const ret = {code: code} 19 | ret.message = message || (assign ? assign.message : undefined) || error_string[code] || 'unknown error' 20 | res.status(status).json({...ret, ...assign}) 21 | } 22 | 23 | const setResponsePrototype = (req, res, next) => { 24 | res.fatal = (code, error, message) => { 25 | 'use strict' 26 | ends(res, code, code, message, {error: error}) 27 | return res 28 | } 29 | 30 | res.fail = (code, error, message) => { 31 | 'use strict' 32 | ends(res, 200, code, message, {error: error}) 33 | return res 34 | } 35 | 36 | res.ok = (data, override = {}) => { 37 | const ret = {data: data} 38 | ends(res, 200, 0, undefined, {...ret, ...override}) 39 | return res 40 | } 41 | 42 | res.gen422 = (key, err) => { 43 | return res.fail(422, [{name: key, message: err}]) 44 | } 45 | 46 | next() 47 | } 48 | 49 | module.exports = { 50 | setResponsePrototype: setResponsePrototype 51 | } 52 | -------------------------------------------------------------------------------- /lib/rate-limit.js: -------------------------------------------------------------------------------- 1 | const {DB_RATE_LIMIT} = require('../config/redis') 2 | const redis = require('./redis')(DB_RATE_LIMIT) 3 | 4 | const handler = { 5 | get (target, name) { 6 | return name in target ? {ttl: target[name][0], times: target[name][1]} : {ttl: 1, times: 10} 7 | } 8 | } 9 | 10 | const r = { 11 | sendmail: [60, 1], 12 | post: [60, 1], 13 | sendmsg: [120, 2] 14 | } 15 | 16 | const rule = new Proxy(r, handler) 17 | 18 | const limit = type => async (req, res, next) => { 19 | 'use strict' 20 | const key = `limit:${req.ip}:${type}` 21 | const ret = await redis.incrAsync(key) 22 | const ttl = rule[type].ttl 23 | if (ret > rule[type].times) { 24 | res.set({'Retry-After': ttl}) 25 | return res.fatal(429, {'Retry-After': ttl}) 26 | } 27 | redis.expire(key, ttl) 28 | next() 29 | } 30 | 31 | const require_limit = type => async (req, res, next) => { 32 | 'use strict' 33 | const key = `limit:${req.ip}:${type}` 34 | const ret = await redis.getAsync(key) 35 | const ttl = rule[type].ttl 36 | if (ret > rule[type].times) { 37 | res.set({'Retry-After': ttl}) 38 | return res.fatal(429, {'Retry-After': ttl}) 39 | } 40 | next() 41 | } 42 | 43 | const apply_limit = async (type, req) => { 44 | 'use strict' 45 | const key = `limit:${req.ip}:${type}` 46 | redis.incr(key) 47 | redis.expire(key, rule[type].ttl) 48 | } 49 | 50 | module.exports = { 51 | limit, 52 | apply_limit, 53 | require_limit 54 | } 55 | -------------------------------------------------------------------------------- /init/rsa_template.js: -------------------------------------------------------------------------------- 1 | // Template File 2 | module.exports = { 3 | key_public: '-----BEGIN PUBLIC KEY-----\n' + 4 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 5 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 6 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 7 | 'AAAAAAAAAAAAAAAAAAAAAAAA\n' + 8 | '-----END PUBLIC KEY-----\n', 9 | 10 | key_private: '-----BEGIN RSA PRIVATE KEY-----\n' + 11 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 12 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 13 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 14 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 15 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 16 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 17 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 18 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 19 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 20 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 21 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 22 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' + 23 | 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n' + 24 | '-----END RSA PRIVATE KEY-----\n' 25 | } 26 | -------------------------------------------------------------------------------- /api/problems.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | const fc = require('../lib/form-check') 4 | 5 | const listProblem = async (req, res) => { 6 | 'use strict' 7 | let form = req.fcResult 8 | let offset = form.l || 0 9 | let requested = form.r || 20 10 | let limit = requested > 50 ? 50 : requested 11 | let result = await db.query('SELECT * FROM problems order by problem_id limit $1 offset $2', [limit, offset]) 12 | 13 | //oi check 14 | let c_ret = await db.query('SELECT * FROM contest_problems LEFT JOIN contests ON contest_problems.contest_id = contests.contest_id WHERE CURRENT_TIMESTAMP < upper(contests.during) AND contests.rule=\'oi\'') 15 | let c_dic = {} 16 | c_ret.rows.forEach(function(c_p, index){ 17 | c_dic[c_p["problem_id"]] = "OI" 18 | }) 19 | result.rows.forEach(function(p, index){ 20 | if(typeof(c_dic[p.problem_id]) != "undefined"){ 21 | p.ac = 0 22 | } 23 | }) 24 | 25 | //acm check 26 | c_ret = await db.query('SELECT * FROM secret_time WHERE CURRENT_TIMESTAMP < UPPER(during) AND CURRENT_TIMESTAMP > lower(during)') 27 | if(c_ret.rows.length > 0){ 28 | result.rows.forEach(function(p, index){ 29 | p.ac = 0 30 | }) 31 | } 32 | 33 | return res.ok({ 34 | requested: requested, 35 | served: result.rows.length, 36 | is_end: result.rows.length < limit, 37 | list: result.rows 38 | }) 39 | } 40 | 41 | router.get('/:l(\\d+)?/:r(\\d+)?', fc.all(['l', 'r']), listProblem) 42 | router.get('/list/:l(\\d+)?/:r(\\d+)?', fc.all(['l', 'r']), listProblem) 43 | 44 | module.exports = router 45 | -------------------------------------------------------------------------------- /api/admin/admin_problem_spj.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | const {getProblemStructure} = require('../../lib/judge') 4 | const {TEMP_PATH} = require('../../config/basic') 5 | const multer = require('multer') 6 | const fs = require('fs') 7 | const language_ext = require('../../lib/extension') 8 | const {spawn} = require('../../lib/spawn') 9 | 10 | router.post('/:pid', (req, res, next) => { 11 | if (!Number.isInteger(Number(req.params.pid))) 12 | return res.fatal(422) 13 | next() 14 | }) 15 | 16 | const upload = multer({ 17 | storage: multer.diskStorage({ 18 | destination: (req, file, cb) => { 19 | cb(null, TEMP_PATH) 20 | } 21 | }) 22 | }) 23 | 24 | router.post('/:pid', upload.single('file'), async (req, res, next) => { 25 | const pid = req.params.pid 26 | const language = req.body.lang || 1 27 | const ext = language_ext[language] 28 | const spj = getProblemStructure(pid).path.spj 29 | const filename = `${spj}.${ext}` 30 | if (req.file) fs.renameSync(req.file.path, filename) 31 | else { 32 | if (!req.body.data) res.fail(400, 'neither file nor data was supplied') 33 | fs.writeFileSync(filename, req.body.data) 34 | } 35 | 36 | const config = { 37 | "lang": language_ext[ext], 38 | "pid": ''+pid, 39 | } 40 | 41 | fs.writeFileSync(`${spj}.config`, JSON.stringify(config)) 42 | const { code, stdout, stderr } = await spawn('docker', ['exec', '-i', 'judgecore', './compiler', `${spj}.config`]).catch(e => e); 43 | if (code === 0) { 44 | return res.ok() 45 | } 46 | return res.fail(500, JSON.parse(stdout), 'compile error'); 47 | }) 48 | 49 | module.exports = router 50 | -------------------------------------------------------------------------------- /database/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | red=`tput setaf 1` 3 | green=`tput setaf 2` 4 | yellow=`tput setaf 12` 5 | grey=`tput setaf 8` 6 | reset=`tput sgr0` 7 | 8 | read -p "${yellow}New database to host the OnlineJudge [onlinejudge]: ${reset}" database 9 | if [[ -z "$database" ]]; then 10 | database="onlinejudge" 11 | fi 12 | 13 | read -p "${yellow}Add a user for $database [ojadmin]: ${reset}" username 14 | if [[ -z "$username" ]]; then 15 | username="ojadmin" 16 | fi 17 | 18 | read -s -p "${yellow}Set a password for $username []: ${reset}" password 19 | echo 20 | if [[ -z "$password" ]]; then 21 | echo "${green}Password is empty. So you must set $database to 'trust' in pg_hba.conf ${reset}" 22 | fi 23 | 24 | echo "${grey}" 25 | sudo -u postgres createdb -e $database 'Online Judge Database' 26 | sudo -u postgres createuser -esl $username 27 | sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '$password'" 28 | echo "${reset}" 29 | 30 | read -n1 -r -p "${yellow}If the commands above produce no error, press any key to continue.${grey}" key 31 | cat functions.sql structure.sql views.sql triggers.sql initialize.sql | sudo -u postgres psql -d $database -f - 32 | 33 | read -n1 -r -p "${yellow}If the commands above produce no error, press any key to continue.${reset}" key 34 | content="module.exports = { user: '$username', host: 'localhost', database: '$database'" 35 | if [[ -z "$password" ]]; then 36 | echo "${green}Please trust connection from localhost." 37 | else 38 | content="$content ,password: '$password'" 39 | fi 40 | content="$content}" 41 | 42 | echo $content > ../config/postgres.js 43 | 44 | echo "${green}Finished.${reset}" 45 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | const session = require('express-session') 2 | const RedisStore = require('connect-redis')(session) 3 | 4 | const redis = require('redis') 5 | const session_client = redis.createClient() 6 | const {DB_SESSION_STORE} = require('../config/redis') 7 | session_client.select(DB_SESSION_STORE) 8 | 9 | const sessionStore = new RedisStore({ 10 | client: session_client, 11 | db: DB_SESSION_STORE, 12 | logErrors: true 13 | }) 14 | 15 | const logoutAll = () => { 16 | 'use strict' 17 | sessionStore.ids((err, ids) => { 18 | ids.forEach(t => { 19 | sessionStore.destroy(t) 20 | }) 21 | }) 22 | 23 | session_client.keys('session:*', function (err, ret) { 24 | if (!ret) return 25 | ret.forEach((k) => { 26 | console.log(k) 27 | session_client.del(k) 28 | }) 29 | }) 30 | } 31 | 32 | const logout = (uid) => { 33 | 'use strict' 34 | session_client.hgetall(`session:${uid}`, function (err, ret) { 35 | console.log(ret) 36 | if (!ret) return 37 | Object.keys(ret).forEach((k) => { 38 | sessionStore.destroy(ret[k]) 39 | session_client.hdel(`session:${uid}`, k) 40 | }) 41 | }) 42 | } 43 | 44 | const login = (uid, sid) => { 45 | 'use strict' 46 | session_client.hset(`session:${uid}`, Date.now().toString(), sid) 47 | } 48 | 49 | const session_config = require('../config/session') 50 | const sessionParser = session({ 51 | ...session_config, 52 | resave: false, 53 | saveUninitialized: false, 54 | unset: 'destroy', 55 | cookie: {maxAge: 36000000}, 56 | store: sessionStore 57 | }) 58 | 59 | sessionParser.login = login 60 | sessionParser.logout = logout 61 | sessionParser.logoutAll = logoutAll 62 | 63 | module.exports = sessionParser 64 | 65 | -------------------------------------------------------------------------------- /lib/captcha.js: -------------------------------------------------------------------------------- 1 | const {DB_RATE_LIMIT} = require('../config/redis') 2 | const client = require('./redis')(DB_RATE_LIMIT) 3 | const svgCaptcha = require('svg-captcha') 4 | const optional = { 5 | login: true 6 | } 7 | 8 | const captcha_option = { 9 | size: 6, 10 | noise: 5, 11 | color: true, 12 | ignoreChars: '0Oo1Il', 13 | background: '#8899AA' 14 | } 15 | 16 | const middleware = type => async (req, res) => { 17 | 'use strict' 18 | if (optional[type]) { 19 | const ret = await client.incrAsync(`${req.ip}`) 20 | await client.expireAsync(`${req.ip}`, 60) 21 | if (ret && ret < 3) { 22 | req.session.captcha = { 23 | text: undefined, 24 | type: type 25 | } 26 | return res.json({}) 27 | } 28 | } 29 | const captcha = svgCaptcha.create(captcha_option) 30 | req.session.captcha = { 31 | text: captcha.text.toLowerCase(), 32 | type: type 33 | } 34 | res.type('svg') 35 | res.status(200).send(captcha.data) 36 | } 37 | 38 | const captcha = type => 39 | (req, res, next) => { 40 | const captcha_session = req.session.captcha 41 | let captcha_client = req.body.captcha || req.params.captcha || req.query.captcha || '' 42 | captcha_client = captcha_client.toLowerCase() 43 | if (!captcha_session) return res.fail(233, [{name: 'captcha', message: 'required'}]) 44 | if (!captcha_session.type || captcha_session.type === type) { 45 | if (!captcha_client) captcha_client = undefined 46 | if (captcha_client === captcha_session.text) { 47 | delete req.session.captcha 48 | return next() 49 | } 50 | } 51 | delete req.session.captcha 52 | res.fail(234, [{name: 'captcha', message: 'not match'}]) 53 | } 54 | 55 | module.exports = { 56 | middleware, 57 | check: captcha 58 | } 59 | -------------------------------------------------------------------------------- /api/admin/admin_problem_data.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {PROBLEM_DATA_PATH, TEMP_PATH} = require('../../config/basic') 3 | const multer = require('multer') 4 | const AdmZip = require('adm-zip') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | router.post('/:pid', (req, res, next) => { 9 | if (!Number.isInteger(Number(req.params.pid))) 10 | return res.fatal(400) 11 | next() 12 | }) 13 | 14 | const upload = multer({ 15 | storage: multer.diskStorage({ 16 | destination: (req, file, cb) => { 17 | cb(null, TEMP_PATH) 18 | } 19 | }) 20 | }) 21 | 22 | router.post('/:pid', upload.single('file'), (req, res, next) => { 23 | const pid = req.params.pid 24 | if (!req.file) return res.fatal(400, 'no zip file found') 25 | 26 | const data_path = `${PROBLEM_DATA_PATH}/${pid}` 27 | if (fs.existsSync(data_path)) { 28 | fs.readdirSync(data_path).forEach(function (file) { 29 | const curPath = data_path + '/' + file 30 | if (fs.lstatSync(curPath).isDirectory()) { // recurse 31 | // TODO: 32 | } else { 33 | fs.unlinkSync(curPath) 34 | } 35 | }) 36 | } else fs.mkdirSync(data_path) 37 | 38 | const zip = new AdmZip(req.file.path) 39 | const zipEntries = zip.getEntries() // an array of ZipEntry records 40 | 41 | let i = 0 42 | 43 | zipEntries.forEach(function (zipEntry) { 44 | if (path.extname(zipEntry.entryName) === '.in') { 45 | const out = zip.getEntry(`${path.basename(zipEntry.entryName, '.in')}.out`) 46 | if (out) { 47 | i++ 48 | fs.writeFileSync(`${data_path}/${i}.in`, zipEntry.getData()) 49 | fs.writeFileSync(`${data_path}/${i}.out`, out.getData()) 50 | } 51 | } 52 | }) 53 | 54 | res.ok({files: i}) 55 | fs.unlinkSync(req.file.path) 56 | }) 57 | 58 | module.exports = router 59 | -------------------------------------------------------------------------------- /database/triggers.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE TRIGGER after_insert_problem_tag_votes AFTER INSERT ON problem_tag_votes FOR EACH ROW EXECUTE PROCEDURE update_tag_votes(); 4 | CREATE TRIGGER after_update_problem_tag_votes AFTER UPDATE ON problem_tag_votes FOR EACH ROW EXECUTE PROCEDURE update_tag_votes(); 5 | CREATE TRIGGER after_delete_problem_tag_votes AFTER DELETE ON problem_tag_votes FOR EACH ROW EXECUTE PROCEDURE update_tag_votes(); 6 | 7 | CREATE TRIGGER insert_users INSTEAD OF INSERT ON users FOR EACH ROW EXECUTE PROCEDURE insert_new_user(); 8 | CREATE TRIGGER update_users INSTEAD OF UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_existing_user(); 9 | CREATE TRIGGER insert_danmaku INSTEAD OF INSERT ON user_danmaku FOR EACH ROW EXECUTE PROCEDURE upsert_user_danmaku(); 10 | 11 | 12 | CREATE TRIGGER insert_solutions AFTER INSERT ON solutions FOR EACH ROW EXECUTE PROCEDURE update_problem_sol(); 13 | CREATE TRIGGER update_solutions AFTER UPDATE ON solutions FOR EACH ROW EXECUTE PROCEDURE update_problem_sol(); 14 | 15 | CREATE TRIGGER after_insert_post_vote AFTER INSERT ON post_vote FOR EACH ROW EXECUTE PROCEDURE update_post_vote(); 16 | CREATE TRIGGER after_update_post_vote AFTER UPDATE ON post_vote FOR EACH ROW EXECUTE PROCEDURE update_post_vote(); 17 | CREATE TRIGGER after_delete_post_vote AFTER DELETE ON post_vote FOR EACH ROW EXECUTE PROCEDURE update_post_vote(); 18 | 19 | CREATE TRIGGER after_insert_reply_vote AFTER INSERT ON reply_vote FOR EACH ROW EXECUTE PROCEDURE update_reply_vote(); 20 | CREATE TRIGGER after_delete_reply_vote AFTER DELETE ON reply_vote FOR EACH ROW EXECUTE PROCEDURE update_reply_vote(); 21 | 22 | CREATE TRIGGER after_insert_post AFTER INSERT ON post FOR EACH ROW EXECUTE PROCEDURE insert_update_post(); 23 | CREATE TRIGGER after_update_post AFTER UPDATE ON post FOR EACH ROW EXECUTE PROCEDURE insert_update_post(); 24 | 25 | COMMIT; 26 | -------------------------------------------------------------------------------- /api/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {require_perm} = require('../lib/permission') 3 | 4 | const admin = require('./admin/admin') 5 | const avatar = require('./avatar') 6 | const captcha = require('./captcha') 7 | const contest = require('./contest') 8 | const contests = require('./contests') 9 | const danmaku = require('./danmaku') 10 | const message = require('./message') 11 | const judge_new = require('./judge_new') 12 | const post = require('./post') 13 | const posts = require('./posts') 14 | const problem = require('./problem') 15 | const problems = require('./problems') 16 | const test = require('./test') 17 | const user = require('./user/user') 18 | const status = require('./status') 19 | const version = require('./version') 20 | const simple_picture=require('./simple_picture') 21 | const video=require('./video') 22 | 23 | router.use('/admin', admin) 24 | router.use('/avatar', avatar) 25 | router.use('/c', contest) // like '/api/contest/1001', '/api/c/1001' 26 | // router.use('/code', require('./code')) // FIXME: temp workaround for mobile app 27 | router.use('/captcha', captcha) 28 | router.use('/contest', contest) 29 | router.use('/contests', contests) 30 | router.use('/danmaku', danmaku) 31 | router.use('/message', require_perm(), message) 32 | router.use('/judge', require_perm(), judge_new) 33 | router.use('/p', problem) // '/api/p/1001', '/api/problem/1001' 34 | router.use('/post', post) 35 | router.use('/posts', posts) 36 | router.use('/problem', problem) 37 | router.use('/problems', problems) 38 | router.use('/u', user) 39 | router.use('/user', user) 40 | router.use('/status', status) 41 | router.use('/version', version) 42 | router.use('/simple_picture',simple_picture) 43 | router.use('/video',video) 44 | // DEV: test 45 | router.use('/test', test) 46 | 47 | router.use((req, res) => { 48 | res.fatal(501) 49 | }) 50 | 51 | module.exports = router 52 | -------------------------------------------------------------------------------- /lib/permission.js: -------------------------------------------------------------------------------- 1 | const db = require('../database/db') 2 | const require_perm = (permission) => { 3 | return async (req, res, next) => { 4 | if (!req.session.user) return res.fatal(401) 5 | if (!permission) return next() 6 | if (!req.session.perm) { 7 | const result = await db.query('SELECT cal_perm($1) AS perm', [req.session.user]) 8 | if (result.rows.length) req.session.perm = result.rows[0].perm 9 | } 10 | req.session.perm && (req.session.perm[permission] === '1') ? next() : res.fatal(403) 11 | } 12 | } 13 | 14 | const check_perm = async (req, permission) => { 15 | if (!req.session.user) return false 16 | if (!permission) return true 17 | if (!req.session.perm) { 18 | const res = await db.query('SELECT cal_perm($1) AS perm', [req.session.user]) 19 | if (res.rows.length) { 20 | req.session.perm = res.rows[0].perm 21 | return req.session.perm[permission] === '1' 22 | } 23 | return false 24 | } 25 | return req.session.perm[permission] === '1' 26 | } 27 | 28 | module.exports = { 29 | require_perm, 30 | check_perm, 31 | LOGIN: 'LOGIN', 32 | CHANGE_PROFILE: 'CHANGE_PROFILE', 33 | CHANGE_AVATAR: 'CHANGE_AVATAR', 34 | SUBMIT_CODE: 'SUBMIT_CODE', 35 | GET_CODE_SELF: 'GET_CODE_SELF', 36 | VIEW_OUTPUT_SELF: 'VIEW_OUTPUT_SELF', 37 | COMMENT: 'COMMENT', 38 | POST_NEW_POST: 'POST_NEW_POST', 39 | REPLY_POST: 'REPLY_POST', 40 | PUBLIC_EDIT: 'PUBLIC_EDIT', 41 | ADD_PROBLEM: 'ADD_PROBLEM', 42 | EDIT_PROBLEM_SELF: 'EDIT_PROBLEM_SELF', 43 | GET_CODE_OWNED: 'GET_CODE_OWNED', 44 | BYPASS_STATISTIC_OWNED: 'BYPASS_STATISTIC_OWNED', 45 | EDIT_PROBLEM_ALL: 'EDIT_PROBLEM_ALL', 46 | ADD_CONTEST: 'ADD_CONTEST', 47 | EDIT_CONTEST_SELF: 'EDIT_CONTEST_SELF', 48 | EDIT_CONTEST_ALL: 'EDIT_CONTEST_ALL', 49 | REJUDGE_CONTEST_SELF: 'REJUDGE_CONTEST_SELF', 50 | REJUDGE_CONTEST_ALL: 'REJUDGE_CONTEST_ALL', 51 | REJUDGE_ALL: 'REJUDGE_ALL', 52 | BYPASS_STATISTIC_ALL: 'BYPASS_STATISTIC_ALL', 53 | GET_CODE_ALL: 'GET_CODE_ALL', 54 | VIEW_OUTPUT_ALL: 'VIEW_OUTPUT_ALL', 55 | MANAGE_ROLE: 'MANAGE_ROLE', 56 | SUPER_ADMIN: 'SUPER_ADMIN' 57 | } 58 | -------------------------------------------------------------------------------- /api/admin/admin_post.js: -------------------------------------------------------------------------------- 1 | const db = require('../../database/db') 2 | const router = require('express').Router() 3 | const fc = require('../../lib/form-check') 4 | 5 | // 编辑帖子 6 | 7 | router.post('/edit/:post', fc.all(['title', 'content']), async (req, res, next) => { 8 | const post = Number(req.params.post) 9 | if (!Number.isInteger(post)) return next() 10 | 11 | const title = req.fcResult.title 12 | const content = req.fcResult.content 13 | 14 | const reply = await db.query( 15 | 'UPDATE post SET title = CASE WHEN parent_id IS NULL THEN $1 ELSE NULL END, content = $2, last_edit_date = current_timestamp, last_editor_id = $3 WHERE post_id = $4 RETURNING *' 16 | , [title, content, req.session.user, post]) 17 | 18 | if (reply.rowCount) { 19 | res.ok(reply.rows[0]) 20 | } else { 21 | res.fail(404) 22 | } 23 | 24 | }) 25 | 26 | // 删除 / 还原帖子 27 | 28 | router.get('/:type(remove|recover)/:post', async (req, res, next) => { 29 | const post = Number(req.params.post) 30 | if (!Number.isInteger(post)) return next() 31 | 32 | let reply 33 | if (req.params.type === 'remove') 34 | reply = await db.query('UPDATE post SET removed_date = current_timestamp WHERE post_id = $1', [post]) 35 | else 36 | reply = await db.query('UPDATE post SET removed_date = NULL WHERE post_id = $1', [post]) 37 | 38 | if (reply.rowCount) { 39 | res.ok({affected: reply.rowCount}) 40 | } else { 41 | res.fail(404) 42 | } 43 | }) 44 | 45 | // 删除评论 46 | 47 | router.get('/:type(remove|recover)/comment/:comment', async (req, res, next) => { 48 | const comment = Number(req.params.comment) 49 | if (!Number.isInteger(comment)) return next() 50 | 51 | let reply 52 | if (req.params.type === 'remove') 53 | reply = await db.query('UPDATE post_reply SET removed_date = current_timestamp WHERE reply_id = $1', [comment]) 54 | else 55 | reply = await db.query('UPDATE post_reply SET removed_date = NULL WHERE reply_id = $1', [comment]) 56 | 57 | if (reply.rowCount) { 58 | res.ok({affected: reply.rowCount}) 59 | } else { 60 | res.fail(404) 61 | } 62 | }) 63 | 64 | module.exports = router 65 | -------------------------------------------------------------------------------- /api/version.js: -------------------------------------------------------------------------------- 1 | const {FRONT_END_PATH, BACK_END_PATH} = require('../config/basic') 2 | const git = { 3 | front: require('../lib/git')(FRONT_END_PATH), 4 | back: require('../lib/git')(BACK_END_PATH) 5 | } 6 | const {require_perm, SUPER_ADMIN} = require('../lib/permission') 7 | const router = require('express').Router() 8 | 9 | router.get('/', async (req, res) => { 10 | const front_log = await git.front.log('-n 1 --no-decorate') 11 | const back_log = await git.back.log('-n 1 --no-decorate') 12 | res.ok({ 13 | frontend: front_log.stdout, 14 | backend: back_log.stdout 15 | }) 16 | }) 17 | 18 | router.get('/rebuild/:type', require_perm(SUPER_ADMIN), async (req, res) => { 19 | res.writeHead(200, { 20 | 'Content-Type': 'text/plain; charset=utf-8', 21 | 'Transfer-Encoding': 'chunked', 22 | 'X-Content-Type-Options': 'nosniff' 23 | }) 24 | switch (req.params.type) { 25 | case 'frontend': 26 | res.write('reset:\n') 27 | res.write(JSON.stringify(await git.front.resetAll())) 28 | res.write('\npull:\n') 29 | const fres = await git.front.pull() 30 | res.write(JSON.stringify(fres)) 31 | if (fres.stdout === 'Already up to date.\n') 32 | return res.end('\nNothing to be done.\n') 33 | res.write('\nnpm_install:\n') 34 | res.write(JSON.stringify(await git.front.install())) 35 | res.write('\nnpm_build:\n') 36 | res.write(JSON.stringify(await git.front.rebuild())) 37 | res.end('\nfinished.\n') 38 | break 39 | case 'backend': 40 | res.write('reset:\n') 41 | res.write(JSON.stringify(await git.back.resetAll())) 42 | res.write('\npull:\n') 43 | const bres = await git.back.pull() 44 | res.write(JSON.stringify(bres)) 45 | if (bres.stdout === 'Already up to date.\n') 46 | return res.end('\nNothing to be done.\n') 47 | res.write('\nnpm_install:\n') 48 | res.write(JSON.stringify(await git.back.install())) 49 | res.write('\nfinished, will restart.\n') 50 | setTimeout(() => process.exit(-1), 1000) 51 | break 52 | default: 53 | res.end('type unknown\n') 54 | } 55 | }) 56 | 57 | module.exports = router 58 | -------------------------------------------------------------------------------- /lib/judge.js: -------------------------------------------------------------------------------- 1 | const {SOLUTION_PATH, DATA_BASE, PROBLEM_DATA_PATH, PROBLEM_SPJ_PATH, PROBLEM_PATH} = require('../config/basic') 2 | const fs = require('fs') 3 | 4 | const deleteFolderRecursive = function (path) { 5 | if (path.indexOf('temp') < 0) throw 'something wrong?' 6 | if (fs.existsSync(path)) { 7 | fs.readdirSync(path).forEach(function (file, index) { 8 | const curPath = path + '/' + file 9 | if (fs.lstatSync(curPath).isDirectory()) { // recurse 10 | deleteFolderRecursive(curPath) 11 | } else { 12 | fs.unlinkSync(curPath) 13 | } 14 | }) 15 | fs.rmdirSync(path) 16 | } 17 | } 18 | 19 | const getSolutionStructure = function (sid) { 20 | const PATH_SOLUTION = `${SOLUTION_PATH}/${sid}` 21 | if (!fs.existsSync(PATH_SOLUTION)) 22 | fs.mkdirSync(PATH_SOLUTION) 23 | const PATH_TEMP = `${PATH_SOLUTION}/temp` 24 | const PATH_EXEC_OUT = `${PATH_SOLUTION}/execout` 25 | 26 | const FILE_RESULT = `${PATH_TEMP}/result` 27 | const FILE_TIME = `${PATH_TEMP}/time` 28 | const FILE_MEMORY = `${PATH_TEMP}/memory` 29 | const FILE_DETAIL = `${PATH_TEMP}/detail` 30 | const FILE_COMPILE_INFO = `${PATH_SOLUTION}/main.cmpinfo` 31 | const FILE_CODE_BASE = `${PATH_SOLUTION}/main.` 32 | return { 33 | path: { 34 | solution: PATH_SOLUTION, 35 | temp: PATH_TEMP, 36 | exec_out: PATH_EXEC_OUT 37 | }, 38 | file: { 39 | result: FILE_RESULT, 40 | time: FILE_TIME, 41 | memory: FILE_MEMORY, 42 | detail: FILE_DETAIL, 43 | compile_info: FILE_COMPILE_INFO, 44 | code_base: FILE_CODE_BASE 45 | } 46 | } 47 | } 48 | 49 | const getProblemStructure = function (pid) { 50 | const PATH_DATA = `${PROBLEM_DATA_PATH}/${pid}` 51 | const PATH_SPJ = `${PROBLEM_SPJ_PATH}/${pid}` 52 | const FILE_MD = `${PROBLEM_PATH}/${pid}.md` 53 | return { 54 | path: { 55 | data: PATH_DATA, 56 | spj: PATH_SPJ 57 | }, 58 | file: { 59 | md: FILE_MD 60 | } 61 | } 62 | } 63 | 64 | module.exports = { 65 | getSolutionStructure, 66 | getProblemStructure, 67 | async unlinkTempFolder (sid) { 68 | fs.unlinkSync(getSolutionStructure(sid).path.temp + "/main") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /database/db.js: -------------------------------------------------------------------------------- 1 | const pool = require('./init') 2 | const session = require('../lib/session') 3 | const db = {} 4 | 5 | db.pool = () => pool 6 | 7 | db.query = (text, params) => { 8 | const start = Date.now() 9 | return new Promise((resolve, reject) => { 10 | 'use strict' 11 | pool.query(text, params, (err, res) => { 12 | const end = Date.now() - start 13 | if (err) { 14 | console.log(`Database query [${text}, ${params}] failed in ${end} ms`) 15 | console.log(err) 16 | reject(err) 17 | } else { 18 | console.log(`Database query [${text}, ${params}] finished in ${end} ms`) 19 | resolve(res) 20 | } 21 | }) 22 | }) 23 | } 24 | 25 | db.splitEmail = email => { 26 | 'use strict' 27 | const arr = email.toLowerCase().split('@') 28 | const a = arr.pop() 29 | const b = arr.join('@') 30 | return [b, a] 31 | } 32 | 33 | db.postLogin = (info, req, res) => { 34 | 'use strict' 35 | req.session.user = info.user_id 36 | req.session.permission = info.perm 37 | req.session.nickname = info.nickname 38 | req.session.save() 39 | delete info.password 40 | session.login(info.user_id, req.session.id) 41 | res.ok(info) 42 | } 43 | 44 | db.checkName = async (name) => { 45 | 'use strict' 46 | let result = await db.query('SELECT nick_id FROM user_nick WHERE lower(nickname) = lower($1) LIMIT 1', [name]) 47 | if (result.rows.length > 0) { 48 | result = await db.query('SELECT user_id FROM user_info WHERE nick_id = $1 LIMIT 1', [result.rows[0].nick_id]) 49 | if (result.rows.length > 0) return [{name: 'nickname', message: 'is being used'}] 50 | return [{name: 'nickname', message: 'has been used'}] 51 | } 52 | return false 53 | } 54 | 55 | db.checkEmail = async (email) => { 56 | 'use strict' 57 | const result = await db.query('SELECT nickname FROM users WHERE email = lower($1) LIMIT 1', [email]) 58 | if (result.rows.length > 0) { 59 | const nickname = result.rows[0].nickname 60 | return [{name: 'email', message: `taken by someone like ${nickname}`}] 61 | } 62 | return false 63 | } 64 | 65 | db.joinQueryArr = (key) => { 66 | 'use strict' 67 | const arr = [] 68 | arr.push(key.join(', ')) 69 | arr.push(arr.map(function (k, i) {return '$' + (i + 1)}).join(',')) 70 | return arr 71 | } 72 | 73 | module.exports = db 74 | -------------------------------------------------------------------------------- /init/init.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | app.set('view engine', 'ejs') 7 | app.use(express.static(path.resolve(__dirname, 'public'))) 8 | app.use(express.urlencoded({extended: true})) 9 | 10 | const evalInContext = (scr) => { 11 | const mask = {} 12 | for (p in this) { 13 | // noinspection JSUnfilteredForInLoop 14 | mask[p] = undefined 15 | } 16 | mask.module = {}; 17 | (new Function('with(this) { ' + scr + '}')).call(mask) 18 | return mask 19 | } 20 | 21 | const files = { 22 | session: path.resolve(__dirname, '../config/session.js'), 23 | db: path.resolve(__dirname, '../config/postgres.js'), 24 | path: path.resolve(__dirname, '../config/path.js'), 25 | mail: path.resolve(__dirname, '../config/mail.js'), 26 | rsa: path.resolve(__dirname, '../config/rsa.js') 27 | } 28 | 29 | app.get('/init', (req, res) => { 30 | 31 | Object.keys(files).forEach((k) => { 32 | if (fs.existsSync(files[k])) 33 | res.locals[k] = fs.readFileSync(files[k]) 34 | res.locals[`${k}_template`] = fs.readFileSync(path.resolve(__dirname, `${k}_template.js`)) 35 | }) 36 | 37 | res.render(path.resolve(__dirname, './init.ejs'), {files: files}) 38 | }) 39 | 40 | 41 | app.post('/init', (req, res) => { 42 | const type = req.body.type 43 | const keys = Object.keys(files) 44 | if (!keys.includes(type)) 45 | return res.json({code: 400, message: `requested type '${type}' is not in the list`, keys: keys}) 46 | 47 | const value = req.body.value.trim() 48 | let ret 49 | try { 50 | ret = evalInContext(value) 51 | } catch (e) { 52 | return res.json({code: 400, message: 'error when eval the file', error: e.stack || e}) 53 | } 54 | if (!ret.module || !ret.module.exports) 55 | return res.json({code: 400, message: 'must contain module.exports'}) 56 | try { 57 | fs.writeFileSync(files[type], value) 58 | } catch (e) { 59 | return res.json({code: 500, message: 'error when write the file', error: e.stack || e}) 60 | } 61 | res.send({code: 0}) 62 | }) 63 | 64 | app.get('/restart', (req, res) => { 65 | res.json({code: 0}) 66 | process.exit(-1) 67 | }) 68 | 69 | app.use((req, res) => { 70 | res.redirect(`/init`) 71 | }) 72 | 73 | app.use((err, req, res) => { 74 | res.json(err) 75 | }) 76 | 77 | module.exports = app 78 | -------------------------------------------------------------------------------- /api/admin/admin_role.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const path = require('path') 3 | const db = require('../../database/db') 4 | 5 | router.get('/', async (req, res) => { 6 | const ret = await db.query('SELECT * FROM user_role') 7 | res.ok(ret.rows) 8 | }) 9 | 10 | router.get('/:rid', async (req, res) => { 11 | const role_id = req.params.rid 12 | try { 13 | const ret = await db.query('SELECT * FROM user_info WHERE user_role @> $1', [`{${role_id}}`]) 14 | if (ret.rows.length) res.ok(ret.rows) 15 | else res.fail(1, {rid: 'no such role'}) 16 | } 17 | catch (err) { 18 | res.fail(520, err) 19 | } 20 | }) 21 | 22 | router.get('/:uid/:type/:rid', async (req, res) => { 23 | const user_id = Number(req.params.uid) 24 | const role_id = Number(req.params.rid) 25 | const type = req.params.type 26 | 27 | let ret = await db.query(`SELECT user_role FROM user_info WHERE user_id = $1 LIMIT 1`, [user_id]) 28 | 29 | if (ret.rows.length === 0) return res.fail(1, {uid: 'not exist'}) 30 | let roles = ret.rows[0].user_role 31 | roles = roles.filter(i => i !== role_id) 32 | 33 | if (type === 'add') roles.push(role_id) 34 | 35 | try { 36 | ret = await db.query(`UPDATE user_info SET user_role = $1 WHERE user_id = $2 RETURNING *`, [`{${roles.join(', ')}}`, user_id]) 37 | } catch (err) { 38 | return res.fail(520, err) 39 | } 40 | 41 | res.ok(ret.rows[0]) 42 | }) 43 | 44 | // TODO: return permission and add role 45 | // router.post('/add_role', check_perm(MANAGE_ROLE), async (req, res) => { 46 | // const keys = ['role_title', 'role_description', 'perm', 'negative'] 47 | // const values = [req.body.title, req.body.description, req.body.perm, req.body.negative] 48 | // if (req.body.perm[20] === '1') { 49 | // res.fail(1, 'can\'t create super admin') 50 | // } 51 | // else { 52 | // const form = {} 53 | // let result = check(keys, values, {}, form) 54 | // if (result) return res.fail(400, result) 55 | // try { 56 | // const query = 'INSERT INTO user_role (title, description,perm, negative) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING RETURNING role_id' 57 | // result = await db.query(query, [...values]) 58 | // } 59 | // catch (err) { 60 | // res.fail(520, err) 61 | // throw err 62 | // } 63 | // if (result.rows.length === 0) 64 | // res.fail(1, {title: 'conflict'}) 65 | // else res.ok(result.rows) 66 | // } 67 | // }) 68 | 69 | module.exports = router 70 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const {PUBLIC_PATH, DIST_PATH} = require('./config/basic') 2 | const express = require('express') 3 | const app = express() 4 | const api = require('./api/api') 5 | const prototype = require('./lib/prototype') 6 | 7 | const logger = require('morgan') 8 | 9 | const path = require('path') 10 | 11 | app.set('trust proxy', '222.30.45.0/24, 127.0.0.1'); 12 | 13 | // Disable Header 'X-Powered-By' added by express. 14 | app.disable('x-powered-by') 15 | 16 | // DEV: request logger 17 | app.use(logger(':date[iso] :remote-addr :method :url :status')) 18 | 19 | // DEV: PPT JSON 20 | app.set('json spaces', 4) 21 | 22 | app.use(prototype.setResponsePrototype) 23 | 24 | const exts = { 25 | 'rar': 'no', 26 | 'zip': 'no', 27 | '7z': 'no', 28 | 'tar': 'no', 29 | 'bz': 'no', 30 | 'gz': 'no' 31 | } 32 | 33 | app.use('*', function (req, res, next) { 34 | const ext = path.extname(req.originalUrl) 35 | if (ext != '') { 36 | if (exts[ext]) { 37 | res.sendStatus(404) 38 | } 39 | } 40 | next() 41 | }) 42 | 43 | app.use('/public', express.static(PUBLIC_PATH, {fallthrough: false})) 44 | 45 | app.use(express.json({limit: '233kb'})) 46 | 47 | // This defaults to 100kb 48 | app.use(express.urlencoded({extended: true})) 49 | 50 | app.use(require('./lib/apikey')) 51 | 52 | // Dispatch to router 53 | app.use('/api', api) 54 | 55 | app.use('/', express.static(DIST_PATH, {fallthrough: true, setHeaders: function (res, path, stat) { 56 | res.set('X-Frame-Options', 'DENY') 57 | }})) 58 | 59 | app.get(/^.+$/, (req, res) => { 60 | res.set({ 61 | // 'Content-Security-Policy': "default-src 'self' ; script-src 'self' 'unsafe-inline' 'unsafe-eval' ; style-src 'self' 'unsafe-inline' ; font-src 'self' data: ; img-src 'self' data: ; connect-src 'self' ws://222.30.51.68;", 62 | 'Content-Security-Policy': "default-src 'self' ; script-src 'self' 'unsafe-inline' 'unsafe-eval' ; style-src 'self' 'unsafe-inline' ; font-src 'self' data: https: ; img-src 'self' data: ; connect-src 'self' wss://acm.nankai.edu.cn/ https://www.tuling123.com/openapi/api ws://acm.nankai.edu.cn/;", 63 | 'Referrer-Policy': 'same-origin', 64 | 'X-Frame-Options': 'DENY' 65 | }) 66 | res.sendFile(`${DIST_PATH}/index.html`, {acceptRanges: false}) 67 | }) 68 | 69 | app.use((req, res) => { 70 | res.fatal(404) 71 | }) 72 | 73 | app.use((err, req, res, next) => { 74 | // DEV: remove 75 | if (!res.headersSent) 76 | res.fatal(err.status || 500, err.stack || err) 77 | }) 78 | 79 | module.exports = app 80 | -------------------------------------------------------------------------------- /api/admin/admin_report.js: -------------------------------------------------------------------------------- 1 | const db = require('../../database/db') 2 | const router = require('express').Router() 3 | const fc = require('../../lib/form-check') 4 | const {sendMessage} = require('../../lib/message') 5 | 6 | // 列出未处理的举报 7 | 8 | router.get('/', async (req, res) => { 9 | const ret = await db.query('SELECT * FROM reports WHERE handler IS NULL ORDER BY "when" DESC LIMIT 150') 10 | 11 | res.ok(ret.rows) 12 | }) 13 | 14 | router.get('/:from(\\d+)/:limit(\\d+)?', async (req, res) => { 15 | 'use strict' 16 | let from = Number(req.params.from) 17 | let limit = Number(req.params.limit || 0) 18 | if (limit > 150) limit = 150 19 | 20 | if (from < 0 || limit < 0) return next() 21 | 22 | const queryString = `SELECT * FROM reports WHERE handler IS NULL ORDER BY "when" DESC LIMIT $1 OFFSET $2` 23 | const result = await db.query(queryString, [limit, from]) 24 | if (result.rows.length > 0) 25 | return res.ok(result.rows) 26 | return res.sendStatus(204) 27 | }) 28 | 29 | // 列出全部举报 30 | 31 | router.get('/all', async (req, res) => { 32 | const ret = await db.query('SELECT * FROM reports ORDER BY "when" DESC LIMIT 150') 33 | 34 | res.ok(ret.rows) 35 | }) 36 | 37 | router.get('/all/:from(\\d+)/:limit(\\d+)?', async (req, res) => { 38 | 'use strict' 39 | let from = Number(req.params.from) 40 | let limit = Number(req.params.limit || 0) 41 | if (limit > 150) limit = 150 42 | 43 | if (from < 0 || limit < 0) return next() 44 | 45 | const queryString = `SELECT * FROM reports ORDER BY "when" DESC LIMIT $1 OFFSET $2` 46 | const result = await db.query(queryString, [limit, from]) 47 | if (result.rows.length > 0) 48 | return res.ok(result.rows) 49 | return res.sendStatus(204) 50 | }) 51 | 52 | // 处理举报,// TODO: 降低用户权限,扣分 53 | // 拒绝举报,// TODO: 识别恶意举报,扣分 54 | 55 | // TODO: 暂时可以多次处理同一举报 56 | // TODO: 根据 type 删除信息等... 57 | 58 | router.get('/:type(approve|decline)/:rid/', fc.all(['rid']), async (req, res) => { 59 | const rid = req.fcResult.rid 60 | const ret = await db.query('UPDATE reports SET result = $1, handler = $2 WHERE report_id = $3 RETURNING *', [req.params.type === 'approve', req.session.user, rid]) 61 | 62 | if (!ret.rowCount) return res.fail(404) 63 | 64 | const row = ret.rows[0] 65 | 66 | if (row.result) { 67 | sendMessage(row.reporter, '举报结果通知', `您在 ${row.when} 对用户 ${row.reportee} 举报成功,感谢~`) 68 | } else { 69 | sendMessage(row.reporter, '举报结果通知', `您在 ${row.when} 对用户 ${row.reportee} 的举报失败了QwQ,如有异议,请私信管理员`) 70 | } 71 | res.ok() 72 | 73 | }) 74 | 75 | module.exports = router 76 | -------------------------------------------------------------------------------- /lib/mail.js: -------------------------------------------------------------------------------- 1 | // noinspection SpellCheckingInspection 2 | const nodemailer = require('nodemailer') 3 | const redis = require('redis') 4 | const validator = require('validator') 5 | const db = require('../database/db') 6 | const client = redis.createClient() 7 | const {DB_EMAIL_VERIFY} = require('../config/redis') 8 | const md5 = require('../lib/md5') 9 | const {BASE_URL} = require('../config/path') 10 | const mailTemplate = require('../config/mailTemplate') 11 | client.select(DB_EMAIL_VERIFY) 12 | 13 | // DEV: Send Email 14 | const transporter = nodemailer.createTransport(require('../config/mail').transporter) 15 | 16 | const mailOptions = require('../config/mail').regMailOptions 17 | 18 | const convertToHash = (email) => { 19 | 'use strict' 20 | return md5(Buffer.from(email).toString('base64') + new Date) 21 | } 22 | 23 | const register = (to, code, link, callback) => { 24 | 'use strict' 25 | to = to.toLowerCase() 26 | if (!validator.isEmail(to)) callback({success: false, error: 'not valid email'}) 27 | const email_suffix = db.splitEmail(to)[1] 28 | return client.set(email_suffix, 1, 'NX', 'EX', 30, (err, rep) => { 29 | if (err) return callback({success: false, error: err}) 30 | if (!rep) return callback({success: false, error: 'sorry, we can only send to that server once every 30 second'}) 31 | 32 | client.get(`banned:${to}`, (err, res) => { 33 | if (res) return callback({success: false, error: 'this email has been banned by user.'}) 34 | const hash = convertToHash(to) 35 | client.set(hash, to, 'NX', 'EX', 172000) 36 | const arg = { 37 | text: `Your Code is: ${code} .\n Or you can use this link: ${link}`, 38 | html: mailTemplate(code, link, BASE_URL, hash, to), 39 | to: to 40 | } 41 | transporter.sendMail(Object.assign(mailOptions, arg), (error, info) => { 42 | if (error) return callback({success: false, error: error}) 43 | return callback({success: true, info: info}) 44 | }) 45 | }) 46 | }) 47 | } 48 | 49 | const banEmail = (hash, email, unban, cb) => { 50 | 'use strict' 51 | email = email.toLowerCase() 52 | if (unban) { 53 | client.del(`banned:${email}`, 1) 54 | cb({ success: true }) 55 | } 56 | else client.get(hash, (err, res) => { 57 | if (res === email) { 58 | client.set(`banned:${email}`, 1) 59 | cb({ success: true }) 60 | } else { 61 | console.log(email, res) 62 | cb({ success: false, error: 'cannot find the key, the TTL is 2 days, sorry.' }) 63 | } 64 | }) 65 | } 66 | 67 | module.exports = { 68 | sendVerificationMail: register, 69 | banEmail: banEmail 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '34 17 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /database/views.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE OR REPLACE VIEW user_perm AS SELECT user_id, cal_perm(user_id) as perm FROM user_info; 4 | 5 | CREATE OR REPLACE VIEW users AS 6 | SELECT user_info.user_id, CASE WHEN removed THEN ''::character varying(20) ELSE nickname END as nickname, gender, concat(CASE WHEN removed THEN '' ELSE email END, '@', email_suffix.email_suffix) as email, last_login, submit_ac, submit_all, ipaddr, user_info.user_role as role, words, qq, phone, real_name, school, current_badge, removed, user_info."password" as "password", credits, cal_perm(user_info.user_id) as perm, null as old_password FROM user_info 7 | INNER JOIN email_suffix ON email_suffix.suffix_id = user_info.email_suffix_id 8 | INNER JOIN ipaddr ON ipaddr.ipaddr_id = user_info.user_ip 9 | INNER JOIN user_nick ON user_nick.nick_id = user_info.nick_id; 10 | 11 | CREATE OR REPLACE VIEW user_danmaku AS 12 | SELECT message, _danmaku.user_id, CASE WHEN _danmaku.user_id IS NOT NULL THEN nickname ELSE host(ipaddr.ipaddr) END AS nickname, ipaddr.ipaddr, "when" FROM _danmaku 13 | LEFT OUTER JOIN users ON _danmaku.user_id = users.user_id 14 | INNER JOIN ipaddr ON _danmaku.ipaddr_id = ipaddr.ipaddr_id; 15 | 16 | CREATE OR REPLACE VIEW user_solutions AS 17 | SELECT solutions.*, solution_status.msg_short, solution_status.msg_en, solution_status.msg_cn, users.nickname 18 | FROM solutions 19 | INNER JOIN solution_status ON solutions.status_id = solution_status.status_id 20 | INNER JOIN users ON solutions.user_id = users.user_id; 21 | 22 | -- TODO: add new table to map language_id with language? 23 | 24 | CREATE OR REPLACE VIEW user_contests AS 25 | SELECT contests.contest_id, contests.title, contests.description, contests.during, contests.perm, contests.private, 26 | COALESCE(a.problems, '{}'::json ARRAY) AS problems 27 | FROM contests 28 | LEFT OUTER JOIN ( 29 | SELECT contest_id, array_agg( 30 | json_build_object('pid', problem_id, 'ac', submit_ac, 'all', submit_all) 31 | ) 32 | as problems FROM contest_problems GROUP BY contest_problems.contest_id 33 | ) a ON a.contest_id = contests.contest_id; 34 | 35 | CREATE OR REPLACE VIEW posts AS 36 | SELECT post.*, row_number() OVER () as n, comments FROM post LEFT OUTER JOIN ( 37 | 38 | WITH t AS ( 39 | SELECT row_number() OVER (PARTITION BY reply_to) as n, reply_id, reply_to, user_id, score, since,nickname, content 40 | FROM post_reply WHERE removed_date IS NULL 41 | ) SELECT t.reply_to as post_id, json_agg(t) as comments FROM t GROUP BY reply_to 42 | 43 | ) a ON a.post_id = post.post_id; 44 | 45 | COMMIT; 46 | -------------------------------------------------------------------------------- /bin/init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const http = require('http') 6 | 7 | const port = 3000 8 | 9 | let app = undefined 10 | let httpServer = http.createServer() 11 | 12 | new Promise(async (resolve, reject) => { 13 | const {REASON_PATH_CONFIG, REASON_DATABASE_CONFIG, REASON_INACCESSIBLE_PATH, REASON_DATABASE_CONNECT, REASON_REDIS_ERROR, REASON_RSA_CONFIG, REASON_RSA_Decrypt} = require('../init/reason') 14 | if (!fs.existsSync(path.resolve(__dirname, '../config/postgres.js'))) 15 | return reject({reason: REASON_DATABASE_CONFIG}) 16 | try { 17 | const db = require('../database/init') 18 | const ret = await db.query('SELECT NOW() AS now') 19 | if (!ret.rows[0].now) return reject({ 20 | reason: REASON_DATABASE_CONNECT, 21 | details: 'unexpected response from select now()' 22 | }) 23 | } catch (e) { 24 | return reject({reason: REASON_DATABASE_CONNECT, details: e.stack || e}) 25 | } 26 | if (!fs.existsSync(path.resolve(__dirname, '../config/path.js'))) 27 | return reject({reason: REASON_PATH_CONFIG}) 28 | try { 29 | const basic = require('../config/basic') 30 | Object.keys(basic).forEach((k) => { 31 | if (!basic.hasOwnProperty(k)) return 32 | if (!fs.existsSync(basic[k])) 33 | fs.mkdirSync(basic[k]) 34 | }) 35 | } catch (e) { 36 | return reject({reason: REASON_INACCESSIBLE_PATH, details: e.stack || e}) 37 | } 38 | 39 | if (!fs.existsSync(path.resolve(__dirname, '../config/rsa.js'))) 40 | return reject({reason: REASON_RSA_CONFIG}) 41 | 42 | const {encrypt, decrypt} = require('../lib/rsa') 43 | if (decrypt(encrypt('hello')) !== 'hello') { 44 | return reject({ 45 | reason: REASON_RSA_Decrypt, 46 | details: `message: hello, encrypt result: ${encrypt('hello')}, descrypt result: ${decrypt(encrypt('hello'))}` 47 | }) 48 | } 49 | 50 | const redis = require('redis') 51 | const client = redis.createClient() 52 | client.on('ready', () => resolve()) 53 | client.on('error', () => reject({reason: REASON_REDIS_ERROR})) 54 | }).then(() => { 55 | require('../lib/chat')(httpServer) 56 | app = require('../app') 57 | }).catch((err) => { 58 | if (process.env.node !== 'development') { 59 | throw err; 60 | } 61 | err.reason = err.reason || err 62 | err.details = err.details || err.stack || err.reason 63 | console.error(`failed to start normal server, reason: ${err.reason},\n details: `, err.details) 64 | app = require('../init/init') 65 | app.locals.err = err 66 | }).then(() => { 67 | app.set('port', port) 68 | httpServer.on('request', app) 69 | httpServer.listen(port) 70 | httpServer.on('listening', () => console.log('Listening on ' + port)) 71 | }).catch((err) => { 72 | console.error('An error occurred when starting the server...') 73 | console.error(err) 74 | process.exit(-1) 75 | }) 76 | -------------------------------------------------------------------------------- /api/user/user.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | const login = require('./user_login') 4 | const register = require('./user_register') 5 | const user_check = require('./user_check') 6 | const update = require('./user_update') 7 | const api_key = require('./user_api') 8 | const problems = require('./user_problems') 9 | const contest = require('./user_contest') 10 | const db = require('../../database/db') 11 | const fc = require('../../lib/form-check') 12 | const {require_perm} = require('../../lib/permission') 13 | 14 | const queryUserInfo = async function (uid) { 15 | const result = await db.query('SELECT * FROM users WHERE user_id = $1', [uid]) 16 | const ac = await db.query('with t as (' + 17 | ' select distinct problem_id, status_id from solutions where user_id = $1' + 18 | ') select ARRAY((select distinct problem_id from t where status_id = 107)) as ac, ARRAY((select distinct problem_id from t)) as all', [uid]) 19 | 20 | //oi check 21 | let c_ret = await db.query('SELECT * FROM contest_problems LEFT JOIN contests ON contest_problems.contest_id = contests.contest_id WHERE CURRENT_TIMESTAMP < upper(contests.during) AND contests.rule=\'oi\'') 22 | let c_dic = {} 23 | c_ret.rows.forEach(function(c_p, index){ 24 | c_dic[c_p["problem_id"]] = "OI" 25 | }) 26 | let ac_list = [] 27 | ac.rows[0]["ac"].forEach(function(solution, index){ 28 | if(typeof(c_dic[solution]) == "undefined") ac_list.push(solution) 29 | }) 30 | ac.rows[0]["ac"] = ac_list 31 | 32 | //acm check 33 | c_ret = await db.query('SELECT * FROM secret_time WHERE CURRENT_TIMESTAMP < UPPER(during) AND CURRENT_TIMESTAMP > lower(during)') 34 | if(c_ret.rows.length > 0){ 35 | ac.rows[0]["ac"] = [] 36 | } 37 | 38 | let row = result.rows[0] 39 | let {password, old_password, ...ret} = row 40 | return {...ret, ...ac.rows[0]} 41 | } 42 | 43 | router.get('/:uid(\\d+)?', require_perm(), async (req, res) => { 44 | 'use strict' 45 | const user = Number(req.params.uid) || req.session.user 46 | try { 47 | const ret = await queryUserInfo(user) 48 | return res.ok(ret) 49 | } catch (e) { 50 | return res.fail(404) 51 | } 52 | }) 53 | 54 | router.get('/info/:nickname', require_perm(), fc.all(['nickname']), async (req, res) => { 55 | 'use strict' 56 | const user = req.fcResult.nickname 57 | console.log(user) 58 | try { 59 | const user_id_result = await db.query('SELECT user_id from user_nick where lower(nickname) = lower($1)', [user]) 60 | const uid = user_id_result.rows[0].user_id 61 | const ret = await queryUserInfo(uid) 62 | return res.ok(ret) 63 | } catch (e) { 64 | return res.fail(404) 65 | } 66 | }) 67 | 68 | router.use(login) 69 | router.use(register) 70 | router.use(user_check) 71 | router.use(update) 72 | router.use(contest) 73 | router.use(problems) 74 | router.use('/key', require_perm(), api_key) 75 | 76 | module.exports = router 77 | -------------------------------------------------------------------------------- /database/initialize.sql: -------------------------------------------------------------------------------- 1 | 2 | SET custom_settings.hash_prefix = 'not production'; 3 | 4 | ALTER SYSTEM SET custom_settings.hash_prefix = 'not production'; 5 | 6 | SELECT pg_reload_conf(); 7 | 8 | BEGIN; 9 | 10 | ALTER SEQUENCE user_role_role_id_seq RESTART WITH 10; 11 | 12 | INSERT INTO user_role (role_id, title, description, perm, negative) VALUES (1, 'Default Group', 'This is a default user group.', ('1','0','0','1','1','0','0','0','0','0','0','0','0','0','0','0','0','0','0','0','0','0'), 'f'); 13 | 14 | INSERT INTO user_role (role_id, title, description, perm, negative) VALUES (2, 'Muted', 'Muted from sociaty', ('0','1','1','0','0','0','1','1','1','1','1','0','1','0','0','0','0','0','0','0','0','0'), 't'); 15 | 16 | INSERT INTO user_role (role_id, title, description, perm, negative) VALUES (3, 'Banned', 'Read Only',('0','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1'), 't'); 17 | 18 | INSERT INTO user_role (role_id, title, description, perm, negative) VALUES (4, 'Verified Users', 'Real-name Authentication Passed', ('1','1','1','1','1','0','1','1','1','1','0','0','0','0','0','0','0','0','0','0','0','0'), 'f'); 19 | 20 | INSERT INTO user_role (role_id, title, description, perm, negative) VALUES (9, 'Super Admin', 'Super Admin', ('1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1','1'), 'f'); 21 | 22 | ALTER SEQUENCE problems_problem_id_seq RESTART WITH 1001; 23 | 24 | ALTER SEQUENCE contests_contest_id_seq RESTART WITH 1001; 25 | 26 | ALTER SEQUENCE user_nick_nick_id_seq RESTART WITH 1; 27 | 28 | INSERT INTO user_nick(nickname, user_id) VALUES ('SunriseFox', 1); 29 | 30 | ALTER SEQUENCE ipaddr_ipaddr_id_seq RESTART WITH 1; 31 | 32 | INSERT INTO ipaddr(ipaddr) VALUES ('::ffff:127.0.0.1'); 33 | 34 | ALTER SEQUENCE email_suffix_suffix_id_seq RESTART WITH 1; 35 | 36 | INSERT INTO email_suffix(email_suffix) VALUES ('qq.com'); 37 | 38 | ALTER SEQUENCE user_info_user_id_seq RESTART WITH 1; 39 | 40 | INSERT INTO user_info(nick_id, user_ip, password, email, email_suffix_id, user_role) VALUES (1, 1, hash_password('123465'), 'sunrisefox', 1, '{1,9}'::integer ARRAY); 41 | 42 | CREATE SEQUENCE users_defaultname_seq OWNED BY user_info.user_id; 43 | 44 | ALTER SEQUENCE IF EXISTS _danmaku_danmaku_id_seq MAXVALUE 1000 CYCLE; 45 | 46 | INSERT INTO solution_status (status_id, msg_short, msg_cn, msg_en) VALUES 47 | ('100', 'RU', '运行中', 'Running'), 48 | ('101', 'CE', '编译错误', 'Compile Error'), 49 | ('120', 'CI', '正在编译', 'Compiling'), 50 | ('102', 'WA', '答案错误', 'Wrong Answer'), 51 | ('103', 'RE', '运行错误', 'Runtime Error'), 52 | ('104', 'MLE', '内存超限', 'Memory Limit Exceed'), 53 | ('105', 'TLE', '时间超限', 'Time Limit Exceed'), 54 | ('106', 'OLE', '输出超限', 'Output Limit Exceed'), 55 | ('107', 'AC', '答案正确', 'Accepted'), 56 | ('108', 'PE', '格式错误', 'Presentation Error'), 57 | ('109', 'FL', '函数调用不合法', 'Function Limit Exceed'), 58 | ('110', 'DM', '多组数据', 'Detail Mode'), 59 | ('118', 'SE', '未知错误', 'System Error'); 60 | 61 | COMMIT; 62 | -------------------------------------------------------------------------------- /api/admin/admin_problem_add.js: -------------------------------------------------------------------------------- 1 | const {generateFileString} = require('../../lib/problem') 2 | const router = require('express').Router() 3 | const db = require('../../database/db') 4 | const fc = require('../../lib/form-check') 5 | const {PROBLEM_PATH} = require('../../config/basic') 6 | const fs = require('fs') 7 | const path = require('path') 8 | 9 | const TYPE_PARTIAL_CONTENT = 0 10 | 11 | router.post('/', 12 | fc.all(['title', 'cases', 'time_limit', 'memory_limit', 'type', 'special_judge', 'detail_judge', 'tags', 'level']), 13 | async (req, res, next) => { 14 | req.form = req.fcResult 15 | return next() 16 | }) 17 | 18 | router.post('/', 19 | fc.all(['description', 'input', 'output', 'sample_input', 'sample_output', 'hint']), 20 | async (req, res, next) => { 21 | 'use strict' 22 | // noinspection EqualityComparisonWithCoercionJS 23 | if (req.form.type != TYPE_PARTIAL_CONTENT) return next() 24 | const form = Object.assign(req.form, req.fcResult) 25 | 26 | let tags = [] 27 | let pid 28 | try { 29 | if (form.tags && form.tags.length) { 30 | const t = typeof form.tags === 'string' ? JSON.parse(form.tags) : form.tags 31 | const r = await db.query('select insert_tags($1) as tags', [t]) 32 | if (r.rows.length) { 33 | tags = r.rows[0].tags 34 | } 35 | } 36 | 37 | const r = await db.query('insert into problems (title, contest_id, cases, special_judge, detail_judge, level, time_limit, memory_limit)' + 38 | ' values ($1, NULL, $2, $3, $4::boolean, $5::integer, $6::integer, $7::integer) returning problem_id', [form.title, form.cases, form.special_judge, form.detail_judge, form.level, form.time_limit, form.memory_limit]) 39 | if (r.rows.length !== 1) return res.fail(520, 'database returns neither an error nor a successful insert') 40 | 41 | pid = r.rows[0].problem_id 42 | for (let tag in tags) { 43 | if (!tags.hasOwnProperty(tag)) continue 44 | await db.query('INSERT INTO problem_tag_assoc(problem_id, tag_id, official) VALUES ($1, $2, TRUE)', [pid, tags[tag]]) 45 | } 46 | 47 | } catch (e) { 48 | console.log(e.stack || e) 49 | return res.fail(520, e.stack || e) 50 | } 51 | 52 | const filename = `${pid}.md` 53 | const content = generateFileString({ 54 | description: form.description, 55 | input: form.input, 56 | output: form.output, 57 | sample_input: `${form.sample_input}`, 58 | sample_output: `${form.sample_output}`, 59 | hint: form.hint 60 | }) 61 | fs.writeFileSync(path.resolve(PROBLEM_PATH, filename), content) 62 | 63 | const json_content = { 64 | problem_id: pid, 65 | time_limit: form.time_limit, 66 | memory_limit: form.memory_limit, 67 | special_judge: form.special_judge, 68 | detail_judge: form.detail_judge 69 | } 70 | fs.writeFileSync(path.resolve(PROBLEM_PATH, `${pid}.json`), json_content) 71 | 72 | res.ok({problem_id: pid, filename: filename, content: content}) 73 | }) 74 | 75 | module.exports = router 76 | -------------------------------------------------------------------------------- /api/admin/admin_problem_update.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../../database/db') 3 | const {PROBLEM_PATH} = require('../../config/basic') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const fc = require('../../lib/form-check') 7 | const {generateFileString} = require('../../lib/problem') 8 | 9 | const TYPE_PARTIAL_CONTENT = 0 10 | 11 | router.post('/:pid', 12 | fc.all(['pid', 'title', 'cases', 'time_limit', 'memory_limit', 'type', 'special_judge', 'detail_judge', 'tags', 'level']), 13 | (req, res, next) => { 14 | req.form = req.fcResult 15 | return next() 16 | }) 17 | 18 | router.post('/:pid', 19 | fc.all(['description', 'input', 'output', 'sample_input', 'sample_output', 'hint']), 20 | async (req, res, next) => { 21 | const form = Object.assign(req.form, req.fcResult) 22 | let tags = [] 23 | let pid = Number(form.pid) 24 | try { 25 | if (req.form.tags) { 26 | const t = typeof form.tags === 'string' ? JSON.parse(form.tags) : form.tags 27 | const r = await db.query('select insert_tags($1) as tags', [t]) 28 | if (r.rows.length) { 29 | tags = r.rows[0].tags 30 | } 31 | await db.query('DELETE FROM problem_tag_assoc WHERE problem_id = $1', [pid]) 32 | } 33 | 34 | const r = await db.query('update problems set title = COALESCE($1, title), cases = COALESCE($2, cases),' + 35 | ' special_judge = COALESCE($3::integer, special_judge), detail_judge = COALESCE($4::boolean, detail_judge),' + 36 | ' level = COALESCE($5::integer, level), time_limit = COALESCE($6::integer, time_limit), memory_limit = COALESCE($7::integer, memory_limit)' + 37 | ' where problem_id = $8 returning problem_id' 38 | , [form.title, form.cases, form.special_judge, form.detail_judge, form.level, form.time_limit, form.memory_limit, pid]) 39 | if (r.rows.length !== 1) return res.fail(520, 'database returns neither an error nor a successful insert') 40 | 41 | for (let tag in tags) { 42 | if (!tags.hasOwnProperty(tag)) continue 43 | await db.query('INSERT INTO problem_tag_assoc(problem_id, tag_id, official) VALUES ($1, $2, TRUE)', [pid, tags[tag]]) 44 | } 45 | 46 | } catch (e) { 47 | return res.fail(520, e.stack || e) 48 | } 49 | 50 | // noinspection EqualityComparisonWithCoercionJS 51 | if (req.form.type != TYPE_PARTIAL_CONTENT) 52 | return res.ok({problem_id: pid}) 53 | 54 | const filename = `${pid}.md` 55 | const content = generateFileString({ 56 | description: form.description, 57 | input: form.input, 58 | output: form.output, 59 | sample_input: `${form.sample_input}`, 60 | sample_output: `${form.sample_output}`, 61 | hint: form.hint 62 | }) 63 | fs.writeFileSync(path.resolve(PROBLEM_PATH, filename), content) 64 | 65 | const json_content = { 66 | problem_id: pid, 67 | time_limit: form.time_limit, 68 | memory_limit: form.memory_limit, 69 | special_judge: form.special_judge, 70 | detail_judge: form.detail_judge 71 | } 72 | fs.writeFileSync(path.resolve(PROBLEM_PATH, `${pid}.json`), json_content) 73 | 74 | res.ok({problem_id: pid, filename: filename, content: content}) 75 | }) 76 | 77 | module.exports = router 78 | -------------------------------------------------------------------------------- /lib/git.js: -------------------------------------------------------------------------------- 1 | const {spawn, exec} = require('child_process') 2 | const path = require('path') 3 | let diff2html = require('diff2html').Diff2Html 4 | 5 | const callback = (resolve) => (error, stdout, stderr) => { 6 | console.log(stdout) 7 | console.error(stderr) 8 | resolve({ 9 | stderr: stderr, 10 | stdout: stdout, 11 | error: error 12 | }) 13 | } 14 | 15 | module.exports = (workingDir) => { 16 | return { 17 | init () { 18 | return new Promise((resolve, reject) => { 19 | const git = exec(`git init`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 20 | }) 21 | }, 22 | 23 | add (file) { 24 | return new Promise((resolve, reject) => { 25 | const git = exec(`git add ${file}`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 26 | }) 27 | }, 28 | 29 | commit (message) { 30 | return new Promise((resolve, reject) => { 31 | const git = exec(`git commit -m "${message || 'No message.'}"`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 32 | }) 33 | }, 34 | 35 | commitFile (file, message) { 36 | return new Promise((resolve, reject) => { 37 | const git = exec(`git add ${file} && git commit -m "${message || 'No message.'}"`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 38 | }) 39 | }, 40 | 41 | commitAll (message) { 42 | return new Promise((resolve, reject) => { 43 | const git = exec(`git add . && git commit -am "${message || 'No message.'}"`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 44 | }) 45 | }, 46 | 47 | diff (file) { 48 | return new Promise((resolve, reject) => { 49 | const git = exec(`git diff HEAD^ HEAD -- ${file || '.'}`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 50 | }) 51 | }, 52 | 53 | install () { 54 | return new Promise((resolve, reject) => { 55 | const npm = exec(`npm install`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 56 | }) 57 | }, 58 | 59 | log (policy) { 60 | return new Promise((resolve, reject) => { 61 | const git = exec(`git log ${policy}`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 62 | }) 63 | }, 64 | 65 | pull (policy) { 66 | return new Promise((resolve, reject) => { 67 | if (policy && policy !== 'theirs' && policy !== 'ours') reject('policy must be "theirs" or "ours"') 68 | const git = exec(`git pull ${policy ? `-X${policy}` : ''}`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 69 | }) 70 | }, 71 | 72 | removeUntracked () { 73 | return new Promise((resolve, reject) => { 74 | const git = exec(`git clean -fd`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 75 | }) 76 | }, 77 | 78 | resetAll () { 79 | return new Promise((resolve, reject) => { 80 | const git = exec(`git reset --hard && git clean -fd`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 81 | }) 82 | }, 83 | 84 | rebuild () { 85 | return new Promise((resolve, reject) => { 86 | const npm = exec(`npm run build`, {cwd: path.resolve(__dirname, workingDir)}, callback(resolve)) 87 | }) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /api/user/user_api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const crypto = require('crypto') 3 | const db = require('../../database/db') 4 | const fc = require('../../lib/form-check') 5 | 6 | router.get('/', async (req, res) => { 7 | const ret = await db.query('SELECT api_name, api_key, enabled, since FROM user_api WHERE user_id = $1', [req.session.user]) 8 | res.ok(ret.rows) 9 | }) 10 | 11 | router.get('/apply/:api_name?', fc.all(['api_name/optional']), async (req, res) => { 12 | let key 13 | let secret 14 | let name = req.fcResult.api_name 15 | 16 | const hash = crypto.createHash('sha256') 17 | 18 | const ret = await db.query('SELECT * FROM user_api WHERE user_id = $1', [req.session.user]) 19 | if (ret.rows.length >= 3) 20 | return res.fail(1, 'You have had too many api keys...') 21 | 22 | new Promise(function (resolve, reject) { 23 | crypto.randomBytes(16, function (err, buffer) { 24 | if (err) return reject(err) 25 | key = buffer.toString('hex') 26 | resolve(key) 27 | }) 28 | }).then(function (key) { 29 | return new Promise(function (resolve, reject) { 30 | crypto.randomBytes(24, function (err, buffer) { 31 | if (err) reject(err) 32 | secret = buffer.toString('hex') 33 | resolve(secret) 34 | }) 35 | }) 36 | }).catch(function (err) { 37 | res.fail(500, err.stack || err) 38 | throw 'magic' 39 | }).then(function (secret) { 40 | hash.update(`${key}${secret}`) 41 | const hashed_key = hash.digest('hex') 42 | return db.query('insert into user_api(user_id, api_key, api_hashed, api_name) values ($1, $2, $3, $4)', [req.session.user, key, hashed_key, name]) 43 | }).then(function () { 44 | res.ok({key, secret}) 45 | }).catch(function (err) { 46 | // TODO: solve magic conflict -> retry promise 47 | if (typeof err === 'string') return 48 | return res.fail(500, err.stack || err) 49 | }) 50 | 51 | }) 52 | 53 | router.get('/:operate/:key', async (req, res, next) => { 54 | const op = req.params.operate 55 | const key = req.params.key 56 | if (key.length !== 32) return res.fail(422) 57 | 58 | let ret 59 | switch (op) { 60 | case 'enable': 61 | ret = await db.query('UPDATE user_api SET enabled = TRUE WHERE api_key = $1 AND user_id = $2 returning enabled', [key, req.session.user]) 62 | break 63 | case 'disable': 64 | ret = await db.query('UPDATE user_api SET enabled = FALSE WHERE api_key = $1 AND user_id = $2 returning enabled', [key, req.session.user]) 65 | break 66 | case 'delete': 67 | case 'remove': 68 | ret = await db.query('DELETE FROM user_api WHERE api_key = $1 AND user_id = $2 returning *', [key, req.session.user]) 69 | break 70 | default: 71 | return next() 72 | } 73 | if (!ret.rows.length) return res.fail(404) 74 | return res.ok(ret.rows[0]) 75 | }) 76 | 77 | router.get('/rename/:key/:api_name', fc.all(['api_name']), async (req, res) => { 78 | const key = req.params.key 79 | if (key.length !== 32) return res.fail(422) 80 | 81 | const name = req.fcResult.api_name 82 | let ret = await db.query('UPDATE user_api SET api_name = $1 WHERE api_key = $2 AND user_id = $3 returning api_name, api_key, enabled, since', [name, key, req.session.user]) 83 | if (!ret.rows.length) return res.fail(404) 84 | return res.ok(ret.rows[0]) 85 | }) 86 | 87 | module.exports = router 88 | -------------------------------------------------------------------------------- /api/problem.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {PROBLEM_PATH} = require('../config/basic') 3 | const path = require('path') 4 | const fs = require('fs') 5 | const db = require('../database/db') 6 | const {splitFileString} = require('../lib/problem') 7 | const {require_perm, check_perm, SUPER_ADMIN} = require('../lib/permission') 8 | 9 | router.get('/:pid', async (req, res) => { 10 | 'use strict' 11 | const pid = req.params.pid 12 | const ret = await db.query('SELECT * FROM problems WHERE problem_id = $1', [pid]) 13 | if (ret.rows.length === 0) return res.fatal(404) 14 | let c_ret = await db.query('SELECT * FROM contest_problems LEFT JOIN contests ON contest_problems.contest_id = contests.contest_id WHERE problem_id = $1 AND CURRENT_TIMESTAMP < lower(contests.during)', [pid]) 15 | if(c_ret.rows.length !== 0){ 16 | if(! (await check_perm(req, SUPER_ADMIN))) return res.fatal(404) 17 | } 18 | //oi check 19 | c_ret = await db.query('SELECT * FROM contest_problems LEFT JOIN contests ON contest_problems.contest_id = contests.contest_id WHERE CURRENT_TIMESTAMP < upper(contests.during) AND contests.rule=\'oi\'') 20 | c_ret.rows.forEach(function(c_p, index){ 21 | if(pid == c_p["problem_id"]) { 22 | ret.rows[0]["ac"] = 0 23 | } 24 | }) 25 | //acm check 26 | c_ret = await db.query('SELECT * FROM secret_time WHERE CURRENT_TIMESTAMP < UPPER(during) AND CURRENT_TIMESTAMP > lower(during)') 27 | if(c_ret.rows.length > 0){ 28 | ret.rows[0]["ac"] = 0 29 | } 30 | const tags = await db.query('SELECT problem_tag_assoc.tag_id as id, official, positive as p, negative as n, tag_name as name FROM problem_tag_assoc INNER JOIN problem_tags ON problem_tags.tag_id = problem_tag_assoc.tag_id WHERE problem_id = $1', [pid]) 31 | ret.rows[0].tags = tags.rows 32 | const readPath = path.resolve(PROBLEM_PATH, `${pid}.md`) 33 | if (fs.existsSync(readPath)) { 34 | const content = splitFileString(fs.readFileSync(readPath).toString()) 35 | res.ok(Object.assign(ret.rows[0], {keys: Object.keys(content), content: content})) 36 | } else { 37 | res.ok( 'data not found') 38 | } 39 | }) 40 | 41 | router.get('/:pid/tag', require_perm(), async (req, res) => { 42 | const pid = Number(req.params.pid) 43 | const user_id = req.session.user 44 | if (!Number.isInteger(pid)) return next() 45 | const dbres = await db.query('SELECT * FROM problem_tag_votes WHERE user_id = $1 AND problem_id = $2', [user_id, pid]) 46 | return res.ok(dbres.rows) 47 | }) 48 | 49 | router.get('/:pid/:type/:tid', require_perm(), async (req, res, next) => { 50 | const pid = Number(req.params.pid) 51 | const tid = Number(req.params.tid) 52 | if (!Number.isInteger(pid) || !Number.isInteger(tid)) 53 | return next() 54 | const type = req.params.type 55 | let ret 56 | try { 57 | switch (type) { 58 | case 'upvote': 59 | ret = await db.query('INSERT INTO problem_tag_votes (user_id, tag_id, problem_id, attitude) VALUES ($1, $2, $3, $4)', [req.session.user, tid, pid, true]) 60 | break 61 | case 'downvote': 62 | ret = await db.query('INSERT INTO problem_tag_votes (user_id, tag_id, problem_id, attitude) VALUES ($1, $2, $3, $4)', [req.session.user, tid, pid, false]) 63 | break 64 | case 'remove': 65 | ret = await db.query('DELETE FROM problem_tag_votes WHERE user_id = $1 and tag_id = $2 and problem_id = $3', [req.session.user, tid, pid]) 66 | break 67 | default: 68 | return next() 69 | } 70 | } catch (e) { 71 | return res.fail(1) 72 | } 73 | res.ok({affected: ret.rowCount}) 74 | }) 75 | 76 | // TODO: admin download data 77 | 78 | module.exports = router 79 | -------------------------------------------------------------------------------- /init/init.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Init Progress... 6 | 8 | 9 | 12 | 13 | 26 | 27 | 28 |
29 |
30 |

An Exception Occurred During Server Startup...

31 |
32 | 36 |
37 | 43 |
44 | <% 45 | var file_keys = Object.keys(locals.files) 46 | for(var i in file_keys) { 47 | %> 48 |
49 |
Modify for <%= files[file_keys[i]] %>
50 |
51 | 52 |
53 | 54 | 56 |
57 |
58 | 59 | 60 | 61 |
62 |
63 |
64 | <% } %> 65 |
66 | 86 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /api/user/user_update.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | const db = require('../../database/db') 4 | const fc = require('../../lib/form-check') 5 | 6 | const multer = require('multer') 7 | const path = require('path') 8 | 9 | const {AVATAR_PATH} = require('../../config/basic') 10 | const {require_perm} = require('../../lib/permission') 11 | const md5 = require('../../lib/md5') 12 | 13 | const {DB_USER} = require('../../config/redis') 14 | const redis = require('../../lib/redis')(DB_USER) 15 | 16 | const sharp = require('sharp') 17 | const fs = require('fs') 18 | 19 | const upload = multer({ 20 | storage: multer.diskStorage({ 21 | destination: (req, file, cb) => { 22 | cb(null, AVATAR_PATH) 23 | }, 24 | filename: (req, file, cb) => { 25 | cb(null, md5(file.fieldname + '-' + Date.now()) + path.extname(file.originalname)) 26 | } 27 | }), 28 | fileSize: '2048000', 29 | files: 1, 30 | fileFilter: (req, file, cb) => { 31 | const mimetype = file.mimetype 32 | if (!mimetype.startsWith('image/')) { 33 | req.fileErr = mimetype 34 | cb(null, false) 35 | } 36 | else cb(null, true) 37 | } 38 | }) 39 | 40 | router.post('/update', 41 | require_perm(), 42 | upload.single('avatar'), 43 | fc.all(['nickname/optional', 'email/optional', 'gender/optional', 'qq/optional', 'phone', 'real_name', 'school', 'password/optional', 'words']), 44 | async (req, res) => { 45 | 'use strict' 46 | 47 | const form = req.fcResult 48 | 49 | const ret = {} 50 | 51 | if (req.file) { 52 | try { 53 | await sharp(req.file.path).resize(512, 512).max().jpeg({quality: 60}).toFile(`${req.file.path}.sharped.jpg`) 54 | } catch (err) { 55 | fs.unlinkSync(req.file.path) 56 | return res.gen422('avatar', 'invalid picture') 57 | } 58 | fs.unlinkSync(req.file.path) 59 | await redis.setAsync(`avatar:${req.session.user}`, `${req.file.filename}.sharped.jpg`) 60 | ret.avatar = 'changed' 61 | } else if (req.fileErr) { 62 | return res.gen422('avatar', 'invalid type: ' + req.fileErr) 63 | } 64 | 65 | try { 66 | const result = await db.query(`UPDATE users SET nickname = $1, email = $2, gender = $3, qq = $4, phone = $5, real_name = $6, school = $7, password = $8, words = $9 WHERE user_id = ${req.session.user} 67 | RETURNING nickname, email, gender, qq, phone, real_name, school, password, words` 68 | , [form.nickname, form.email, form.gender, form.qq, form.phone, form.real_name, form.school, form.password, form.words]) 69 | if (result.rows.length) { 70 | Object.keys(result.rows[0]).forEach((k) => { 71 | if (result.rows[0][k]) ret[k] = 'changed' 72 | }) 73 | 74 | if (ret['nickname'] == 'changed'){ 75 | const nick_exist = await db.query(`SELECT * FROM post WHERE user_id = ${req.session.user}`) 76 | if(nick_exist.rows.length){ 77 | const nick_result = await db.query(`UPDATE post SET nickname = '${form.nickname}' WHERE user_id = ${req.session.user}`) 78 | } 79 | const nick_reply_exist = await db.query(`SELECT * FROM post_reply WHERE user_id = ${req.session.user}`) 80 | if(nick_exist.rows.length){ 81 | const nick_result = await db.query(`UPDATE post SET nickname = '${form.nickname}' WHERE user_id = ${req.session.user}`) 82 | } 83 | } 84 | return res.ok(ret) 85 | } 86 | else return res.fail(500, 'user id not found or no values could be changed') 87 | } catch (err) { 88 | res.fatal(520, err) 89 | throw err 90 | } 91 | }) 92 | 93 | 94 | router.post('/nkpc', require_perm(), fc.all(['real_name', 'student_number', 'gender', 'institute', 'qq', 'phone']), async(req, res) => { 95 | console.log(req.session.user) 96 | const form = req.fcResult 97 | let ret = {} 98 | await db.query(`INSERT INTO users_nkpc(user_id) VALUES(${req.session.user}) ON CONFLICT DO NOTHING`) 99 | try{ 100 | const result = await db.query(`UPDATE users_nkpc SET real_name = $1, student_number = $2, gender = $3, institute = $4, qq = $5, phone = $6 WHERE user_id = ${req.session.user} 101 | RETURNING real_name, student_number, gender, institute, qq, phone`, [form.real_name, form.student_number, form.gender, form.institute, form.qq, form.phone]) 102 | if(result.rows.length){ 103 | Object.keys(result.rows[0]).forEach((k) => { 104 | if (result.rows[0][k]) ret[k] = result.rows[0][k] 105 | }) 106 | return res.ok(ret) 107 | } else return res.fail(500, 'database error') 108 | } catch(err){ 109 | res.fatal(520, err) 110 | throw err 111 | } 112 | }) 113 | module.exports = router 114 | -------------------------------------------------------------------------------- /api/message.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const fc = require('../lib/form-check') 3 | const db = require('../database/db') 4 | const {limit} = require('../lib/rate-limit') 5 | 6 | router.get('/', async (req, res) => { 7 | const uid = req.session.user 8 | 9 | // TODO: 聚合,限制长度 10 | 11 | const ret = await db.query('SELECT message_id, a as "from", b as "to", since FROM messages WHERE (a = $1 AND deleted_a = FALSE) OR (b = $1 AND deleted_b = FALSE) ORDER BY since', [uid]) 12 | 13 | res.ok(ret.rows) 14 | }) 15 | 16 | router.get('/announcement', async (req, res) => { 17 | const uid = req.session.user 18 | 19 | const ret = await db.query('SELECT message_id, title, content, since FROM messages WHERE a IS NULL AND b IS NULL ORDER BY since DESC') 20 | 21 | res.ok(ret.rows) 22 | }) 23 | 24 | router.get('/system', async (req, res) => { 25 | const uid = req.session.user 26 | 27 | const ret = await db.query('SELECT message_id, title, content, since FROM messages WHERE a IS NULL AND b = $1 ORDER BY since DESC', [uid]) 28 | 29 | res.ok(ret.rows) 30 | }) 31 | 32 | router.get('/:uid', async (req, res, next) => { 33 | const uid = req.session.user 34 | const target = Number(req.params.uid) 35 | if (!Number.isInteger(target)) return next() 36 | 37 | if (target === uid) return res.ok([]) 38 | 39 | const ret = await db.query('SELECT message_id, content, since FROM messages WHERE (a = $1 AND b = $2 AND deleted_a = FALSE) OR (b = $1 AND a = $2 AND deleted_b = FALSE) ORDER BY since', [uid, target]) 40 | 41 | res.ok(ret.rows) 42 | }) 43 | 44 | router.post('/:uid', fc.all(['uid', 'message']), limit('sendmsg'), async (req, res) => { 45 | const from = req.session.user 46 | const to = req.fcResult.uid 47 | const message = req.fcResult.message 48 | 49 | try { 50 | const r = await db.query('SELECT * FROM user_blocks WHERE blocker = $1 AND blockee = $2 LIMIT 1', [to, from]) 51 | 52 | if (r.rowCount) return res.ok('but the target seems not like to receive your message') 53 | 54 | const ret = await db.query('INSERT INTO messages(a, b, content) VALUES ($1, $2, $3)', [from, to, message]) 55 | return res.ok() 56 | } catch (e) { 57 | return res.fail(422) 58 | } 59 | }) 60 | 61 | router.get('/delete/:mid', fc.all(['mid']), async (req, res) => { 62 | const mid = req.fcResult.mid 63 | const uid = req.session.user 64 | const ret = await db.query('SELECT * FROM messages WHERE message_id = $1 AND (a = $2 OR b = $2)', [mid, uid]) 65 | if (!ret.rows.length) return res.fail(422) 66 | const row = ret.rows[0] 67 | if (!row.a) 68 | await db.query('DELETE FROM messages WHERE message_id = $1', [mid]) 69 | else if (row.a === uid) 70 | await db.query('UPDATE messages SET deleted_a = TRUE WHERE message_id = $1', [mid]) 71 | else 72 | await db.query('UPDATE messages SET deleted_b = TRUE WHERE message_id = $1', [mid]) 73 | res.ok() 74 | }) 75 | 76 | router.get('/deleteall/system', async (req, res) => { 77 | const uid = req.session.user 78 | await db.query('DELETE FROM messages WHERE a IS NULL AND b = $1', [uid]) 79 | res.ok() 80 | }) 81 | 82 | router.get('/deleteall/:uid', fc.all(['uid']), async (req, res) => { 83 | const uid = req.session.user 84 | const target = req.fcResult.uid 85 | if (uid === target) return res.fail(422) 86 | 87 | await db.query('UPDATE messages SET deleted_a = TRUE WHERE a = $1 AND b = $2', [uid, target]) 88 | await db.query('UPDATE messages SET deleted_b = TRUE WHERE b = $1 AND a = $2', [uid, target]) 89 | res.ok() 90 | }) 91 | 92 | router.get('/report/:mid', fc.all(['mid']), async (req, res) => { 93 | const mid = req.fcResult.mid 94 | const uid = req.session.user 95 | const ret = await db.query('SELECT * FROM messages WHERE message_id = $1 AND a = $2 OR b = $2', [mid, uid]) 96 | if (!ret.rows.length) return res.fail(422) 97 | const reportee = ret.rows[0].a 98 | 99 | try { 100 | const r = await db.query('INSERT INTO reports (reporter, reportee, type, which) VALUES ($1, $2, get_report_type_id($3), $4)', [uid, reportee, 'message', mid]) 101 | } catch (e) { 102 | return res.fail(422, 'you have reported it...') 103 | } 104 | res.ok() 105 | }) 106 | 107 | router.get('/block', async (req, res) => { 108 | const blocker = req.session.user 109 | 110 | const r = await db.query('SELECT blockee, since FROM user_blocks WHERE blocker = $1', [blocker]) 111 | 112 | res.ok(r.rows) 113 | }) 114 | 115 | router.get('/:type(block|unblock)/:uid', fc.all(['uid']), async (req, res) => { 116 | const blocker = req.session.user 117 | const blockee = req.fcResult.uid 118 | 119 | if (blocker === blockee) return res.fail(422) 120 | 121 | const ret = await db.query('SELECT count(*) as n FROM user_blocks WHERE blocker = $1', [blocker]) 122 | if (ret.rows[0].n >= 30) return res.fail(1, 'you can block at most 30 users...') 123 | 124 | if (req.params.type === 'block') { 125 | try { 126 | const r = await db.query('INSERT INTO user_blocks (blocker, blockee) VALUES ($1, $2)', [blocker, blockee]) 127 | } catch (e) { 128 | return res.fail(422, 'you have blocked this user before...') 129 | } 130 | } else { 131 | const r = await db.query('DELETE FROM user_blocks WHERE blocker = $1 AND blockee = $2', [blocker, blockee]) 132 | return res.ok({affected: r.rowCount}) 133 | } 134 | 135 | res.ok() 136 | }) 137 | 138 | 139 | module.exports = router 140 | -------------------------------------------------------------------------------- /api/contest.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | const fs = require('fs') 4 | const fc = require('../lib/form-check') 5 | const {check_perm, SUPER_ADMIN} = require('../lib/permission') 6 | const {CONTEST_PATH} = require('../config/basic') 7 | 8 | router.get('/:cid', fc.all(['cid']), async (req, res) => { 9 | 'use strict' 10 | const cid = req.fcResult.cid 11 | 12 | const result = await db.query( 13 | 'SELECT *, lower(during) AS start, upper(during) AS end FROM contests WHERE contest_id = $1', [cid] 14 | ) 15 | if (result.rows.length === 0) return res.fail(404) 16 | 17 | const basic = result.rows[0] 18 | 19 | const problems = await db.query( 20 | 'SELECT contest_problems.problem_id, submit_ac as ac, submit_all as all, title, special_judge, detail_judge, level' + 21 | ' FROM contest_problems LEFT JOIN problems ON contest_problems.problem_id = problems.problem_id WHERE contest_problems.contest_id = $1 ORDER BY contest_problems.problem_id', [cid] 22 | ) 23 | if(basic.rule == "oi"){ 24 | problems.rows.forEach(p => { 25 | p.ac = 0 26 | }); 27 | } 28 | 29 | let file 30 | try { 31 | file = fs.readFileSync(`${CONTEST_PATH}/${cid}.md`, 'utf8') 32 | } catch (e) { 33 | 34 | } 35 | res.ok({...basic, problems: problems.rows, file}) 36 | }) 37 | 38 | router.get('/:cid(\\d+)/oirank', fc.all(['cid']), async (req, res) => { 39 | 'use strict' 40 | const cid = req.fcResult.cid 41 | let result = await db.query('SELECT * FROM contests WHERE contest_id = $1 AND CURRENT_TIMESTAMP < upper(contests.during)', [cid]) 42 | if(result.rows.length > 0){ 43 | if (await check_perm(req, SUPER_ADMIN)) {} else return res.fail(404) 44 | } 45 | result = await db.query(`SELECT * FROM user_solutions LEFT JOIN user_info ON user_info.user_id = user_solutions.user_id WHERE contest_id = $1 ORDER BY solution_id LIMIT 10086`,[cid]) 46 | let dic = {} 47 | let tot = 0 48 | let ret = [] 49 | result.rows.forEach((r) => { 50 | if(typeof(dic[r["user_id"]]) == "undefined"){ 51 | dic[r["user_id"]] = tot++ 52 | ret.push({"user_id": r["user_id"], "nickname": r["nickname"], "score" : 0, "info" : {}}) 53 | } 54 | let id = dic[r["user_id"]] 55 | if(typeof(ret[id][r["problem_id"]]) != "undefined"){ 56 | ret[id]["score"] -= ret[id][r["problem_id"]] 57 | } 58 | ret[id][r["problem_id"]] = r["score"] 59 | ret[id]["score"] += r["score"] 60 | }) 61 | if(await check_perm(req, SUPER_ADMIN)){ 62 | result = await db.query(`SELECT * FROM users_nkpc`) 63 | result.rows.forEach((r) => { 64 | if(typeof(dic[r["user_id"]]) != "undefined"){ 65 | let id = dic[r["user_id"]] 66 | ret[id]["real_name"] = r["real_name"] 67 | ret[id]["student_number"] = r["student_number"] 68 | ret[id]["gender"] = r["gender"] 69 | ret[id]["institute"] = r["institute"] 70 | ret[id]["qq"] = r["qq"] 71 | ret[id]["phone"] = r["phone"] 72 | } 73 | }) 74 | } 75 | ret.sort( (x, y) => { 76 | if(x["score"] < y["score"]){ 77 | return 1; 78 | } else if(x["score"] > y["score"]){ 79 | return -1; 80 | } else return 0; 81 | }) 82 | return res.ok(ret) 83 | }) 84 | 85 | router.get('/:cid(\\d+)/oiresult', fc.all(['cid']), async (req, res) => { 86 | 'use strict' 87 | const cid = req.fcResult.cid 88 | const isAdmin = await check_perm(req, SUPER_ADMIN); 89 | let result = await db.query('SELECT * FROM contests WHERE contest_id = $1 AND CURRENT_TIMESTAMP < upper(contests.during)', [cid]) 90 | if(result.rows.length > 0){ 91 | if (!isAdmin) return res.fail(404) 92 | } 93 | // 好长啊 QwQ 94 | result = await db.query(`select row_number() over (order by score desc) as rank, j.* ${isAdmin ? `, to_json(c) as info`: ''}, nickname from (select user_id, sum(score) as score, json_object_agg(problem_id, json_build_object('selected', selected, 'tried', tried, 'score', score)) as details from (select t.*, solutions.score from (SELECT user_id, problem_id, max(solution_id) as selected, array_agg(solution_id) as tried FROM solutions WHERE contest_id = $1 and status_id != 101 group by user_id, problem_id order by user_id, problem_id) as t inner join solutions on t.selected = solutions.solution_id) as q group by q.user_id) as j inner join users on users.user_id = j.user_id inner join users_nkpc as c on c.user_id = j.user_id order by score desc`, [cid]) 95 | return res.ok(result.rows) 96 | }) 97 | 98 | router.get('/:cid/user', fc.all(['cid']), async (req, res) => { 99 | 'use strict' 100 | const cid = req.fcResult.cid 101 | const result = await db.query('SELECT * FROM contest_users WHERE contest_id = $1', [cid]) 102 | res.ok(result.rows) 103 | }) 104 | 105 | router.get('/:cid/first_ac_all',fc.all(['cid']),async (req,res)=>{ 106 | 'use strict' 107 | const cid = req.fcResult.cid 108 | const c_ret = await db.query(`SELECT * FROM contests WHERE contest_id = ${cid}`) 109 | if(c_ret.rows[0].rule == "oi"){ 110 | return res.fail(404) 111 | } 112 | const result = await db.query('SELECT * FROM user_solutions WHERE solution_id IN (SELECT min(solution_id) FROM solutions WHERE contest_id = $1 AND status_id = 107 GROUP BY problem_id ORDER BY min(solution_id))', [cid]) 113 | res.ok(result.rows) 114 | }) 115 | 116 | router.get('/:cid/own_submitted', fc.all(['cid']),async (req, res)=>{ 117 | 'use strict' 118 | const cid = req.fcResult.cid 119 | const c_ret = await db.query(`SELECT ARRAY(SELECT problem_id FROM user_solutions WHERE contest_id = $1 AND user_id = $2 AND status_id <> 101 GROUP BY problem_id ORDER BY problem_id) AS array`, [cid, req.session.user]) 120 | if (c_ret.rows.length) { 121 | res.ok(c_ret.rows[0].array) 122 | } else { 123 | return res.fail(404) 124 | } 125 | }) 126 | 127 | module.exports = router 128 | -------------------------------------------------------------------------------- /api/admin/admin_user.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const {require_perm, MANAGE_ROLE, SUPER_ADMIN} = require('../../lib/permission') 3 | const db = require('../../database/db') 4 | const pool = require('../../database/init') 5 | const fc = require('../../lib/form-check') 6 | const session = require('../../lib/session') 7 | const {genRawStr, getPass} = require('../../lib/rsa') 8 | 9 | // TODO: test 10 | router.post('/add', fc.all(['nickname', 'password', 'email', 'words', 'count']), async (req, res) => { 11 | 'use strict' 12 | const form = req.fcResult 13 | 14 | if (form.count === 1) { 15 | const password = form.password || Math.random().toString(36).substring(2) 16 | try { 17 | const ret = await db.query(`INSERT INTO users (nickname, password, email, gender, role, school, words, ipaddr) 18 | SELECT COALESCE($1, CONCAT('ojuser_', t.val)), $2, COALESCE($3, CONCAT('ojuser_',t.val,'@dummy.nankai.edu.cn')), $4, $5, $6, $7, '::ffff:127.0.0.1' 19 | FROM (SELECT nextval('users_defaultname_seq') as val) AS t RETURNING nickname` 20 | , [form.nickname, password, form.email, 3, `{1, 4}`, 'NKU', form.words]) 21 | res.ok({nickname: ret.rows[0].nickname, password: password}) 22 | } catch (err) { 23 | res.fail(520, err) 24 | throw err 25 | } 26 | } else { 27 | const retArr = [] 28 | for (let i = 0; i < form.count; i++) { 29 | const password = form.password || Math.random().toString(36).substring(2) 30 | try { 31 | const ret = await db.query(`INSERT INTO users (nickname, password, email, gender, role, school, words, ipaddr) 32 | SELECT CONCAT(COALESCE($1, 'ojuser'), '_' , t.val), $2, CONCAT(COALESCE($1, 'ojuser'), '_', t.val,'@dummy.nankai.edu.cn'), $3, $4, $5, $6, '::ffff:127.0.0.1' 33 | FROM (SELECT nextval('users_defaultname_seq') as val) AS t RETURNING nickname` 34 | , [form.nickname, password, 3, `{1, 4}`, 'NKU', form.words]) 35 | retArr.push({nickname: ret.rows[0].nickname, password: password}) 36 | } catch (err) { 37 | res.fail(520, err) 38 | throw err 39 | } 40 | } 41 | res.ok(retArr) 42 | } 43 | }) 44 | 45 | router.get('/remove/:who', async (req, res) => { 46 | 'use strict' 47 | const user = req.params.who 48 | if (user === req.session.user) 49 | return res.fail(1) 50 | 51 | let ret 52 | if (Number.isInteger(Number(user))) 53 | ret = await db.query(`UPDATE users SET removed = 't'::boolean WHERE user_id = $1 RETURNING user_id`, [user]) 54 | else 55 | ret = await db.query(`UPDATE users SET removed = 't'::boolean WHERE nickname = $1 RETURNING user_id`, [user]) 56 | 57 | if (ret.rows.length) { 58 | res.ok({removed: ret.rows[0].user_id}) 59 | session.logout(ret.rows[0].user_id) 60 | } 61 | else 62 | res.fail(1, 'no such user') 63 | }) 64 | 65 | router.get('/logout/:who', async (req, res) => { 66 | 'use strict' 67 | const who = req.params.who 68 | if (who === 'all') { 69 | session.logoutAll() 70 | } else { 71 | session.logout(who) 72 | } 73 | res.ok() 74 | }) 75 | 76 | // add multiple contset users 77 | router.post('/addmulti', async (req, res, next) => { 78 | 'use strict' 79 | const form = req.body 80 | const cid = form.cid 81 | const num = form.num 82 | const nameList = form.nameList 83 | // console.warn(cid, num, nameList) 84 | if (nameList.length !== num) 85 | return res.fail(422, 'nameList length not match with number of users to add') 86 | let ret = await db.query(`SELECT contest_id, private FROM contests WHERE contest_id = ${cid}`) 87 | if (ret.rows.length === 0) return res.fail(404, 'contest not found') 88 | if (!ret.rows[0].private) 89 | return res.fail(422, 'A public contest is not applicable for contest users') 90 | ret = await db.query( 91 | 'SELECT split_part(split_part(email, \'_\', 2), \'_\', 1) AS uid FROM users WHERE email LIKE $1', 92 | ['c' + cid + '_%'] 93 | ) 94 | // find max id with like c1001_1 95 | let begin = 1, nickID 96 | for (let i = 0, len = ret.rows.length; i < len; i++) { 97 | nickID = parseInt(ret.rows[i].uid) 98 | if (nickID) { 99 | begin = Math.max(begin, nickID + 1) 100 | } 101 | } 102 | let insertArr = Array(num), resUser = new Array(num) 103 | // prepare users 104 | for (let i = 0; i < num; i++) { 105 | const name = `c${cid}_${i+begin}` 106 | const nickname = name + '_' + nameList[i] 107 | const pass_raw = genRawStr(8) 108 | const pass_en = pass_raw // getPass(pass_raw) 109 | //console.error(pass_en) 110 | insertArr[i] = `( 111 | '${nickname}','${pass_en}','${name}@nkpc.nankai.edu.cn', 112 | 3, 'NKU', 'A', '::ffff:127.0.0.1')` 113 | resUser[i] = { name: nameList[i], username: nickname, password: pass_raw, email: name+'@nkpc.nankai.edu.cn'} 114 | } 115 | const insertPrefix = 'INSERT INTO users (nickname, password, email, gender, school, words, ipaddr) VALUES' 116 | const addUserSQL = insertPrefix + insertArr.join(',') + ' RETURNING user_id' 117 | //console.error(addUserSQL) 118 | // use transaction 119 | const client = await pool.connect() 120 | try { 121 | await client.query('BEGIN') 122 | ret = await db.query(addUserSQL) 123 | if (ret.rows.length !== num) { 124 | throw RangeError('some user did not insert successfully') 125 | } 126 | //console.error(ret.rows) 127 | let idList = new Array(num) 128 | for (let i = 0; i < num; i++) { 129 | resUser[i].user_id = ret.rows[i].user_id 130 | idList[i] = `(${cid}, ${ret.rows[i].user_id})` 131 | } 132 | // console.error(idList) 133 | ret = await db.query( 134 | 'INSERT INTO contest_users (contest_id, user_id) VALUES ' + 135 | idList.join(',') + ' ON CONFLICT DO NOTHING' 136 | ) 137 | // console.error(ret.rows) 138 | return res.ok({resUser}) 139 | } catch (e) { 140 | ret = await db.query('ROLLBACK') 141 | return res.fail(520, e) 142 | } finally { 143 | client.release() 144 | } 145 | }) 146 | 147 | module.exports = router 148 | -------------------------------------------------------------------------------- /lib/chat.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | const striptags = require('striptags') 3 | const db = require('../database/db') 4 | const session = require('./session') 5 | const {check_perm, SUPER_ADMIN} = require('./permission') 6 | const {FRONT_END_PATH, BACK_END_PATH} = require('../config/basic') 7 | const git = { 8 | front: require('../lib/git')(FRONT_END_PATH), 9 | back: require('../lib/git')(BACK_END_PATH) 10 | } 11 | 12 | const errorCallback = () => {} 13 | 14 | const formMessage = (isAdmin, color, user, message) => { 15 | color = Math.random() * 16 16 | const buf = Buffer.concat([ 17 | Buffer.from([0x02, isAdmin ? 0x01 : 0x00, color % 16]), 18 | Buffer.from([Buffer.byteLength(user)]), 19 | Buffer.from(user), 20 | Buffer.from([Buffer.byteLength(message)]), 21 | Buffer.from(message) 22 | ]) 23 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) 24 | } 25 | 26 | // DEV: just for fun 27 | const rebuild = async (ws, type) => { 28 | switch (type) { 29 | case undefined: 30 | case 'frontend': 31 | ws.send(formMessage(1, 0, 'SYSTEM', 'Resetting..'), errorCallback) 32 | ws.send(formMessage(1, 0, 'SYSTEM', (await git.front.resetAll()).stdout), errorCallback) 33 | ws.send(formMessage(1, 0, 'SYSTEM', 'Pulling..'), errorCallback) 34 | const fres = await git.front.pull() 35 | ws.send(formMessage(1, 0, 'SYSTEM', fres.stdout), errorCallback) 36 | if (fres.stdout === 'Already up to date.\n') 37 | return ws.send(formMessage(1, 0, 'SYSTEM', 'Nothing to be done.'), errorCallback) 38 | 39 | ws.send(formMessage(1, 0, 'SYSTEM', 'NPM Installing..'), errorCallback) 40 | ws.send(formMessage(1, 0, 'SYSTEM', (await git.front.install()).stdout), errorCallback) 41 | ws.send(formMessage(1, 0, 'SYSTEM', 'Webpack Building..'), errorCallback) 42 | ws.send(formMessage(1, 0, 'SYSTEM', (await git.front.rebuild()).stdout), errorCallback) 43 | ws.send(formMessage(1, 0, 'SYSTEM', 'finished.'), errorCallback) 44 | break 45 | case 'backend': 46 | ws.send(formMessage(1, 0, 'SYSTEM', 'Resetting..'), errorCallback) 47 | ws.send(formMessage(1, 0, 'SYSTEM', (await git.back.resetAll()).stdout), errorCallback) 48 | ws.send(formMessage(1, 0, 'SYSTEM', 'Pulling..'), errorCallback) 49 | const bres = await git.back.pull() 50 | ws.send(formMessage(1, 0, 'SYSTEM', bres.stdout), errorCallback) 51 | if (bres.stdout === 'Already up to date.\n') 52 | return ws.send(formMessage(1, 0, 'SYSTEM', 'Nothing to be done.'), errorCallback) 53 | 54 | ws.send(formMessage(1, 0, 'SYSTEM', 'NPM Installing..'), errorCallback) 55 | ws.send(formMessage(1, 0, 'SYSTEM', (await git.back.install()).stdout), errorCallback) 56 | ws.send(formMessage(1, 0, 'SYSTEM', 'finished. restarting server..'), errorCallback) 57 | setTimeout(() => process.exit(-1), 1000) 58 | break 59 | default: 60 | ws.send(formMessage(1, 0, 'SYSTEM', 'unknown type'), errorCallback) 61 | } 62 | } 63 | 64 | module.exports = (server) => { 65 | let currentConnection = 0 66 | const wss = new WebSocket.Server({ 67 | server, 68 | perMessageDeflate: { 69 | zlibDeflateOptions: { 70 | chunkSize: 1024, 71 | memLevel: 7, 72 | level: 3 73 | }, 74 | zlibInflateOptions: { 75 | chunkSize: 10 * 1024 76 | }, 77 | concurrencyLimit: 10, 78 | threshold: 1024 79 | }, 80 | maxPayload: 10000 81 | }) 82 | 83 | setInterval(function ping () { 84 | wss.clients.forEach(function each (ws) { 85 | if (ws.isAlive === false) 86 | return ws.terminate() 87 | ws.send(Buffer.from([0x1, 0x2]), errorCallback) 88 | ws.isAlive = false 89 | ws.ping() 90 | }) 91 | }, 30000) 92 | 93 | wss.on('connection', function connection (ws, req) { 94 | ws.binaryType = 'arraybuffer' 95 | currentConnection++ 96 | ws.isAlive = true 97 | session(req, {}, async function (err, _) { 98 | if (!err) { 99 | const ipaddr = req.connection.remoteAddress.endsWith('127.0.0.1') 100 | ? (req.headers['x-forwarded-for'] || req.connection.remoteAddress) : req.connection.remoteAddress 101 | ws.username = req.session.nickname || ipaddr 102 | ws.ipaddr = ipaddr 103 | ws.user_id = req.session.user || null 104 | ws.isAdmin = await check_perm(req, SUPER_ADMIN) 105 | } 106 | }) 107 | ws.on('pong', () => ws.isAlive = true) 108 | ws.on('message', async function incoming (message) { 109 | if (typeof message !== 'string') 110 | return ws.terminate() 111 | message = message.trim() 112 | if (!ws.username) 113 | return ws.send(formMessage(1, 2, 'System', 'please wait...'), errorCallback) 114 | if (ws.isAdmin) { 115 | if (message.startsWith('rebuild')) return await rebuild(ws, message.split(' ')[1]) 116 | } else if (message.length > 30 || message.length === 0) 117 | return ws.send(formMessage(1, 2, 'System', 'too long...'), errorCallback) 118 | db.query('insert into user_danmaku (message, user_id, ipaddr) values ($1, $2, $3)', [message, ws.user_id, ws.ipaddr]) 119 | wss.clients.forEach(function each (client) { 120 | if (client.readyState === WebSocket.OPEN) { 121 | message = striptags(message, ['span', 'strong', 'color', 'font']) 122 | client.send(formMessage(ws.isAdmin, 0, ws.username, message)) 123 | } 124 | }) 125 | }) 126 | ws.on('close', () => currentConnection--) 127 | ws.send(formMessage(1, 2, 'Online', currentConnection.toString()), errorCallback) 128 | }) 129 | 130 | wss.on('headers', (headers) => { 131 | headers.push('Access-Control-Allow-Credentials: true') 132 | headers.push('Access-Control-Allow-Origin: http://acm.nankai.edu.cn') 133 | headers.push('Access-Control-Allow-Headers: Sec-WebSocket-Extensions') 134 | headers.push('Access-Control-Allow-Headers: Sec-WebSocket-Key') 135 | headers.push('Access-Control-Allow-Headers: Sec-WebSocket-Version') 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /api/post.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | const fc = require('../lib/form-check') 4 | const striptags = require('striptags') 5 | const {limit} = require('../lib/rate-limit') 6 | const {require_perm, POST_NEW_POST, REPLY_POST} = require('../lib/permission') 7 | 8 | // 获得帖子 9 | 10 | router.get('/:post', async (req, res, next) => { 11 | const post = Number(req.params.post) 12 | if (!Number.isInteger(post)) return next() 13 | 14 | const result = await db.query( 15 | 'SELECT * FROM posts WHERE removed_date IS NULL AND (post_id = $1 OR parent_id = $1) ORDER BY since' 16 | , [post] 17 | ) 18 | 19 | res.ok(result.rows) 20 | }) 21 | 22 | // 发新帖子 23 | 24 | router.post('/:pid' 25 | , require_perm(POST_NEW_POST) 26 | , fc.all(['title', 'content']) 27 | , limit('post') 28 | , async (req, res, next) => { 29 | 30 | const pid = Number(req.params.pid) 31 | if (!Number.isInteger(pid)) return next() 32 | 33 | const uid = req.session.user 34 | const title = req.fcResult.title 35 | 36 | const content = striptags(req.fcResult.content, ['span', 'strong', 'color', 'img', 'b', 'a']) 37 | const nick_result = await db.query(`SELECT nickname FROM users WHERE user_id = ${uid}`) 38 | if (!(nick_result.rows.length)) { 39 | res.fail(500, 'user id not found') 40 | } 41 | const nickname = nick_result.rows[0].nickname 42 | const ret = await db.query( 43 | `INSERT INTO post (user_id, nickname, title, content, problem_id, ipaddr_id) `+ 44 | `VALUES ($1, $2, $3, $4, $5, get_ipaddr_id($6) ) RETURNING *`, [uid, nickname, title, content, pid || undefined, req.ip] 45 | ) 46 | 47 | res.ok(ret.rows[0]) 48 | }) 49 | 50 | 51 | // 回复帖子 52 | 53 | router.post('/reply/:parent' 54 | , require_perm(REPLY_POST) 55 | , fc.all(['content']) 56 | , limit('post') 57 | , async (req, res, next) => { 58 | 59 | const parent = Number(req.params.parent) 60 | if (!Number.isInteger(parent)) return next() 61 | 62 | const uid = req.session.user 63 | const content = striptags(req.fcResult.content, ['span', 'strong', 'color', 'img', 'b', 'a']) 64 | 65 | const p = await db.query('SELECT parent_id, problem_id, closed_date, removed_date FROM post WHERE post_id = $1', [parent]) 66 | if (!p.rows.length) 67 | return res.fail(404, 'origin post not found') 68 | 69 | const row = p.rows[0] 70 | if (row.parent_id || row.closed_date || row.removed_date) 71 | return res.fail(422, 'origin post not accept reply') 72 | 73 | const pid = row.problem_id 74 | 75 | const nick_result = await db.query(`SELECT nickname FROM users WHERE user_id = ${uid}`) 76 | if ( !nick_result.rows.length) { 77 | res.fail(500, 'user id not found') 78 | } 79 | const nickname = nick_result.rows[0].nickname 80 | 81 | const ret = await db.query( 82 | 'INSERT INTO post (user_id, nickname,content, parent_id, problem_id, ipaddr_id)' + 83 | ' VALUES ($1, $2, $3, $4, $5, get_ipaddr_id($6)) RETURNING *', [uid, nickname, content, parent, pid, req.ip] 84 | ) 85 | 86 | res.ok(ret.rows[0]) 87 | }) 88 | 89 | // 帖子赞同 / 反对 90 | 91 | router.get('/:type(upvote|downvote|rmvote)/:post' 92 | , require_perm() 93 | , limit('vote') 94 | , async (req, res, next) => { 95 | 96 | const post = Number(req.params.post) 97 | if (!Number.isInteger(post)) return next() 98 | 99 | const uid = req.session.user 100 | 101 | const p = await db.query('SELECT problem_id, closed_date, removed_date FROM post WHERE post_id = $1', [post]) 102 | if (!p.rows.length) 103 | return res.fail(404, 'origin post not found') 104 | 105 | const row = p.rows[0] 106 | if (row.closed_date || row.removed_date) 107 | return res.fail(422, 'origin post not support vote') 108 | 109 | let ret 110 | 111 | if (req.params.type === 'rmvote') { 112 | ret = await db.query('DELETE FROM post_vote WHERE user_id = $1 AND post_id = $2', [uid, post]) 113 | } else { 114 | ret = await db.query( 115 | 'INSERT INTO post_vote (user_id, post_id, attitude)' + 116 | ' VALUES ($1, $2, $3) ON CONFLICT DO NOTHING RETURNING *', [uid, post, req.params.type === 'upvote'] 117 | ) 118 | } 119 | res.ok({affected: ret.rowCount}) 120 | }) 121 | 122 | // 评论帖子 123 | 124 | router.post('/comment/:post', require_perm(REPLY_POST), limit('post') 125 | , fc.all(['content']), async (req, res, next) => { 126 | const post = Number(req.params.post) 127 | if (!Number.isInteger(post)) return next() 128 | 129 | const uid = req.session.user 130 | const p = await db.query('SELECT problem_id, closed_date, removed_date FROM post WHERE post_id = $1', [post]) 131 | if (!p.rows.length) 132 | return res.fail(404, 'origin post not found') 133 | 134 | const row = p.rows[0] 135 | 136 | if (row.closed_date || row.removed_date) 137 | return res.fail(422, 'origin post not support comment') 138 | 139 | const content = striptags(req.fcResult.content, ['span', 'strong', 'color', 'img', 'b', 'a']) 140 | const nick_result = await db.query(`SELECT nickname FROM users WHERE user_id = ${uid}`) 141 | if ( !nick_result.rows.length) { 142 | res.fail(500, 'user id not found') 143 | } 144 | const nickname = nick_result.rows[0].nickname 145 | const ret = await db.query( 146 | 'INSERT INTO post_reply (reply_to, user_id, nickname, content, ipaddr_id)' + 147 | ' VALUES ($1, $2, $3, $4, get_ipaddr_id($5)) RETURNING *', [post, uid, nickname, content, req.ip] 148 | ) 149 | 150 | res.ok(ret.rows[0]) 151 | }) 152 | 153 | // 赞同评论 154 | 155 | router.get('/like/:comment', require_perm(), limit('vote'), async (req, res, next) => { 156 | 157 | const comment = Number(req.params.comment) 158 | if (!Number.isInteger(comment)) return next() 159 | 160 | const uid = req.session.user 161 | 162 | const p = await db.query('SELECT removed_date FROM post_reply WHERE reply_id = $1', [comment]) 163 | if (!p.rows.length) 164 | return res.fail(404, 'origin comment not found') 165 | 166 | const row = p.rows[0] 167 | if (row.removed_date) 168 | return res.fail(422, 'origin comment not support vote') 169 | 170 | const ret = await db.query( 171 | 'INSERT INTO reply_vote (user_id, reply_id)' + 172 | ' VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING *', [uid, comment] 173 | ) 174 | 175 | res.ok({affected: ret.rowCount}) 176 | }) 177 | 178 | module.exports = router 179 | -------------------------------------------------------------------------------- /lib/form-check.js: -------------------------------------------------------------------------------- 1 | const {checkSchema, validationResult, oneOf} = require('express-validator/check') 2 | const {matchedData} = require('express-validator/filter') 3 | const validator = require('validator') 4 | const decrypt = require('./rsa').decrypt 5 | 6 | const all_rule = { 7 | in: ['params', 'query', 'body'] 8 | } 9 | 10 | const default_rules = { 11 | none: {}, 12 | integer: { 13 | errorMessage: 'is not an integer', 14 | isInt: true, 15 | toInt: true 16 | }, 17 | nickname: { 18 | custom: { 19 | options: str => !validator.isNumeric(str) && !validator.isEmail(str), 20 | errorMessage: 'should not be pure number' 21 | }, 22 | isLength: { 23 | options: {min: 3, max: 20}, 24 | errorMessage: 'length is not between 3 and 20' 25 | } 26 | }, 27 | email: { 28 | isEmail: true, 29 | errorMessage: 'is not an email' 30 | }, 31 | password: { 32 | custom: { 33 | options: pwd => { 34 | pwd = pwd ? (pwd.length === 344 ? decrypt(pwd) : pwd) : '' 35 | return pwd && pwd.length >= 6 && pwd.length <= 20 36 | }, 37 | errorMessage: 'length is not between 6 and 20, or failed to decrypt' 38 | }, 39 | customSanitizer: { 40 | options: pwd => pwd ? (pwd.length === 344 ? decrypt(pwd) : pwd) : '' 41 | } 42 | }, 43 | gender: { 44 | custom: { 45 | options: gen => Number.isInteger(Number(gen)) && Number(gen) >= 0 && Number(gen) <= 3, 46 | errorMessage: 'not an integer between 0 and 3' 47 | }, 48 | customSanitizer: { 49 | options: gen => Number(gen) || 0 50 | }, 51 | optional: true 52 | }, 53 | school: { 54 | isLength: { 55 | options: {min: 2, max: 40}, 56 | errorMessage: 'length is not between 2 and 40' 57 | }, 58 | optional: true 59 | }, 60 | optional: { 61 | optional: true 62 | }, 63 | title: { 64 | isLength: { 65 | options: {min: 3, max: 80}, 66 | errorMessage: 'length is not between 3 and 80' 67 | } 68 | }, 69 | boolean: { 70 | custom: { 71 | options: bool => (typeof bool === 'boolean') ? true : (validator.isIn(bool ? bool.toString() : '', ['0', '1'])) 72 | }, 73 | toInt: true 74 | }, 75 | qq: { 76 | isLength: { 77 | options: {min: 5, max: 13}, 78 | errorMessage: 'length is not between 5 and 13' 79 | }, 80 | isInt: true, 81 | optional: true 82 | }, 83 | phone: { 84 | isMobilePhone: { 85 | options: 'zh-CN' 86 | }, 87 | optional: true 88 | }, 89 | real_name: { 90 | isLength: { 91 | options: {max: 20}, 92 | errorMessage: 'length is not between 2 and 20' 93 | }, 94 | optional: true 95 | }, 96 | words: { 97 | isLength: { 98 | options: {max: 50}, 99 | errorMessage: 'max length is 50' 100 | }, 101 | trim: true, 102 | escape: true, 103 | optional: true 104 | }, 105 | code: { 106 | isLength: { 107 | options: {min: 1, max: 50000}, 108 | errorMessage: 'max length is 50k bytes...' 109 | }, 110 | trim: true 111 | }, 112 | api_name: { 113 | isLength: { 114 | options: {min: 1, max: 20}, 115 | errorMessage: 'max length is 20' 116 | }, 117 | trim: true 118 | }, 119 | content: { 120 | isLength: { 121 | options: {min: 1, max: 5000}, 122 | errorMessage: 'max length is 5000' 123 | }, 124 | trim: true 125 | }, 126 | comment_content: { 127 | isLength: { 128 | options: {min: 1, max: 600}, 129 | errorMessage: 'max length is 600' 130 | }, 131 | trim: true 132 | }, 133 | message: { 134 | isLength: { 135 | options: {min: 3, max: 600}, 136 | errorMessage: 'length: 3 - not very long' 137 | }, 138 | escape: true 139 | } 140 | } 141 | 142 | const getRule = (key, which) => { 143 | which = which ? which.split('/') : [key] 144 | let obj = {...all_rule} 145 | which.forEach(k => obj = {...obj, ...default_rules[k]}) 146 | return {[key]: obj} 147 | } 148 | 149 | const getCheckSchema = (key, which) => { 150 | return checkSchema(getRule(key, which)) 151 | } 152 | 153 | const rules = { 154 | l: getCheckSchema('l', 'integer/optional'), 155 | r: getCheckSchema('r', 'integer/optional'), 156 | id: getCheckSchema('id', 'integer'), 157 | user: [oneOf([getCheckSchema('user', 'email'), getCheckSchema('user', 'nickname')])], 158 | count: getCheckSchema('count', 'integer'), 159 | problem_id: getCheckSchema('problem_id', 'integer'), 160 | cases: getCheckSchema('cases', 'integer'), 161 | time_limit: getCheckSchema('time_limit', 'integer'), 162 | memory_limit: getCheckSchema('memory_limit', 'integer'), 163 | special_judge: getCheckSchema('special_judge', 'integer'), 164 | detail_judge: getCheckSchema('detail_judge', 'boolean'), 165 | level: getCheckSchema('level', 'integer'), 166 | pid: getCheckSchema('pid', 'integer'), 167 | did: getCheckSchema('did', 'integer'), 168 | cid: getCheckSchema('cid', 'integer'), 169 | mid: getCheckSchema('mid', 'integer'), 170 | uid: getCheckSchema('uid', 'integer'), 171 | rid: getCheckSchema('rid', 'integer'), 172 | lang: getCheckSchema('lang', 'integer'), 173 | comment: getCheckSchema('content', 'comment_content') 174 | } 175 | 176 | const formatter = ({location, msg, param, value, nestedErrors}) => { 177 | let name = new Set() 178 | let reason = [] 179 | if (nestedErrors) { 180 | nestedErrors.forEach(v => { 181 | name.add(v.param) 182 | reason.push(v.msg) 183 | }) 184 | } else { 185 | name.add(param) 186 | reason.push(msg) 187 | } 188 | return { 189 | name: [...name].join(', '), 190 | message: reason.join(', or '), 191 | debug: { 192 | value: value === undefined ? null : value, 193 | location: location 194 | } 195 | } 196 | } 197 | 198 | module.exports = { 199 | all: (r) => async (req, res, next) => { 200 | let err = false 201 | if (!Array.isArray(r)) r = [r] 202 | for (let rule of r) { 203 | try { 204 | await new Promise(async (resolve, reject) => { 205 | const key = rule.split('/')[0] 206 | let handler = rules[rule] || getCheckSchema(key, default_rules[key] ? rule : 'none') 207 | const handlerCallback = () => { 208 | const errors = validationResult(req).formatWith(formatter) 209 | if (!errors.isEmpty()) { 210 | res.fail ? res.fail(422, errors.array()) : res.status(422).json(errors.array()) 211 | reject() 212 | } 213 | req.fcResult = req.fcResult ? {...req.fcResult, ...matchedData(req)} : {...matchedData(req)} 214 | resolve() 215 | } 216 | if (!Array.isArray(handler)) handler = [handler] 217 | await new Promise(() => {handler.forEach((v) => { v(req, res, handlerCallback)})}) 218 | resolve() 219 | }) 220 | } catch (e) { 221 | if (e) console.error(e) 222 | return 223 | } 224 | } 225 | next() 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /api/user/user_register.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const fc = require('../../lib/form-check') 3 | const db = require('../../database/db') 4 | 5 | const md5 = require('../../lib/md5') 6 | const captcha = require('../../lib/captcha') 7 | const {require_limit, apply_limit} = require('../../lib/rate-limit') 8 | 9 | const {DB_USER} = require('../../config/redis') 10 | const redis = require('../../lib/redis')(DB_USER) 11 | const {sendVerificationMail, banEmail} = require('../../lib/mail') 12 | const regex_email = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/ 13 | 14 | router.get('/verify/:email', captcha.check('sendmail'), fc.all('email'), require_limit('sendmail'), async (req, res) => { 15 | 'use strict' 16 | const email = req.fcResult.email 17 | let result 18 | if (result = await db.checkEmail(email)) 19 | return res.fail(1, result) 20 | 21 | const code = Math.floor(Math.random() * 900000) + 100000 22 | const key = md5(email + code) 23 | const link = `${require('../../config/path').BASE_URL}/api/u/verify/${key}/${code}` 24 | 25 | result = await redis.setAsync(key, code, 'NX', 'EX', 600) 26 | if (!result) return res.fail(500, 'unexpected hash conflict') 27 | 28 | sendVerificationMail(email, code, link, (result) => { 29 | if (result.success) { 30 | apply_limit('sendmail', req) 31 | return res.ok({key: key}) 32 | } 33 | redis.del(key) 34 | return res.fail(1, result) 35 | }) 36 | }) 37 | 38 | router.get('/verify/:key/:code', async (req, res, next) => { 39 | 'use strict' 40 | const key = req.params.key 41 | const code = req.params.code 42 | const ret = await redis.getAsync(key) 43 | if (regex_email.test(code)) { 44 | req.email_code = ret 45 | return next() 46 | } 47 | req.session.ecode = code 48 | if (ret !== code) return res.fail(1, [{name: 'ecode', message: 'not match'}]) 49 | return res.ok('email verified') 50 | }) 51 | 52 | router.get('/verify/:key/:email', require_limit('sendmail'), async (req, res) => { 53 | 'use strict' 54 | const key = req.params.key 55 | const email = req.params.email 56 | const code = req.email_code 57 | 58 | if (key === md5(email + code)) { 59 | const link = `${require('../../config/path').BASE_URL}/api/u/verify/${key}/${code}` 60 | return sendVerificationMail(email, code, link, (result) => { 61 | console.log(email, result) 62 | if (result.success) { 63 | apply_limit('sendmail') 64 | return res.ok({key: key}) 65 | } 66 | return res.fail(1, result) 67 | }) 68 | } 69 | return res.fail(422, [{name: 'email', message: 'not match'}]) 70 | }) 71 | 72 | // noinspection JSUnresolvedFunction 73 | router.post('/register', fc.all(['nickname', 'password', 'email', 'school', 'gender']), async (req, res) => { 74 | 'use strict' 75 | const form = req.fcResult 76 | let result 77 | if (result = await db.checkName(form.nickname)) 78 | return res.fail(422, result) 79 | 80 | form.ecode = req.body.ecode || req.session.ecode 81 | const hashed_key = md5(form.email + form.ecode) 82 | if (await redis.getAsync(hashed_key) !== form.ecode) 83 | return res.fail(422, [{name: 'ecode', message: 'not match'}]) 84 | 85 | try { 86 | if (result = await db.checkEmail(form.email)) 87 | return res.fail(1, result) 88 | const query = 'INSERT INTO users (nickname, password, email, gender, school, ipaddr) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *' 89 | result = await db.query(query, [form.nickname, form.password, form.email, form.gender, form.school, req.ip]) 90 | } catch (err) { 91 | res.fail(520, err) 92 | throw err 93 | } 94 | req.session.nickname = form.nickname 95 | req.session.user = result.rows[0].user_id 96 | req.session.save() 97 | res.ok(result.rows[0]) 98 | redis.del(hashed_key) 99 | }) 100 | 101 | 102 | /** 103 | * Reset password 104 | */ 105 | router.get('/resetpwd/:email', captcha.check('sendmail'), fc.all('email'), require_limit('sendmail'), async (req, res) => { 106 | 'use strict' 107 | const email = req.fcResult.email 108 | let result 109 | result = await db.checkEmail(email) 110 | if (result === false){ 111 | result = [{ name: 'email', message: '邮箱未注册' }] 112 | return res.fail(1, result) 113 | } 114 | const code = Math.floor(Math.random() * 900000) + 100000 115 | const key = md5(email + code) 116 | const link = `${require('../../config/path').BASE_URL}/api/u/verify/${key}/${code}` 117 | 118 | result = await redis.setAsync(key, code, 'NX', 'EX', 600) 119 | if (!result) return res.fail(500, 'unexpected hash conflict') 120 | 121 | sendVerificationMail(email, code, link, (result) => { 122 | if (result.success) { 123 | apply_limit('sendmail', req) 124 | return res.ok({ key: key }) 125 | } 126 | redis.del(key) 127 | return res.fail(1, result) 128 | }) 129 | }) 130 | 131 | router.get('/resetpwd/verify/:key/:code', async (req, res, next) => { 132 | 'use strict' 133 | const key = req.params.key 134 | const code = req.params.code 135 | const ret = await redis.getAsync(key) 136 | if (regex_email.test(code)) { 137 | req.email_code = ret 138 | return next() 139 | } 140 | req.session.ecode = code 141 | if (ret !== code) return res.fail(1, [{ name: 'ecode', message: 'not match' }]) 142 | return res.ok('email verified') 143 | }) 144 | 145 | router.post('/reset_passwd', fc.all(['email', 'password']), async (req, res) => { 146 | 'use strict' 147 | const form = req.fcResult 148 | let result 149 | form.ecode = req.body.ecode || req.session.ecode 150 | const hashed_key = md5(form.email + form.ecode) 151 | if (await redis.getAsync(hashed_key) !== form.ecode) 152 | return res.fail(422, [{ name: 'ecode', message: 'not match' }]) 153 | try { 154 | result = await db.checkEmail(form.email) 155 | if (result === false) { 156 | result = [{ name: 'email', message: '邮箱未注册' }] 157 | return res.fail(1, result) 158 | } 159 | const query = 'UPDATE users SET password=$1, ipaddr=$2 WHERE email=$3 RETURNING *' 160 | result = await db.query(query, [form.password, req.ip, form.email]) 161 | } catch (err) { 162 | res.fail(520, err) 163 | throw err 164 | } 165 | req.session.nickname = form.nickname 166 | req.session.user = result.rows[0].user_id 167 | req.session.save() 168 | res.ok(result.rows[0]) 169 | redis.del(hashed_key) 170 | }) 171 | 172 | 173 | /** 174 | * Unsubscribe 175 | */ 176 | router.get('/unsubscribe/:hash/:email', async (req, res) => { 177 | 'use strict' 178 | const email = Buffer.from(req.params.email, 'base64').toString() 179 | const hash = req.params.hash 180 | const cb = result => { 181 | if (result.success) return res.ok(result) 182 | return res.fail(1, result) 183 | } 184 | banEmail(hash, email, false, cb) 185 | }) 186 | 187 | module.exports = router 188 | -------------------------------------------------------------------------------- /api/admin/admin_contest.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const multer = require('multer') 3 | const path = require('path') 4 | const {CONTEST_PATH, TEMP_PATH} = require('../../config/basic') 5 | const fc = require('../../lib/form-check') 6 | const db = require('../../database/db') 7 | const fs = require('fs') 8 | 9 | // 添加一个新的竞赛 10 | 11 | 12 | const upload = multer({ 13 | storage: multer.diskStorage({ 14 | destination: (req, file, cb) => { 15 | cb(null, TEMP_PATH) 16 | }, 17 | filename: (req, file, cb) => { 18 | cb(null, `${Date.now().toString(36)}${Math.floor(Math.random() * 100).toString(36)}${path.extname(file.originalname)}`) 19 | } 20 | }) 21 | }) 22 | 23 | router.post('/', upload.single('file') 24 | , fc.all(['title', 'description', 'start', 'end', 'perm', 'private', 'rule']) 25 | , async (req, res) => { 26 | 27 | const form = req.fcResult 28 | const during = '[' + form.start + ',' + form.end + ']' 29 | 30 | try { 31 | const ret = await db.query( 32 | 'INSERT INTO contests (title, during, description, perm, private, rule) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *' 33 | , [form.title, during, form.description, form.perm, form.private, form.rule] 34 | ) 35 | const cid = ret.rows[0].contest_id 36 | let desc 37 | if (req.file) { 38 | const file = req.file 39 | fs.renameSync(`${file.destination}/${file.filename}`, `${CONTEST_PATH}/${cid}.md`) 40 | desc = fs.readFileSync(`${CONTEST_PATH}/${cid}.md`, 'utf8') 41 | } 42 | 43 | return res.ok({file: desc, ...ret.rows[0]}) 44 | 45 | } catch (e) { 46 | if (req.file) { 47 | fs.unlinkSync(`${req.file.destination}/${req.file.filename}`) 48 | } 49 | return res.fail(500, e.stack || e) 50 | } 51 | }) 52 | 53 | // 获得已有竞赛信息 54 | router.get('/:cid', fc.all(['cid']), async (req, res) => { 55 | const cid = req.fcResult.cid 56 | // 基本信息 57 | const basic = await db.query('SELECT * FROM user_contests WHERE contest_id = $1', [cid]) 58 | if (basic.rows.length === 0) return res.fail(404) 59 | 60 | // 人员信息 61 | const participants = await db.query('SELECT user_id, nickname FROM contest_users NATURAL JOIN user_info NATURAL JOIN user_nick WHERE contest_id = $1', [cid]) 62 | 63 | // 描述文件 64 | let file 65 | try { 66 | file = fs.readFileSync(`${CONTEST_PATH}/${cid}.md`, 'utf8') 67 | } catch (e) { 68 | 69 | } 70 | res.ok({...basic.rows[0], participants: participants.rows, file}) 71 | }) 72 | 73 | router.get('/remove/:cid', fc.all(['cid']), async (req, res) => { 74 | const cid = req.fcResult.cid 75 | // 查看基本信息 76 | const basic = await db.query('SELECT * FROM user_contests WHERE contest_id = $1', [cid]) 77 | if (basic.rows.length === 0) return res.fail(404) 78 | // 删除数据库 79 | const participants = await db.query('DELETE FROM contests WHERE contest_id = $1', [cid]) 80 | 81 | // 删除描述文件 82 | let file 83 | try { 84 | file = fs.unlinkSync(`${CONTEST_PATH}/${cid}.md`, 'utf8') 85 | } catch (e) { 86 | 87 | } 88 | res.ok('remove successfully') 89 | }) 90 | 91 | 92 | // 添加 / 删除竞赛用户 93 | 94 | router.get('/:cid/user/:type(add|remove)/:uid', fc.all(['cid']), async (req, res) => { 95 | const form = req.fcResult 96 | const debug = {debug: {type: req.params.type, ...form}} 97 | let ret 98 | // DEBUG: 99 | ret = await db.query('SELECT private FROM contests WHERE contest_id = $1', [form.cid]) 100 | if (!ret.rows.length) return res.fail(404) 101 | else if (!ret.rows[0].private) return res.fail(422, debug, 'operation not supported on non-private contest') 102 | 103 | let successCount = 0 104 | const uid = req.params.uid.split(',') 105 | switch (req.params.type) { 106 | case 'add': 107 | if (!uid.every(u => Number.isInteger(Number(u)))) return res.gen422('uid', 'some of them is not integer') 108 | for (let u of uid) { 109 | ret = await db.query('INSERT INTO contest_users(contest_id, user_id) VALUES($1, $2) ON CONFLICT DO NOTHING', [form.cid, u]) 110 | successCount += ret.rowCount 111 | } 112 | break 113 | case 'remove': 114 | if (uid[0] === 'all') { 115 | ret = await db.query('DELETE FROM contest_users WHERE contest_id = $1', [form.cid]) 116 | successCount += ret.rowCount 117 | } else { 118 | if (!uid.every(u => Number.isInteger(Number(u)))) return res.gen422('uid', 'some of them is not integer') 119 | for (let u of uid) { 120 | ret = await db.query('DELETE FROM contest_users WHERE contest_id = $1 AND user_id = $2', [form.cid, u]) 121 | successCount += ret.rowCount 122 | } 123 | } 124 | break 125 | } 126 | 127 | if (successCount) return res.ok({success: successCount}) 128 | return res.fail(404, debug, 'operation succeeded but nothing changed') 129 | }) 130 | 131 | // 添加 / 删除竞赛题目 132 | router.get('/:cid/problem/:type(add|remove)/:pid', fc.all(['cid', 'pid']), async (req, res) => { 133 | const form = req.fcResult 134 | let ret 135 | switch (req.params.type) { 136 | case 'add': 137 | ret = await db.query('UPDATE problems SET contest_id = $1 WHERE problem_id = $2 AND contest_id IS NULL', [form.cid, form.pid]) 138 | if (ret.rowCount) 139 | await db.query('INSERT INTO contest_problems(problem_id, contest_id) VALUES ($1, $2)', [form.pid, form.cid]) 140 | break 141 | case 'remove': 142 | ret = await db.query('UPDATE problems SET contest_id = NULL WHERE problem_id = $1 AND contest_id = $2', [form.pid, form.cid]) 143 | if (ret.rowCount) 144 | await db.query('DELETE FROM contest_problems WHERE problem_id = $1 AND contest_id = $2', [form.pid, form.cid]) 145 | break 146 | } 147 | if (ret.rowCount) return res.ok(ret.rows[0]) 148 | return res.fail(404, {debug: {type: req.params.type, ...form}}) 149 | }) 150 | 151 | // 修改竞赛 152 | router.post('/:cid' 153 | , upload.single('file') 154 | , fc.all(['title/optional', 'description/optional', 'start/optional', 'end/optional', 'perm/optional', 'private', 'rule']) 155 | , async (req, res) => { 156 | 157 | const cid = Number(req.params.cid) 158 | const basic = await db.query('SELECT * FROM user_contests WHERE contest_id = $1', [cid]) 159 | if (basic.rows.length === 0) { 160 | if (req.file) { 161 | fs.unlinkSync(`${req.file.destination}/${req.file.filename}`) 162 | } 163 | return res.fail(404) 164 | } 165 | 166 | if (req.file) { 167 | const file = req.file 168 | fs.renameSync(`${file.destination}/${file.filename}`, `${CONTEST_PATH}/${cid}.md`) 169 | } 170 | let file 171 | try { 172 | file = fs.readFileSync(`${CONTEST_PATH}/${cid}.md`, 'utf8') 173 | } catch (e) { 174 | 175 | } 176 | 177 | const {start, end, ...form} = req.fcResult 178 | form.during = '[' + start + ',' + end + ']' 179 | const query = [] 180 | const value = [] 181 | let c = 1 182 | for (let i in form) { 183 | if (!form.hasOwnProperty(i)) continue 184 | query.push(`"${i}" = $${c}`) 185 | value.push(form[i]) 186 | c++ 187 | } 188 | if (query.length !== 0) { 189 | const result = 190 | await db.query(`UPDATE contests SET ${query.join(', ')} WHERE contest_id = $${c} RETURNING *`, [...value, cid]) 191 | if (result.rows.length) 192 | return res.ok({file, ...result.rows[0]}) 193 | } 194 | return res.fail(422) 195 | }) 196 | 197 | //查看NKPC报名情况 198 | router.get('/nkpc/members', async (req, res) => { 199 | const result = await db.query('SELECT * FROM users_nkpc') 200 | res.ok(result.rows) 201 | }) 202 | 203 | router.post('/nkpc/secret_time', async(req, res) => { 204 | const during = '[' + req.body.start_time + ',' + req.body.end_time + ']' 205 | const result = await db.query(`INSERT INTO secret_time (during) VALUES($1)`, [during]) 206 | res.ok(result) 207 | }) 208 | 209 | //去重 210 | router.get('/nkpc/distinct', async(req, res) => { 211 | const result = await db.query('SELECT DISTINCT * FROM users_nkpc') 212 | await db.query('DELETE FROM users_nkpc where user_id != 0') 213 | console.log(result.rows.length) 214 | result.rows.forEach( (key) => { 215 | db.query('INSERT INTO users_nkpc (user_id, real_name, student_number, gender, institute, qq, phone) VALUES($1, $2, $3, $4, $5, $6, $7)', 216 | [key.user_id, key.real_name, key.student_number, key.gender, key.institute, key.qq, key.phone]) 217 | }) 218 | }) 219 | 220 | 221 | router.post('/nkpc/standings', async(req, res) => { 222 | const cid = req.body.cid 223 | const nameList = req.body.nameList 224 | let starMap = {} 225 | for (let i = 0; i < nameList.length; i++) { 226 | starMap[nameList[i].nickname]=nameList[i].school 227 | } 228 | let ret = await db.query(`SELECT contest_id, lower(during) as begin FROM contests WHERE contest_id = $1`, [cid]) 229 | if (ret.rows.length === 0) return res.fail(404, 'contest not found') 230 | const beginTime = new Date(ret.rows[0].begin) 231 | 232 | ret = await db.query('SELECT problem_id FROM contest_problems WHERE contest_id = $1 ORDER BY problem_id ASC', [cid]) 233 | let data = { problem_count: ret.rows.length} 234 | let problemMap = {} 235 | for (let i = 0, len = ret.rows.length; i < len; i++) { 236 | problemMap[ret.rows[i].problem_id] = String(i + 1) 237 | } 238 | 239 | ret = await db.query('SELECT user_id, nickname, school FROM users WHERE user_id IN\ 240 | (SELECT user_id FROM contest_users WHERE contest_id=$1)', [cid]) 241 | let participants = {} 242 | for (let i = 0, len = ret.rows.length; i < len; i++) { 243 | const nickname = ret.rows[i].nickname 244 | const isStar = nickname in starMap 245 | const school = isStar ? starMap[nickname] : ret.rows[i].school 246 | participants[String(ret.rows[i].user_id)] = { 247 | name: nickname, college: school, is_exclude: isStar 248 | } 249 | } 250 | data['users'] = participants 251 | 252 | ret = await db.query('SELECT solution_id, user_id, problem_id, score, "when" FROM solutions WHERE contest_id=$1', [cid]) 253 | let solutions = {} 254 | for (let i = 0, len = ret.rows.length; i < len ; i++) { 255 | const item = ret.rows[i] 256 | solutions[String(item.solution_id)] = { 257 | user_id: String(item.user_id), 258 | problem_index: problemMap[item.problem_id], 259 | verdict: item.score === 100.0 ? 'AC' : 'WA', 260 | submitted_seconds: parseInt((new Date(item.when) - beginTime) / 1000) 261 | } 262 | } 263 | data['solutions'] = solutions 264 | res.ok(data) 265 | }) 266 | 267 | module.exports = router 268 | -------------------------------------------------------------------------------- /api/status.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const db = require('../database/db') 3 | const fs = require('fs') 4 | const fc = require('../lib/form-check') 5 | const {check_perm, SUPER_ADMIN, GET_CODE_SELF, GET_CODE_ALL, VIEW_OUTPUT_SELF, VIEW_OUTPUT_ALL} = require('../lib/permission') 6 | const {getSolutionStructure, getProblemStructure} = require('../lib/judge') 7 | const language_ext = require('../lib/extension') 8 | async function check_acm_solution(req, ret, ban_status = 0){ 9 | let c_ret = await db.query("SELECT lower(secret_time.during) as start_time, upper(secret_time.during) as end_time FROM secret_time WHERE CURRENT_TIMESTAMP < upper(secret_time.during) ORDER BY lower(secret_time.during) LIMIT 1") 10 | if(await check_perm(req, SUPER_ADMIN)){ 11 | return 12 | } else if(c_ret.rows.length > 0){ 13 | let re_l = [] 14 | ret.rows.forEach((solution, index) => { 15 | solution.score = 666 16 | // temp disable 17 | if(solution.user_id != req.session.user){ 18 | solution.time = "810159641" 19 | solution.memory = "3141592653589" 20 | solution.code_size = "2357111317" 21 | } 22 | if(solution.user_id != req.session.user && solution.when < c_ret.rows[0].end_time && solution.when > c_ret.rows[0].start_time){ 23 | solution.detail = {} 24 | solution.status_id = 233 25 | solution.msg_en = "Secret" 26 | solution.msg_short = "ST" 27 | solution.msg_cn = "量子纠缠" 28 | solution.time = "810159641" 29 | solution.memory = "3141592653589" 30 | if(ban_status){ 31 | solution.delete = true 32 | } 33 | } 34 | if(ban_status && typeof(solution.delete) == "undefined") re_l.push(solution) 35 | }) 36 | if(ban_status) ret.rows = re_l 37 | } 38 | } 39 | async function check_oi_solution(req, ret, ban_status = 0){ 40 | let c_ret = await db.query("SELECT * FROM contest_problems LEFT JOIN contests ON contest_problems.contest_id = contests.contest_id WHERE CURRENT_TIMESTAMP < upper(contests.during) AND contests.rule=\'oi\'") 41 | if(await check_perm(req, SUPER_ADMIN)){ 42 | return 43 | } else { 44 | c_dic = {} 45 | c_ret.rows.forEach(function(c_p, index){ 46 | c_dic[c_p["problem_id"]] = "OI" 47 | }) 48 | let re_l = [] 49 | ret.rows.forEach(function(solution, index){ 50 | if(typeof(c_dic[solution.problem_id]) != "undefined"){ 51 | if(solution.status_id != 101){ 52 | solution.detail = {} 53 | solution.score = 100 54 | solution.status_id = 233 55 | solution.msg_en = "Secret" 56 | solution.msg_short = "ST" 57 | solution.msg_cn = "量子纠缠" 58 | solution.time = "810159641" 59 | solution.memory = "3141592653589" 60 | } 61 | if(ban_status){ 62 | solution.delete = true 63 | } 64 | } 65 | if(ban_status && typeof(solution.delete) == "undefined") re_l.push(solution) 66 | }) 67 | if(ban_status) ret.rows = re_l 68 | } 69 | } 70 | 71 | function loadPartialData (path) { 72 | return new Promise((resolve, reject) => { 73 | const stream = fs.createReadStream(path, { 74 | start: 0, 75 | end: 1000, 76 | autoClose: true, 77 | encoding: 'utf8' 78 | }) 79 | let res = '' 80 | stream.on('data', data => res += data) 81 | stream.on('end', () => { 82 | let arr = res.split('\n') 83 | if (arr.length > 20) arr = [...arr.slice(0, 20), '<...>'] 84 | resolve(arr.join('\n')) 85 | }) 86 | stream.on('error', (err) => reject(err)) 87 | }) 88 | } 89 | 90 | const fcMiddleware = fc.all(['pid/optional', 'uid/optional', 'nickname/optional', 'sid/integer/optional']) 91 | 92 | const getSQLClause = (offset, fields) => { 93 | let i = offset; 94 | let str = ['1=$' + ++i] 95 | let arr = [1]; 96 | let { pid, uid, nickname, sid } = fields; 97 | let ban_status = 0 98 | pid = Number(pid); 99 | uid = Number(uid); 100 | sid = Number(sid); 101 | if (pid) { str.push('problem_id=$' + ++i); arr.push(pid) } 102 | if (uid) { str.push('user_id=$' + ++i); arr.push(uid) } 103 | if (nickname) { str.push('nickname=$' + ++i); arr.push(nickname) } 104 | if (sid) { str.push('status_id=$' + ++i); arr.push(sid); ban_status = 1 } 105 | return [`WHERE ${str.join(' AND ')}`, arr, ban_status]; 106 | } 107 | 108 | router.get('/', fcMiddleware, async (req, res) => { 109 | 'use strict' 110 | let limit = 20 111 | let [wClause, sParam, ban_status] = getSQLClause(1, req.fcResult) 112 | const queryString = `SELECT * FROM user_solutions ${wClause} ORDER BY solution_id DESC LIMIT $1` 113 | let result = await db.query(queryString, [limit, ...sParam]) 114 | await check_oi_solution(req, result, ban_status) 115 | await check_acm_solution(req, result, ban_status) 116 | if (result.rows.length > 0) 117 | return res.ok(result.rows) 118 | return res.sendStatus(204) 119 | }) 120 | 121 | router.get('/:from(\\d+)/:limit(\\d+)?', fcMiddleware, async (req, res) => { 122 | 'use strict' 123 | let from = Number(req.params.from) 124 | let limit = Number(req.params.limit || 0) 125 | if (limit > 50) limit = 50 126 | if (from < 0 || limit < 0) return next() 127 | 128 | let [wClause, sParam] = getSQLClause(limit ? 2 : 1, req.fcResult) 129 | const queryString = `SELECT * FROM user_solutions ${wClause} ORDER BY solution_id DESC LIMIT ${limit ? '$1 OFFSET $2' : 'cal_solution_limit($1)'}` 130 | let result = await db.query(queryString, [...(limit ? [limit, from] : [from]), ...sParam]) 131 | await check_oi_solution(req, result) 132 | await check_acm_solution(req, result) 133 | if (result.rows.length > 0) 134 | return res.ok(result.rows) 135 | return res.sendStatus(204) 136 | }) 137 | 138 | router.get('/contest/:sid(\\d+)/:from(\\d+)?', async (req, res) => { 139 | 'use strict' 140 | // Warning: for tmp limit 10086 141 | let sid = Number(req.params.sid) 142 | let from = Number(req.params.from || 0) 143 | //console.log(sid, from) 144 | const queryString = 'SELECT * FROM user_solutions WHERE contest_id = $1 AND solution_id > $2 ORDER BY solution_id DESC LIMIT 10086' 145 | const result = await db.query(queryString, [sid, from]) 146 | await check_oi_solution(req, result) 147 | await check_acm_solution(req, result) 148 | // console.dir(result); 149 | 150 | if (result.rows.length > 0) 151 | return res.ok(result.rows) 152 | return res.sendStatus(204) 153 | }) 154 | 155 | router.get('/detail/:sid(\\d+)', async (req, res) => { 156 | 'use strict' 157 | 158 | const sid = Number(req.params.sid) 159 | 160 | if (!Number.isInteger(sid)) 161 | res.fail(422) 162 | 163 | const result = await db.query('SELECT * FROM user_solutions WHERE solution_id = $1 LIMIT 1', [sid]) 164 | await check_oi_solution(req, result) 165 | await check_acm_solution(req, result) 166 | if (result.rows.length > 0) { 167 | const row = result.rows[0] 168 | const {ipaddr_id, ...ret} = {...row} 169 | ret.canViewOutput = await check_perm(req, VIEW_OUTPUT_SELF) 170 | if ((req.session.user === row.user_id && await check_perm(req, GET_CODE_SELF)) 171 | || await check_perm(req, GET_CODE_ALL) 172 | || ret.shared) { 173 | const struct = getSolutionStructure(sid) 174 | ret.compile_info = (row.compile_info || '').replace(/\/var\/www\/data\/.+?main.\w+:? ?/g, `main:`) 175 | let langExt = language_ext[row.language] 176 | ret.code = fs.readFileSync(struct.file.code_base + langExt, 'utf8') 177 | } 178 | return res.ok(ret) 179 | } 180 | return res.fail(404) 181 | }) 182 | 183 | router.get('/detail/:sid(\\d+)/case/:i(\\d+)', async (req, res) => { 184 | 'use strict' 185 | 186 | const sid = Number(req.params.sid) 187 | const i = Number(req.params.i) 188 | const isFullData = req.query.all 189 | 190 | if (!Number.isInteger(sid) || !Number.isInteger(i)) 191 | res.fail(422) 192 | 193 | const result = await db.query('SELECT user_id, problem_id,status_id FROM user_solutions WHERE solution_id = $1 LIMIT 1', [sid]) 194 | await check_oi_solution(req, result) 195 | await check_acm_solution(req, result) 196 | if (result.rows.length > 0) { 197 | const uid = result.rows[0].user_id 198 | if (!(req.session.user === uid && await check_perm(req, VIEW_OUTPUT_SELF)) && !await check_perm(req, VIEW_OUTPUT_ALL)) 199 | return res.fail(403) 200 | 201 | const pid = result.rows[0].problem_id 202 | 203 | const ret = {} 204 | 205 | const solution = getSolutionStructure(sid) 206 | const problem = getProblemStructure(pid) 207 | ret.status_id=result.rows[0].status_id 208 | try { 209 | ret.stdin = await (isFullData ? fs.readFileSync(`${problem.path.data}/${i}.in`, 'utf8') : loadPartialData(`${problem.path.data}/${i}.in`)) 210 | ret.stdout = await (isFullData ? fs.readFileSync(`${problem.path.data}/${i}.out`, 'utf8') : loadPartialData(`${problem.path.data}/${i}.out`)) 211 | } catch (e) { 212 | return res.fail(404) 213 | } 214 | try { 215 | ret.execout = await (isFullData ? fs.readFileSync(`${solution.path.exec_out}/${i}.execout`, 'utf8') : loadPartialData(`${solution.path.exec_out}/${i}.execout`)) 216 | } catch (e) { 217 | ret.execout = null 218 | } 219 | return res.ok(ret) 220 | } 221 | return res.fail(404) 222 | }) 223 | 224 | router.get('/share/:type(add|remove)/:sid(\\d+)', async (req, res) => { 225 | 'use strict' 226 | 227 | const sid = Number(req.params.sid) 228 | 229 | if (!Number.isInteger(sid)) 230 | return res.fail(422) 231 | /** 232 | * have to check by multiple query to avoid 233 | * somebody that share a solution submit outside contest page 234 | * to the problem in a running contest. 235 | */ 236 | const pid = await db.query('select problem_id from solutions where solution_id = $1 LIMIT 1', [sid]) 237 | if (pid.rows.length === 0) return res.fail(404) 238 | const endtime = await db.query('select upper(during) as end from contests where contest_id in\ 239 | (select contest_id from contest_problems where problem_id = $1)\ 240 | order by upper(during) desc limit 1;', [pid.rows[0].problem_id]) 241 | if (endtime.rows.length !== 0) { 242 | const now = new Date() 243 | const endTime = new Date(endtime.rows[0].end) 244 | if (now < endTime) { return res.fail(405) } 245 | } 246 | 247 | if (await check_perm(req, GET_CODE_SELF)) { 248 | const result = await db.query('UPDATE solutions SET shared = $1 WHERE solution_id = $2 AND user_id = $3 AND shared <> $1 RETURNING solution_id, shared' 249 | , [req.params.type === 'add', sid, req.session.user]) 250 | if (result.rows.length) return res.ok(result.rows[0]) 251 | } 252 | 253 | return res.fail(404) 254 | }) 255 | 256 | module.exports = router 257 | -------------------------------------------------------------------------------- /config/mailTemplate.js: -------------------------------------------------------------------------------- 1 | module.exports = function mailTemplete (code, link, BASE_URL, hash, to) { 2 | var ret = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 验证邮件 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 111 | 112 | 113 | 114 |
19 | 21 | 22 | 23 | 24 | 42 | 43 | 44 | 45 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 75 | 76 | 77 | 79 | 80 | 81 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 106 | 107 | 108 | 109 |
25 | 26 | 27 | 29 | 34 | 37 | 39 | 40 |
28 | 30 | NKUOJ 33 | 35 | 验证你的邮件 36 | 38 |
41 |
46 | 47 | 48 | 53 | 54 |
49 |

50 | 现在验证你的邮件,开启你在NKUOJ的探索! 51 |

52 |
55 |
60 | code:${code} 61 |
66 | 或者 67 |
72 | 验证邮件地址 74 |
78 |
82 | 注意:验证仅在十分钟内有效。 83 |
87 |
93 | - 94 |
99 | Copyright(c)2018 NKUOJ,All rights reserved. 100 |
104 | Unsubscribe from NKUOJ. 105 |
110 |
115 | 116 | 117 | 118 | ` 119 | return ret 120 | } 121 | -------------------------------------------------------------------------------- /database/functions.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE OR REPLACE FUNCTION hash_password(pwd text) 4 | RETURNS text AS $$ 5 | SELECT md5(current_setting('custom_settings.hash_prefix') || pwd)::text; 6 | $$ LANGUAGE SQL STABLE RETURNS NULL ON NULL INPUT PARALLEL SAFE; 7 | 8 | CREATE OR REPLACE FUNCTION get_ipaddr_id(ip inet) RETURNS integer AS 9 | $$ 10 | BEGIN 11 | INSERT INTO ipaddr(ipaddr) VALUES (ip) ON CONFLICT DO NOTHING; 12 | RETURN (SELECT ipaddr_id FROM ipaddr WHERE ipaddr = ip LIMIT 1); 13 | END; 14 | $$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT ; 15 | 16 | CREATE OR REPLACE FUNCTION cal_perm (who integer) RETURNS jsonb AS $$ 17 | DECLARE 18 | v_return jsonb; 19 | v_user RECORD; 20 | v_groups RECORD; 21 | v_perms RECORD; 22 | BEGIN 23 | v_return := '{}'::jsonb; 24 | SELECT user_role INTO v_user FROM user_info WHERE user_id = who; 25 | FOR v_groups IN (SELECT row_to_json(perm) as perm, negative FROM (SELECT * FROM user_role WHERE role_id = ANY(v_user.user_role)) AS t) ORDER BY negative LOOP 26 | FOR v_perms IN SELECT * FROM json_each_text(v_groups.perm) LOOP 27 | IF (v_groups.negative = 'f') THEN 28 | SELECT v_return 29 | || jsonb_build_object( v_perms.key, COALESCE((v_return->>v_perms.key)::bit, v_perms.value::bit)::bit | v_perms.value::bit ) 30 | INTO v_return; 31 | ELSE 32 | SELECT v_return 33 | || jsonb_build_object( v_perms.key, COALESCE((v_return->>v_perms.key)::bit, v_perms.value::bit)::bit & ~v_perms.value::bit ) 34 | INTO v_return; 35 | END IF; 36 | END LOOP; 37 | END LOOP; 38 | RETURN v_return; 39 | END; 40 | $$ LANGUAGE plpgsql STABLE RETURNS NULL ON NULL INPUT PARALLEL SAFE; 41 | 42 | CREATE OR REPLACE FUNCTION get_report_type_id(msg text) 43 | RETURNS integer AS 44 | $$ 45 | BEGIN 46 | RETURN (SELECT report_type_id FROM report_types WHERE message = msg); 47 | END; 48 | $$ LANGUAGE plpgsql STABLE RETURNS NULL ON NULL INPUT PARALLEL SAFE; 49 | 50 | 51 | CREATE OR REPLACE FUNCTION update_tag_votes() 52 | RETURNS trigger AS 53 | $BODY$ 54 | BEGIN 55 | IF TG_OP='DELETE' THEN 56 | IF OLD.attitude THEN 57 | UPDATE problem_tag_assoc SET positive = positive - 1 WHERE tag_id = OLD.tag_id AND problem_id = OLD.problem_id; 58 | ELSE 59 | UPDATE problem_tag_assoc SET negative = negative - 1 WHERE tag_id = OLD.tag_id AND problem_id = OLD.problem_id; 60 | END IF; 61 | ELSEIF TG_OP='INSERT' THEN 62 | IF NEW.attitude THEN 63 | UPDATE problem_tag_assoc SET positive = positive + 1 WHERE tag_id = NEW.tag_id AND problem_id = NEW.problem_id; 64 | ELSE 65 | UPDATE problem_tag_assoc SET negative = negative + 1 WHERE tag_id = NEW.tag_id AND problem_id = NEW.problem_id; 66 | END IF; 67 | ELSEIF NEW.attitude IS DISTINCT FROM OLD.attitude THEN 68 | IF NEW.attitude THEN 69 | UPDATE problem_tag_assoc SET positive = positive + 1 WHERE tag_id = NEW.tag_id AND problem_id = NEW.problem_id; 70 | UPDATE problem_tag_assoc SET negative = negative - 1 WHERE tag_id = NEW.tag_id AND problem_id = NEW.problem_id; 71 | ELSE 72 | UPDATE problem_tag_assoc SET positive = positive - 1 WHERE tag_id = NEW.tag_id AND problem_id = NEW.problem_id; 73 | UPDATE problem_tag_assoc SET negative = negative + 1 WHERE tag_id = NEW.tag_id AND problem_id = NEW.problem_id; 74 | END IF; 75 | END IF; 76 | IF NOT FOUND THEN 77 | RAISE EXCEPTION 'no data found'; 78 | END IF; 79 | RETURN NEW; 80 | END; 81 | $BODY$ 82 | LANGUAGE plpgsql; 83 | 84 | CREATE OR REPLACE FUNCTION insert_new_user() 85 | RETURNS trigger AS 86 | $BODY$ 87 | DECLARE 88 | v_email_prefix varchar(64); 89 | v_email_suffix varchar(255); 90 | v_user RECORD; 91 | BEGIN 92 | v_email_prefix:= lower(split_part(NEW.email,'@',1)); 93 | v_email_suffix:= lower(split_part(NEW.email,'@',2)); 94 | INSERT INTO email_suffix(email_suffix) VALUES (v_email_suffix) ON CONFLICT DO NOTHING; 95 | INSERT INTO ipaddr(ipaddr) VALUES (NEW.ipaddr) ON CONFLICT DO NOTHING; 96 | WITH a AS ( 97 | INSERT INTO user_nick(nickname) VALUES (NEW.nickname) RETURNING nick_id 98 | ), b AS ( 99 | SELECT suffix_id FROM email_suffix WHERE email_suffix = v_email_suffix LIMIT 1 100 | ), c AS ( 101 | SELECT ipaddr_id FROM ipaddr WHERE ipaddr = NEW.ipaddr LIMIT 1 102 | ), d AS ( 103 | INSERT INTO user_info(nick_id, email, email_suffix_id, user_ip, "password", gender, qq, phone, real_name, school, words) 104 | SELECT a.nick_id, v_email_prefix, b.suffix_id, c.ipaddr_id, hash_password(NEW."password"), COALESCE(NEW.gender, 0), NEW.qq, NEW.phone, NEW.real_name, NEW.school, NEW.words FROM a,b,c RETURNING user_id 105 | ) SELECT a.nick_id as nick_id, d.user_id as user_id FROM a,d INTO v_user; 106 | -- RETURNING * INTO v_user_id 107 | UPDATE user_nick SET user_id = v_user.user_id WHERE user_nick.nick_id = v_user.nick_id; 108 | NEW.user_id := v_user.user_id; 109 | IF NEW.role IS NOT NULL THEN 110 | UPDATE user_info SET user_role = NEW.role WHERE user_id = NEW.user_id; 111 | END IF; 112 | RETURN NEW; 113 | END; 114 | $BODY$ 115 | LANGUAGE plpgsql; 116 | 117 | CREATE OR REPLACE FUNCTION update_existing_user() 118 | RETURNS trigger AS 119 | $BODY$ 120 | DECLARE 121 | v_email_prefix varchar(64); 122 | v_email_suffix varchar(255); 123 | v_record RECORD; 124 | BEGIN 125 | IF NEW.nickname IS NOT NULL AND NEW.nickname IS DISTINCT FROM OLD.nickname THEN 126 | INSERT INTO user_nick(nickname, user_id) VALUES (NEW.nickname, OLD.user_id) ON CONFLICT DO NOTHING; 127 | SELECT nick_id, user_id FROM user_nick WHERE nickname = NEW.nickname INTO v_record; 128 | IF v_record.user_id <> OLD.user_id THEN 129 | RAISE EXCEPTION 'cannot take that name'; 130 | ELSE 131 | UPDATE user_info SET nick_id = v_record.nick_id WHERE user_info.user_id = v_record.user_id; 132 | END IF; 133 | ELSE 134 | NEW.nickname := null; 135 | END IF; 136 | 137 | IF NEW.email IS NOT NULL AND NEW.email IS DISTINCT FROM OLD.email THEN 138 | v_email_prefix:= lower(split_part(NEW.email,'@',1)); 139 | v_email_suffix:= lower(split_part(NEW.email,'@',2)); 140 | IF v_email_prefix = '' OR v_email_suffix = '' THEN 141 | RAISE EXCEPTION 'cannot set that email'; 142 | END IF; 143 | INSERT INTO email_suffix(email_suffix) VALUES (v_email_suffix) ON CONFLICT DO NOTHING; 144 | WITH t AS ( 145 | SELECT suffix_id FROM email_suffix WHERE email_suffix = v_email_suffix 146 | ) UPDATE user_info SET email = v_email_prefix, email_suffix_id = t.suffix_id FROM t WHERE user_id = OLD.user_id; 147 | ELSE 148 | NEW.email := null; 149 | END IF; 150 | 151 | UPDATE user_info SET gender = COALESCE(NEW.gender, gender), qq = COALESCE(NEW.qq, qq), phone = COALESCE(NEW.phone, phone), real_name = COALESCE(NEW.real_name, real_name), school = COALESCE(NEW.school, school), user_role = COALESCE(NEW.role, user_role), current_badge = COALESCE(NEW.current_badge, current_badge), "password" = COALESCE(hash_password(NEW.password), password), words = COALESCE(NEW.words, words), removed = COALESCE(NEW.removed, removed) WHERE user_id = OLD.user_id; 152 | RETURN NEW; 153 | END; 154 | $BODY$ 155 | LANGUAGE plpgsql; 156 | 157 | CREATE OR REPLACE FUNCTION upsert_user_danmaku() RETURNS TRIGGER AS 158 | $$ 159 | BEGIN 160 | INSERT INTO _danmaku(user_id, ipaddr_id, message) VALUES (NEW.user_id, get_ipaddr_id(NEW.ipaddr), NEW.message) 161 | ON conflict(danmaku_id) DO UPDATE SET user_id = NEW.user_id, ipaddr_id = get_ipaddr_id(NEW.ipaddr), message = NEW.message, "when" = DEFAULT; 162 | RETURN NEW; 163 | END; 164 | $$ LANGUAGE plpgsql; 165 | 166 | CREATE OR REPLACE FUNCTION insert_tags(tags text ARRAY) RETURNS integer ARRAY AS 167 | $$ 168 | DECLARE 169 | v_return_arr integer ARRAY; 170 | v_temp integer; 171 | tag text; 172 | BEGIN 173 | v_return_arr = '{}'::integer ARRAY; 174 | FOREACH tag IN ARRAY tags 175 | LOOP 176 | v_temp := NULL; 177 | SELECT tag_id FROM problem_tags WHERE tag_name = tag INTO v_temp; 178 | IF v_temp IS NULL THEN 179 | INSERT INTO problem_tags(tag_name) VALUES (tag) ON CONFLICT DO NOTHING RETURNING tag_id INTO v_temp; 180 | END IF; 181 | IF v_temp IS NOT NULL THEN 182 | v_return_arr = array_append(v_return_arr, v_temp); 183 | END IF; 184 | END LOOP; 185 | RETURN v_return_arr; 186 | END; 187 | $$ LANGUAGE plpgsql; 188 | 189 | CREATE OR REPLACE FUNCTION cal_solution_limit(last_id integer) RETURNS integer AS 190 | $$ 191 | DECLARE 192 | v_value integer; 193 | BEGIN 194 | select last_value - last_id - 1 from solutions_solution_id_seq into v_value; 195 | v_value := CASE WHEN v_value > 150 THEN 150 ELSE v_value END; 196 | RETURN CASE WHEN v_value < 0 THEN 0 ELSE v_value END; 197 | END; 198 | $$ LANGUAGE plpgsql COST 1; 199 | 200 | CREATE OR REPLACE FUNCTION update_problem_sol() RETURNS trigger AS 201 | $$ 202 | BEGIN 203 | IF TG_OP='INSERT' THEN 204 | UPDATE problems SET "all" = "all" + 1 WHERE problem_id = NEW.problem_id; 205 | IF NEW.contest_id IS NOT NULL THEN 206 | UPDATE contest_problems SET submit_all = submit_all + 1 WHERE problem_id = NEW.problem_id AND contest_id = NEW.contest_id; 207 | END IF; 208 | ELSEIF TG_OP='UPDATE' THEN 209 | IF NEW.status_id = 107 AND OLD.status_id != 107 THEN 210 | UPDATE problems SET "ac" = "ac" + 1 WHERE problem_id = NEW.problem_id; 211 | IF NEW.contest_id IS NOT NULL THEN 212 | UPDATE contest_problems SET submit_ac = submit_ac + 1 WHERE problem_id = NEW.problem_id AND contest_id = NEW.contest_id; 213 | END IF; 214 | ELSEIF NEW.status_id != 107 AND OLD.status_id = 107 THEN 215 | UPDATE problems SET "ac" = "ac" - 1 WHERE problem_id = NEW.problem_id; 216 | IF NEW.contest_id IS NOT NULL THEN 217 | UPDATE contest_problems SET submit_ac = submit_ac - 1 WHERE problem_id = NEW.problem_id AND contest_id = NEW.contest_id; 218 | END IF; 219 | END IF; 220 | END IF; 221 | RETURN NEW; 222 | END; 223 | $$ LANGUAGE plpgsql; 224 | 225 | CREATE OR REPLACE FUNCTION update_post_vote() RETURNS trigger AS 226 | $$ 227 | BEGIN 228 | IF TG_OP='DELETE' THEN 229 | IF OLD.attitude THEN 230 | UPDATE post SET positive = positive - 1 WHERE post_id = OLD.post_id; 231 | ELSE 232 | UPDATE post SET negative = negative - 1 WHERE post_id = OLD.post_id; 233 | END IF; 234 | ELSEIF TG_OP='INSERT' THEN 235 | IF NEW.attitude THEN 236 | UPDATE post SET positive = positive + 1 WHERE post_id = NEW.post_id; 237 | ELSE 238 | UPDATE post SET negative = negative + 1 WHERE post_id = NEW.post_id; 239 | END IF; 240 | ELSEIF NEW.attitude IS DISTINCT FROM OLD.attitude THEN 241 | IF NEW.attitude THEN 242 | UPDATE post SET positive = positive + 1 WHERE post_id = NEW.post_id; 243 | UPDATE post SET negative = negative - 1 WHERE post_id = NEW.post_id; 244 | ELSE 245 | UPDATE post SET positive = positive - 1 WHERE post_id = NEW.post_id; 246 | UPDATE post SET negative = negative + 1 WHERE post_id = NEW.post_id; 247 | END IF; 248 | END IF; 249 | IF NOT FOUND THEN 250 | RAISE EXCEPTION 'no data found'; 251 | END IF; 252 | RETURN NEW; 253 | END; 254 | $$ LANGUAGE plpgsql; 255 | 256 | CREATE OR REPLACE FUNCTION update_reply_vote() RETURNS trigger AS 257 | $$ 258 | BEGIN 259 | IF TG_OP='DELETE' THEN 260 | UPDATE post_reply SET score = score - 1 WHERE reply_id = NEW.reply_id; 261 | ELSEIF TG_OP='INSERT' THEN 262 | UPDATE post_reply SET score = score + 1 WHERE reply_id = NEW.reply_id; 263 | END IF; 264 | IF NOT FOUND THEN 265 | RAISE EXCEPTION 'no data found'; 266 | END IF; 267 | RETURN NEW; 268 | END; 269 | $$ LANGUAGE plpgsql; 270 | 271 | CREATE OR REPLACE FUNCTION insert_update_post() RETURNS trigger AS 272 | $$ 273 | BEGIN 274 | IF TG_OP='INSERT' THEN 275 | IF NEW.parent_id IS NOT NULL THEN 276 | UPDATE post SET last_active_date = NEW.since, last_active_user = NEW.user_id WHERE post_id = NEW.parent_id; 277 | END IF; 278 | ELSEIF TG_OP='UPDATE' THEN 279 | IF NEW.removed_date IS NOT NULL THEN 280 | UPDATE post SET removed_date = NEW.removed_date WHERE parent_id = NEW.post_id; 281 | ELSE 282 | UPDATE post SET removed_date = NULL WHERE parent_id = NEW.post_id AND removed_date = OLD.removed_date; 283 | END IF; 284 | IF NEW.closed_date IS NOT NULL THEN 285 | UPDATE post SET closed_date = NEW.closed_date WHERE parent_id = NEW.post_id; 286 | ELSE 287 | UPDATE post SET closed_date = NULL WHERE parent_id = NEW.post_id AND closed_date = OLD.closed_date; 288 | END IF; 289 | END IF; 290 | RETURN NEW; 291 | END; 292 | $$ LANGUAGE plpgsql; 293 | 294 | COMMIT; 295 | -------------------------------------------------------------------------------- /database/structure.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | 3 | CREATE SCHEMA IF NOT EXISTS public; 4 | 5 | CREATE TABLE email_suffix ( 6 | suffix_id serial PRIMARY KEY, 7 | email_suffix varchar(255) UNIQUE NOT NULL -- store lower string only 8 | ); 9 | 10 | CREATE UNIQUE INDEX ON email_suffix(email_suffix); 11 | 12 | CREATE TABLE ipaddr ( 13 | ipaddr_id serial PRIMARY KEY, -- ip address will be handled by a middleware 14 | ipaddr inet UNIQUE NOT NULL, 15 | banned boolean NOT NULL DEFAULT 'f'::boolean 16 | ); 17 | 18 | CREATE UNIQUE INDEX ON ipaddr(ipaddr); 19 | 20 | CREATE TABLE user_nick ( 21 | nick_id serial PRIMARY KEY, 22 | nickname varchar(20) UNIQUE NOT NULL, -- case insensitive 23 | user_id integer, -- create constraint after user table created !!NOTE: recursive reference 24 | since timestamp DEFAULT current_timestamp 25 | ); 26 | 27 | CREATE UNIQUE INDEX lower_nickname_idx ON user_nick ((lower(nickname))); 28 | 29 | CREATE TYPE type_user_perm AS ( 30 | "LOGIN" bit, 31 | "CHANGE_PROFILE" bit, 32 | "CHANGE_AVATAR" bit, 33 | "SUBMIT_CODE" bit, 34 | "GET_CODE_SELF" bit, 35 | "VIEW_OUTPUT_SELF" bit, 36 | "COMMENT" bit, 37 | "POST_NEW_POST" bit, 38 | "REPLY_POST" bit, 39 | "PUBLIC_EDIT" bit, 40 | "ADD_PROBLEM" bit, 41 | "EDIT_PROBLEM_ALL" bit, 42 | "ADD_CONTEST" bit, 43 | "EDIT_CONTEST_ALL" bit, 44 | "REJUDGE_CONTEST_SELF" bit, 45 | "REJUDGE_CONTEST_ALL" bit, 46 | "REJUDGE_ALL" bit, 47 | "BYPASS_STATISTIC_ALL" bit, 48 | "GET_CODE_ALL" bit, 49 | "VIEW_OUTPUT_ALL" bit, 50 | "MANAGE_ROLE" bit, 51 | "SUPER_ADMIN" bit 52 | ); 53 | 54 | CREATE TABLE user_role ( 55 | role_id serial PRIMARY KEY, 56 | title varchar(16) UNIQUE NOT NULL, 57 | description text, 58 | perm type_user_perm NOT NULL, 59 | negative boolean NOT NULL 60 | ); 61 | 62 | CREATE TABLE user_info ( 63 | user_id serial PRIMARY KEY, 64 | nick_id integer NOT NULL REFERENCES user_nick(nick_id), 65 | user_ip integer NOT NULL REFERENCES ipaddr(ipaddr_id), 66 | last_login timestamp DEFAULT current_timestamp, -- store failed tries in redis 67 | password varchar(50), 68 | gender smallint NOT NULL DEFAULT 0 CHECK (gender < 4), -- iso standard 69 | email varchar(64) NOT NULL, -- lower case only 70 | email_suffix_id integer NOT NULL REFERENCES email_suffix(suffix_id), 71 | qq varchar(15), 72 | phone varchar(15), 73 | real_name varchar(20), 74 | school varchar(80), 75 | words varchar(100), 76 | credits integer DEFAULT 0, 77 | submit_ac integer DEFAULT 0, 78 | submit_all integer DEFAULT 0, 79 | user_role integer ARRAY DEFAULT '{1}'::integer ARRAY NOT NULL, 80 | current_badge integer DEFAULT 0, 81 | achievement integer ARRAY, 82 | join_time timestamp DEFAULT current_timestamp, 83 | removed boolean DEFAULT 'f'::boolean, 84 | UNIQUE (email, email_suffix_id) 85 | ); 86 | CREATE UNIQUE INDEX ON user_info(nick_id, "password"); 87 | 88 | CREATE UNIQUE INDEX ON user_info(email, email_suffix_id, "password"); 89 | 90 | ALTER TABLE user_nick ADD FOREIGN KEY (user_id) REFERENCES user_info(user_id) DEFERRABLE INITIALLY DEFERRED; 91 | 92 | CREATE TABLE user_api ( 93 | user_id integer NOT NULL REFERENCES user_info(user_id), 94 | enabled boolean NOT NULL DEFAULT 't'::boolean, 95 | api_name varchar(36), 96 | api_key varchar(32) NOT NULL CHECK(length(api_key)=32), 97 | -- TODO: find a better way or logic 98 | api_hashed varchar(64) NOT NULL CHECK(length(api_hashed)=64), 99 | since timestamp DEFAULT current_timestamp 100 | ); 101 | 102 | CREATE UNIQUE INDEX ON user_api (api_key); 103 | 104 | 105 | CREATE TABLE problem_tags ( 106 | tag_id serial PRIMARY KEY, 107 | tag_name text UNIQUE NOT NULL CHECK(length(tag_name) > 1) 108 | ); 109 | 110 | CREATE UNIQUE INDEX ON problem_tags(tag_name); 111 | 112 | CREATE TYPE type_problem_restriction AS ( 113 | "NO_VIEW_BEFORE_START" bit, 114 | "NO_RESULT_BEFORE_END_SELF" bit, 115 | "NO_RESULT_BEFORE_END_OTHERS" bit, 116 | "NO_VIEW_BEFORE_END" bit, 117 | "NO_RESULT_BEFORE_END_ALL" bit 118 | ); 119 | 120 | CREATE TABLE contests ( 121 | contest_id serial PRIMARY KEY, 122 | title varchar(255) NOT NULL, 123 | description text, 124 | enabled boolean NOT NULL DEFAULT 't'::boolean, 125 | during tsrange NOT NULL, 126 | perm type_problem_restriction NOT NULL DEFAULT ('1', '0', '0', '0', '0'), 127 | private boolean NOT NULL DEFAULT 'f'::boolean, 128 | rule text DEFAULT 'acm' 129 | ); 130 | 131 | CREATE TABLE problems ( 132 | problem_id serial PRIMARY KEY, 133 | title varchar(255) NOT NULL, -- content stores in file (use svn) 134 | "ac" integer NOT NULL DEFAULT 0, 135 | "all" integer NOT NULL DEFAULT 0, 136 | contest_id integer REFERENCES contests(contest_id), 137 | special_judge boolean NOT NULL DEFAULT 'f'::boolean, 138 | detail_judge boolean NOT NULL DEFAULT 't'::boolean, 139 | cases integer NOT NULL DEFAULT 1, 140 | time_limit integer NOT NULL DEFAULT 1000, 141 | memory_limit integer NOT NULL DEFAULT 10000, 142 | "level" integer 143 | ); 144 | 145 | 146 | CREATE INDEX ON problems(title); 147 | CREATE INDEX ON problems(contest_id); 148 | 149 | CREATE TABLE problem_tag_assoc ( 150 | tag_id integer references problem_tags(tag_id), 151 | problem_id integer references problems(problem_id), 152 | official boolean not null default 'f'::boolean, 153 | positive integer not null default 0, 154 | negative integer not null default 0, 155 | primary key (tag_id, problem_id) 156 | ); 157 | 158 | CREATE TABLE problem_tag_votes ( 159 | user_id integer references user_info(user_id), 160 | tag_id integer references problem_tags(tag_id), 161 | problem_id integer references problems(problem_id), 162 | attitude boolean not null, 163 | primary key (user_id, tag_id, problem_id) 164 | ); 165 | 166 | CREATE TABLE contest_problems ( 167 | contest_id integer NOT NULL REFERENCES contests(contest_id), 168 | problem_id integer NOT NULL REFERENCES problems(problem_id), 169 | submit_ac integer NOT NULL DEFAULT 0, 170 | submit_all integer NOT NULL DEFAULT 0, 171 | PRIMARY KEY(contest_id, problem_id) 172 | ); 173 | 174 | CREATE TABLE contest_users ( 175 | contest_id integer NOT NULL REFERENCES contests(contest_id), 176 | user_id integer NOT NULL REFERENCES user_info(user_id), 177 | status jsonb NOT NULL DEFAULT '{}'::jsonb, -- {"pid" : {"a": 1, "b": 2} } 178 | PRIMARY KEY(contest_id, user_id) 179 | ); 180 | 181 | CREATE TABLE messages ( 182 | message_id serial PRIMARY KEY, 183 | a integer REFERENCES user_info(user_id), 184 | b integer REFERENCES user_info(user_id), 185 | title varchar(60), 186 | content text, 187 | since timestamp DEFAULT current_timestamp, 188 | deleted_a boolean NOT NULL DEFAULT 'f'::boolean, 189 | deleted_b boolean NOT NULL DEFAULT 'f'::boolean, 190 | CHECK (a <> b) 191 | ); 192 | 193 | CREATE INDEX ON messages(a, b); 194 | 195 | CREATE TABLE solution_status ( 196 | status_id serial PRIMARY KEY, 197 | msg_short varchar(10) NOT NULL, 198 | msg_cn varchar(25) NOT NULL, 199 | msg_en varchar(40) NOT NULL 200 | ); 201 | 202 | CREATE TABLE solutions ( 203 | solution_id serial PRIMARY KEY, 204 | user_id integer NOT NULL REFERENCES user_info(user_id), 205 | problem_id integer NOT NULL REFERENCES problems(problem_id), 206 | status_id integer NOT NULL REFERENCES solution_status, 207 | contest_id integer REFERENCES contests(contest_id), 208 | language integer, 209 | code_size integer, 210 | shared boolean NOT NULL DEFAULT 'f'::boolean, 211 | "time" integer, 212 | "memory" integer, 213 | "when" timestamp DEFAULT current_timestamp, 214 | ipaddr_id integer REFERENCES ipaddr(ipaddr_id), 215 | score integer DEFAULT 0 216 | ); 217 | 218 | CREATE TABLE post ( 219 | post_id serial PRIMARY KEY, 220 | parent_id integer DEFAULT NULL REFERENCES post(post_id), 221 | user_id integer NOT NULL REFERENCES user_info(user_id), 222 | nickname varchar(20), 223 | title text, 224 | content text, 225 | problem_id integer REFERENCES problems(problem_id), 226 | since timestamp DEFAULT current_timestamp, 227 | last_edit_date timestamp DEFAULT NULL, 228 | last_editor_id integer REFERENCES user_info(user_id), 229 | last_active_date timestamp DEFAULT current_timestamp, 230 | last_active_user integer REFERENCES user_info(user_id), 231 | closed_date timestamp, 232 | removed_date timestamp, 233 | positive integer NOT NULL DEFAULT 0, 234 | negative integer NOT NULL DEFAULT 0, 235 | ipaddr_id integer REFERENCES ipaddr(ipaddr_id) 236 | ); 237 | 238 | CREATE TABLE post_reply ( 239 | reply_id serial PRIMARY KEY, 240 | reply_to integer NOT NULL REFERENCES post(post_id), 241 | user_id integer NOT NULL REFERENCES user_info(user_id), 242 | nickname varchar(20), 243 | score integer NOT NULL DEFAULT 0, 244 | removed_date timestamp, 245 | since timestamp DEFAULT current_timestamp, 246 | content varchar(600), 247 | ipaddr_id integer REFERENCES ipaddr(ipaddr_id) 248 | ); 249 | 250 | CREATE UNLOGGED TABLE post_vote ( 251 | post_id integer NOT NULL REFERENCES post(post_id), 252 | user_id integer NOT NULL REFERENCES user_info(user_id), 253 | attitude boolean NOT NULL, 254 | since timestamp DEFAULT current_timestamp, 255 | primary key(post_id, user_id) 256 | ); 257 | 258 | CREATE UNLOGGED TABLE reply_vote ( 259 | reply_id integer NOT NULL REFERENCES post(post_id), 260 | user_id integer NOT NULL REFERENCES user_info(user_id), 261 | since timestamp DEFAULT current_timestamp, 262 | primary key(reply_id, user_id) 263 | ); 264 | 265 | CREATE TABLE reports ( 266 | report_id serial PRIMARY KEY, 267 | reporter integer NOT NULL REFERENCES user_info(user_id), 268 | reportee integer NOT NULL REFERENCES user_info(user_id), 269 | type integer NOT NULL, -- comment, post, message, avatar, info 270 | which integer, 271 | handler integer REFERENCES user_info(user_id), 272 | result boolean, 273 | "when" timestamp DEFAULT current_timestamp, 274 | UNIQUE(reporter, reportee, type, which) 275 | ); 276 | 277 | CREATE TABLE report_types ( 278 | report_type_id integer PRIMARY KEY, 279 | message text UNIQUE NOT NULL 280 | ); 281 | 282 | CREATE INDEX ON report_types(message); 283 | 284 | CREATE TABLE user_blocks ( 285 | blocker serial NOT NULL, 286 | blockee integer NOT NULL REFERENCES user_info(user_id), 287 | since timestamp DEFAULT current_timestamp, 288 | PRIMARY KEY(blocker, blockee), 289 | CHECK (blocker <> blockee) 290 | ); 291 | 292 | CREATE UNLOGGED TABLE _danmaku ( 293 | danmaku_id serial primary key, 294 | user_id integer, 295 | ipaddr_id integer, 296 | message text, 297 | "when" timestamp default current_timestamp 298 | ); 299 | 300 | CREATE TABLE users_nkpc ( 301 | user_id integer NOT NULL REFERENCES user_info(user_id), 302 | real_name text, 303 | student_number text, 304 | gender text, 305 | institute text, 306 | qq text, 307 | phone text 308 | ); 309 | 310 | CREATE TABLE secret_time( 311 | during tsrange NOT NULL 312 | ); 313 | COMMIT; 314 | --------------------------------------------------------------------------------