├── migrations ├── .keep ├── 2024-11-05-141321_add-nickname │ ├── up.sql │ └── down.sql ├── 2024-11-26-172757_use-session │ ├── up.sql │ └── down.sql ├── 2024-11-13-145509_add-user-random │ ├── down.sql │ └── up.sql ├── 2024-09-28-125657_init │ ├── down.sql │ └── up.sql ├── 2024-11-05-021926_add-points │ ├── down.sql │ └── up.sql ├── 2024-11-07-094130_unique-username │ ├── up.sql │ └── down.sql └── 2024-11-07-111455_add-difficulty │ ├── down.sql │ └── up.sql ├── src ├── core │ └── mod.rs ├── utils │ ├── mod.rs │ ├── webcolor.rs │ ├── jinja.rs │ ├── query.rs │ ├── dynfmt.rs │ ├── responder.rs │ ├── script.rs │ └── fsext.rs ├── db │ ├── query │ │ ├── mod.rs │ │ ├── score.rs │ │ ├── problemset.rs │ │ ├── difficulty.rs │ │ ├── submission.rs │ │ ├── user.rs │ │ ├── challenge.rs │ │ ├── artifact.rs │ │ └── solved.rs │ ├── mod.rs │ ├── types.rs │ ├── schema.rs │ └── models.rs ├── functions │ ├── mod.rs │ ├── event.rs │ └── user.rs ├── pages │ ├── error.rs │ ├── core │ │ ├── root.rs │ │ ├── mod.rs │ │ ├── scoreboard.rs │ │ └── user.rs │ ├── admin │ │ ├── root.rs │ │ ├── mod.rs │ │ ├── submission.rs │ │ ├── problemset.rs │ │ ├── artifact.rs │ │ ├── difficulty.rs │ │ └── user.rs │ └── mod.rs ├── main.rs ├── configs │ ├── activity.rs │ ├── mod.rs │ ├── user.rs │ ├── event.rs │ └── challenge.rs └── activity │ ├── challenge.rs │ └── mod.rs ├── Rocket.toml ├── assets ├── 1.webp ├── 2.webp ├── 3.webp └── 4.webp ├── static ├── images │ └── avatar.webp ├── css │ └── markdown.css ├── icons │ ├── minus-solid.svg │ ├── plus-solid.svg │ ├── message-regular.svg │ ├── upload-solid.svg │ ├── file-lines-regular.svg │ ├── eye-solid.svg │ └── pen-to-square-regular.svg └── js │ ├── markdown.js │ └── name-renderer.js ├── templates ├── functions │ ├── points.html.j2 │ └── time.html.j2 ├── core │ ├── error.html.j2 │ ├── components │ │ ├── progress.html.j2 │ │ └── navbar.html.j2 │ ├── base.html.j2 │ ├── user │ │ ├── login.html.j2 │ │ ├── register.html.j2 │ │ ├── edit.html.j2 │ │ └── index.html.j2 │ ├── index.html.j2 │ ├── challenge │ │ ├── index.html.j2 │ │ └── detail.html.j2 │ └── scoreboard │ │ └── index.html.j2 ├── admin │ ├── error.html.j2 │ ├── index.html.j2 │ ├── base.html.j2 │ ├── problemset │ │ ├── new.html.j2 │ │ ├── index.html.j2 │ │ └── edit.html.j2 │ ├── difficulty │ │ ├── new.html.j2 │ │ ├── index.html.j2 │ │ └── edit.html.j2 │ ├── artifact │ │ ├── index.html.j2 │ │ └── detail.html.j2 │ ├── components │ │ └── navbar.html.j2 │ ├── user │ │ ├── index.html.j2 │ │ └── edit.html.j2 │ ├── challenge │ │ ├── publish.html.j2 │ │ ├── index.html.j2 │ │ ├── edit.html.j2 │ │ ├── detail.html.j2 │ │ └── new.html.j2 │ └── submission │ │ └── index.html.j2 ├── components │ └── flash.html.j2 ├── error.html.j2 └── base.html.j2 ├── examples ├── challenges │ ├── README.md │ ├── docker │ │ ├── build.yml │ │ └── Dockerfile │ └── binary │ │ ├── build.yml │ │ └── challenge.c ├── configs │ ├── activity.yml │ ├── user.yml │ ├── event.yml │ └── challenge.yml ├── activity │ └── simple.koto └── dynpoints │ └── simple.koto ├── .gitignore ├── Cargo.toml ├── README.md └── LICENSE /migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod conductor; 2 | -------------------------------------------------------------------------------- /Rocket.toml: -------------------------------------------------------------------------------- 1 | [global.databases] 2 | database = { url = "db.sqlite" } 3 | -------------------------------------------------------------------------------- /assets/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricky8955555/attackr/HEAD/assets/1.webp -------------------------------------------------------------------------------- /assets/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricky8955555/attackr/HEAD/assets/2.webp -------------------------------------------------------------------------------- /assets/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricky8955555/attackr/HEAD/assets/3.webp -------------------------------------------------------------------------------- /assets/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricky8955555/attackr/HEAD/assets/4.webp -------------------------------------------------------------------------------- /static/images/avatar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricky8955555/attackr/HEAD/static/images/avatar.webp -------------------------------------------------------------------------------- /templates/functions/points.html.j2: -------------------------------------------------------------------------------- 1 | {% macro display(points) %} 2 | {{ points | round(1) }} 3 | {% endmacro %} -------------------------------------------------------------------------------- /templates/functions/time.html.j2: -------------------------------------------------------------------------------- 1 | {% macro display(time) %} 2 | {{ time | split(".") | first }} 3 | {% endmacro %} -------------------------------------------------------------------------------- /migrations/2024-11-05-141321_add-nickname/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | ALTER TABLE "users" ADD "nickname" TEXT; 4 | -------------------------------------------------------------------------------- /examples/challenges/README.md: -------------------------------------------------------------------------------- 1 | # 题目源代码示例 2 | 3 | 此目录下提供了关于题目源代码的相关示例,分别为: 4 | 5 | - `binary`: 二进制题目构建示例 6 | - `docker`: Docker 题目构建示例 7 | -------------------------------------------------------------------------------- /examples/configs/activity.yml: -------------------------------------------------------------------------------- 1 | scripts: 2 | - path: activity/simple.koto # 脚本路径 3 | 4 | kinds: # 监听事件 5 | - Solved # 监听解题通过事件 6 | -------------------------------------------------------------------------------- /migrations/2024-11-26-172757_use-session/up.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | ALTER TABLE "users" DROP COLUMN "random"; 4 | -------------------------------------------------------------------------------- /examples/configs/user.yml: -------------------------------------------------------------------------------- 1 | session: 2 | expiry: # Session 有效期 3 | secs: 3600 4 | nanos: 0 5 | 6 | no_verify: true # 值为 true 时表示注册不需要审核 7 | -------------------------------------------------------------------------------- /migrations/2024-11-05-141321_add-nickname/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | ALTER TABLE "users" DROP COLUMN "nickname"; 4 | -------------------------------------------------------------------------------- /migrations/2024-11-13-145509_add-user-random/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | ALTER TABLE "users" DROP COLUMN "random"; 4 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dynfmt; 2 | pub mod fsext; 3 | pub mod jinja; 4 | pub mod query; 5 | pub mod responder; 6 | pub mod script; 7 | pub mod webcolor; 8 | -------------------------------------------------------------------------------- /templates/core/error.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "core/base" %} 2 | 3 | {% block content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/error.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block content %} 4 | 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block content %} 4 |
5 |

欢迎来调教我,我的 Master

6 |
7 | {% endblock %} -------------------------------------------------------------------------------- /src/db/query/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod artifact; 2 | pub mod challenge; 3 | pub mod difficulty; 4 | pub mod problemset; 5 | pub mod score; 6 | pub mod solved; 7 | pub mod submission; 8 | pub mod user; 9 | -------------------------------------------------------------------------------- /examples/activity/simple.koto: -------------------------------------------------------------------------------- 1 | export solved = |user, challenge, problemset, solved, rank| # 解题通过事件 2 | message = '恭喜 {user.username} 获得 {problemset.name} {challenge.name} 的第 {rank} 名' 3 | print message 4 | -------------------------------------------------------------------------------- /examples/configs/event.yml: -------------------------------------------------------------------------------- 1 | name: attackr # 比赛名称 2 | description: A platform for ctfer # 比赛介绍 3 | 4 | timezone: +08:00 # 时区 (默认为 UTC) 5 | 6 | start_at: 2024-11-11 11:45:14.0 # 开始时间 7 | end_at: 2025-01-09 19:08:10.0 # 结束时间 8 | -------------------------------------------------------------------------------- /templates/components/flash.html.j2: -------------------------------------------------------------------------------- 1 | {% if flash and flash.message %} 2 | 5 | {% endif %} -------------------------------------------------------------------------------- /templates/core/components/progress.html.j2: -------------------------------------------------------------------------------- 1 | {% macro show_progress(passed) %} 2 | {% if passed %} 3 | ✓ 已通过 4 | {% else %} 5 | ✗ 未通过 6 | {% endif %} 7 | {% endmacro %} -------------------------------------------------------------------------------- /migrations/2024-09-28-125657_init/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE "users"; 4 | DROP TABLE "problemsets"; 5 | DROP TABLE "challenges"; 6 | DROP TABLE "artifacts"; 7 | DROP TABLE "submissions"; 8 | DROP TABLE "solved"; 9 | -------------------------------------------------------------------------------- /examples/challenges/docker/build.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - type: Docker 3 | 4 | path: . # 作为 Docker 构建的根目录 (即 Dockerfile 所在目录) 5 | 6 | config: 7 | exposed: # 暴露端口 8 | - 1337/tcp 9 | 10 | # 此处无需配置 Docker 镜像产物,会在 steps 中自动推断出产物 11 | artifacts: [] 12 | -------------------------------------------------------------------------------- /src/functions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod challenge; 2 | pub mod event; 3 | pub mod user; 4 | 5 | use rocket::fairing::AdHoc; 6 | 7 | pub fn stage() -> AdHoc { 8 | AdHoc::on_ignite("Functions", |rocket| async { 9 | rocket.attach(challenge::stage()).attach(user::stage()) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /examples/challenges/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk add --update --no-cache socat 4 | 5 | # 将与写入 Flag 相关的操作放在最后面 6 | ARG ATTACKR_FLAG 7 | RUN echo $ATTACKR_FLAG > /flag 8 | 9 | EXPOSE 1337 10 | 11 | ENTRYPOINT ["socat", "tcp-l:1337,reuseaddr,fork", "exec:/bin/sh,pty,ctty,setsid,stderr,echo=0"] 12 | -------------------------------------------------------------------------------- /examples/challenges/binary/build.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - type: Cmd 3 | 4 | image: buildpack-deps:bookworm # 用于进行构建操作的 Docker 镜像 5 | 6 | cmds: # 构建指令 7 | - sed -i 's_flag{}_'"$ATTACKR_FLAG"'_' challenge.c 8 | - gcc -ochallenge challenge.c 9 | 10 | artifacts: 11 | - type: Binary 12 | path: challenge # 产物路径 13 | -------------------------------------------------------------------------------- /examples/dynpoints/simple.koto: -------------------------------------------------------------------------------- 1 | export calculate_points = |raw, solved| 2 | minimum = 1.max (raw * 0.1) 3 | step = raw * 0.1 4 | result = raw - (solved * step) 5 | return result.max minimum 6 | 7 | export calculate_factor = |raw, solved| 8 | match solved 9 | 0 then 1.05 10 | 1 then 1.03 11 | 2 then 1.01 12 | else 1 13 | -------------------------------------------------------------------------------- /static/css/markdown.css: -------------------------------------------------------------------------------- 1 | .md-block strong { 2 | color: brown; 3 | } 4 | 5 | .md-block em { 6 | color: deepskyblue; 7 | } 8 | 9 | .md-block del { 10 | color: gray; 11 | } 12 | 13 | .md-block blockquote { 14 | border-left: 3px solid violet; 15 | color: darkviolet; 16 | padding-left: 0.75rem; 17 | margin: 1rem 0; 18 | } -------------------------------------------------------------------------------- /static/icons/minus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/js/markdown.js: -------------------------------------------------------------------------------- 1 | import { marked } from 'https://cdn.jsdelivr.net/npm/marked/+esm' 2 | import DOMPurify from 'https://cdn.jsdelivr.net/npm/dompurify@3/+esm' 3 | 4 | export const renderMarkdown = element => { 5 | const content = element.innerText; 6 | const html = DOMPurify.sanitize(marked.parse(content)); 7 | element.innerHTML = html; 8 | }; 9 | -------------------------------------------------------------------------------- /migrations/2024-11-05-021926_add-points/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | DROP TABLE "scores"; 4 | DROP TABLE "solved"; 5 | 6 | CREATE TABLE "solved" ( 7 | "id" INTEGER, 8 | "submission" INTEGER NOT NULL, 9 | "factor" REAL NOT NULL, 10 | PRIMARY KEY("id"), 11 | FOREIGN KEY("submission") REFERENCES "submissions"("id") ON DELETE CASCADE 12 | ); 13 | -------------------------------------------------------------------------------- /src/utils/webcolor.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | 3 | pub fn parse_webcolor(expr: &str) -> Result { 4 | let hex = expr 5 | .strip_prefix('#') 6 | .ok_or_else(|| anyhow!("invalid webcolor format."))?; 7 | 8 | if hex.len() != 6 { 9 | bail!("invalid webcolor format."); 10 | } 11 | 12 | Ok(u32::from_str_radix(hex, 16)?) 13 | } 14 | -------------------------------------------------------------------------------- /templates/admin/base.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 |
4 | {% include "admin/components/navbar" %} 5 | 6 | {% block header %} 7 | {% endblock %} 8 |
9 | 10 |
11 |
12 | {% include "components/flash" %} 13 | 14 | {% block content %} 15 | {% endblock %} 16 |
17 |
18 | {% endblock %} -------------------------------------------------------------------------------- /templates/core/base.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "base" %} 2 | {% block body %} 3 |
4 | {% include "core/components/navbar" %} 5 | 6 | {% block header %} 7 | {% endblock %} 8 |
9 | 10 |
11 |
12 | {% include "components/flash" %} 13 | 14 | {% block content %} 15 | {% endblock %} 16 |
17 |
18 | {% endblock %} -------------------------------------------------------------------------------- /src/pages/error.rs: -------------------------------------------------------------------------------- 1 | use rocket::{fairing::AdHoc, http::Status, Request}; 2 | use rocket_dyn_templates::{context, Template}; 3 | 4 | #[catch(default)] 5 | fn error_handler(status: Status, _: &Request) -> Template { 6 | Template::render("error", context! {status}) 7 | } 8 | 9 | pub fn stage() -> AdHoc { 10 | AdHoc::on_ignite("Pages - Error", |rocket| async { 11 | rocket.register("/", catchers![error_handler]) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /static/icons/plus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/challenges/binary/challenge.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | const char flag[] = "flag{}"; 5 | 6 | int main() { 7 | char buf[50]; 8 | puts("please type your flag:"); 9 | fgets(buf, sizeof(buf), stdin); 10 | buf[strcspn(buf, "\r\n")] = 0; // remove newline char 11 | if (strcmp(flag, buf) == 0) { 12 | puts("cheers! you've got the flag!"); 13 | } else { 14 | puts("oh no, that's not correct!"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/admin/problemset/new.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

添加题集

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 名称 11 | 12 |
13 | 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /migrations/2024-11-07-094130_unique-username/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE "new_users" ( 4 | "id" INTEGER, 5 | "username" TEXT NOT NULL UNIQUE, 6 | "password" TEXT NOT NULL, 7 | "email" TEXT NOT NULL, 8 | "contact" TEXT NOT NULL, 9 | "enabled" BOOLEAN NOT NULL, 10 | "role" TEXT NOT NULL, 11 | "nickname" TEXT, 12 | PRIMARY KEY("id") 13 | ); 14 | 15 | INSERT INTO "new_users" SELECT * FROM "users"; 16 | DROP TABLE "users"; 17 | ALTER TABLE "new_users" RENAME TO "users"; 18 | -------------------------------------------------------------------------------- /src/utils/jinja.rs: -------------------------------------------------------------------------------- 1 | use rocket_dyn_templates::minijinja::{Error, ErrorKind, Value}; 2 | 3 | pub fn sum(value: Value) -> Result { 4 | let iter = value.try_iter().map_err(|err| { 5 | Error::new(ErrorKind::InvalidOperation, "cannot convert value to list").with_source(err) 6 | })?; 7 | 8 | Ok(iter 9 | .map(f64::try_from) 10 | .collect::, _>>() 11 | .map(|x| Value::from(x.into_iter().sum::())) 12 | .unwrap_or(Value::UNDEFINED)) 13 | } 14 | -------------------------------------------------------------------------------- /migrations/2024-11-07-094130_unique-username/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | CREATE TABLE "new_users" ( 4 | "id" INTEGER, 5 | "username" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "email" TEXT NOT NULL, 8 | "contact" TEXT NOT NULL, 9 | "enabled" BOOLEAN NOT NULL, 10 | "role" TEXT NOT NULL, 11 | "nickname" TEXT, 12 | PRIMARY KEY("id") 13 | ); 14 | 15 | INSERT INTO "new_users" SELECT * FROM "users"; 16 | DROP TABLE "users"; 17 | ALTER TABLE "new_users" RENAME TO "users"; 18 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rocket; 3 | 4 | #[cfg(feature = "activity")] 5 | mod activity; 6 | mod configs; 7 | mod core; 8 | mod db; 9 | mod functions; 10 | mod pages; 11 | mod utils; 12 | 13 | use rocket::fs::{FileServer, Options as FsOptions}; 14 | 15 | #[launch] 16 | fn rocket() -> _ { 17 | rocket::build() 18 | .attach(db::stage()) 19 | .attach(functions::stage()) 20 | .attach(pages::stage()) 21 | .mount("/static", FileServer::new("static", FsOptions::None)) 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/query.rs: -------------------------------------------------------------------------------- 1 | use diesel::QueryResult; 2 | 3 | pub trait QueryResultExt { 4 | fn some(self) -> QueryResult>; 5 | } 6 | 7 | impl QueryResultExt for QueryResult { 8 | fn some(self) -> QueryResult> { 9 | match self { 10 | Ok(val) => Ok(Some(val)), 11 | Err(err) => { 12 | if err == diesel::NotFound { 13 | return Ok(None); 14 | } 15 | Err(err) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/configs/activity.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use validator::Validate; 5 | 6 | use crate::activity::ActivityKind; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct Script { 10 | pub path: String, 11 | pub kinds: Vec, 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize, Validate)] 15 | pub struct Config { 16 | #[serde(default)] 17 | pub scripts: Vec 19 | 20 | 33 | {% block script %} 34 | {% endblock %} 35 | 36 | 37 | -------------------------------------------------------------------------------- /templates/admin/problemset/edit.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

修改题集信息

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 名称 11 | 12 |
13 | 14 | 15 |
16 | 17 | 37 | {% endblock %} -------------------------------------------------------------------------------- /templates/core/user/register.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "core/base" %} 2 | 3 | {% block header %} 4 |

注册

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 用户名 11 | 13 |
14 |
15 | 密码 16 | 17 |
18 |
19 | 昵称 20 | 21 |
22 |
23 | 联系方式 24 | 25 |
26 |
27 | Email 28 | 29 |
30 | 31 | 登录 32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /templates/core/user/edit.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "core/base" %} 2 | 3 | {% block header %} 4 |

修改信息

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 用户名 11 | 13 |
14 |
15 | 密码 16 | 17 |
18 |
19 | 昵称 20 | 22 |
23 |
24 | 联系方式 25 | 26 |
27 |
28 | Email 29 | 30 |
31 | 32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/components/navbar.html.j2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/admin/difficulty/edit.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

修改难度信息

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 名称 11 | 12 |
13 |
14 | 颜色 15 | 16 |
17 | 18 | 19 |
20 | 21 | 41 | {% endblock %} -------------------------------------------------------------------------------- /src/utils/dynfmt.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | 3 | #[derive(Debug, Eq, PartialEq)] 4 | enum State { 5 | None, 6 | OpeningBrace, 7 | ClosingBrace, 8 | } 9 | 10 | pub fn format(fmt: &str, args: &[&str]) -> Result { 11 | let mut result = String::new(); 12 | let mut state = State::None; 13 | let mut arg_iter = args.iter(); 14 | 15 | for c in fmt.chars() { 16 | match state { 17 | State::None => match c { 18 | '{' => state = State::OpeningBrace, 19 | '}' => state = State::ClosingBrace, 20 | _ => result.push(c), 21 | }, 22 | State::OpeningBrace => match c { 23 | '{' => { 24 | state = State::None; 25 | result.push('{'); 26 | } 27 | '}' => { 28 | state = State::None; 29 | result.push_str( 30 | arg_iter 31 | .next() 32 | .ok_or_else(|| anyhow!("unmatched number of arguments."))?, 33 | ); 34 | } 35 | _ => bail!("unmatched brace."), 36 | }, 37 | State::ClosingBrace => match c { 38 | '}' => { 39 | state = State::None; 40 | result.push('}'); 41 | } 42 | _ => bail!("unmatched brace."), 43 | }, 44 | } 45 | } 46 | 47 | if state != State::None { 48 | bail!("unmatched brace."); 49 | } 50 | 51 | if arg_iter.next().is_some() { 52 | bail!("unmatched number of arguments."); 53 | } 54 | 55 | Ok(result) 56 | } 57 | -------------------------------------------------------------------------------- /templates/admin/user/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

用户列表

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% for enabled, users in users | groupby("enabled") | sort %} 9 |
10 | {% if not enabled %} 11 | 未启用用户 12 | {% else %} 13 | 普通用户 14 | {% endif %} 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for user in users %} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | {% endfor %} 50 | 51 |
ID用户名昵称联系方式Email权限组启用操作
{{ user.id }}{{ user.username }}{{ user.nickname }}{{ user.contact }}{{ user.email }}{{ user.role }}{{ user.enabled }} 41 | 42 | 43 | 44 | 45 | 46 | 47 |
52 |
53 | {% endfor %} 54 | {% endblock %} -------------------------------------------------------------------------------- /src/db/query/problemset.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use anyhow::Result as AnyResult; 4 | use diesel::QueryResult; 5 | use validator::Validate; 6 | 7 | use crate::db::{models::Problemset, schema::problemsets, Db}; 8 | 9 | pub async fn add_problemset(db: &Db, problemset: Problemset) -> AnyResult { 10 | problemset.validate()?; 11 | 12 | Ok(db 13 | .run(move |conn| { 14 | diesel::insert_into(problemsets::table) 15 | .values(&problemset) 16 | .returning(problemsets::id) 17 | .get_result(conn) 18 | }) 19 | .await 20 | .map(|id: Option| id.expect("returning guarantees id present"))?) 21 | } 22 | 23 | pub async fn update_problemset(db: &Db, problemset: Problemset) -> AnyResult<()> { 24 | problemset.validate()?; 25 | 26 | db.run(move |conn| { 27 | diesel::update(problemsets::table.filter(problemsets::id.eq(problemset.id))) 28 | .set(&problemset) 29 | .execute(conn) 30 | }) 31 | .await?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub async fn get_problemset(db: &Db, id: i32) -> QueryResult { 37 | db.run(move |conn| { 38 | problemsets::table 39 | .filter(problemsets::id.eq(id)) 40 | .first(conn) 41 | }) 42 | .await 43 | } 44 | 45 | pub async fn list_problemsets(db: &Db) -> QueryResult> { 46 | db.run(move |conn| problemsets::table.load(conn)).await 47 | } 48 | 49 | pub async fn delete_problemset(db: &Db, id: i32) -> QueryResult<()> { 50 | db.run(move |conn| { 51 | diesel::delete(problemsets::table) 52 | .filter(problemsets::id.eq(id)) 53 | .execute(conn) 54 | }) 55 | .await?; 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/db/query/difficulty.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use anyhow::Result as AnyResult; 4 | use diesel::QueryResult; 5 | use validator::Validate; 6 | 7 | use crate::db::{models::Difficulty, schema::difficulties, Db}; 8 | 9 | pub async fn add_difficulty(db: &Db, difficulty: Difficulty) -> AnyResult { 10 | difficulty.validate()?; 11 | 12 | Ok(db 13 | .run(move |conn| { 14 | diesel::insert_into(difficulties::table) 15 | .values(&difficulty) 16 | .returning(difficulties::id) 17 | .get_result(conn) 18 | }) 19 | .await 20 | .map(|id: Option| id.expect("returning guarantees id present"))?) 21 | } 22 | 23 | pub async fn update_difficulty(db: &Db, difficulty: Difficulty) -> AnyResult<()> { 24 | difficulty.validate()?; 25 | 26 | db.run(move |conn| { 27 | diesel::update(difficulties::table.filter(difficulties::id.eq(difficulty.id))) 28 | .set(&difficulty) 29 | .execute(conn) 30 | }) 31 | .await?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub async fn get_difficulty(db: &Db, id: i32) -> QueryResult { 37 | db.run(move |conn| { 38 | difficulties::table 39 | .filter(difficulties::id.eq(id)) 40 | .first(conn) 41 | }) 42 | .await 43 | } 44 | 45 | pub async fn list_difficulties(db: &Db) -> QueryResult> { 46 | db.run(move |conn| difficulties::table.load(conn)).await 47 | } 48 | 49 | pub async fn delete_difficulty(db: &Db, id: i32) -> QueryResult<()> { 50 | db.run(move |conn| { 51 | diesel::delete(difficulties::table) 52 | .filter(difficulties::id.eq(id)) 53 | .execute(conn) 54 | }) 55 | .await?; 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/db/query/submission.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use anyhow::Result as AnyResult; 4 | use validator::Validate; 5 | 6 | use crate::db::{models::Submission, schema::submissions, Db}; 7 | 8 | pub async fn add_submission(db: &Db, submission: Submission) -> AnyResult { 9 | submission.validate()?; 10 | 11 | Ok(db 12 | .run(move |conn| { 13 | diesel::insert_into(submissions::table) 14 | .values(&submission) 15 | .returning(submissions::id) 16 | .get_result(conn) 17 | }) 18 | .await 19 | .map(|id: Option| id.expect("returning guarantees id present"))?) 20 | } 21 | 22 | pub async fn list_submissions(db: &Db) -> QueryResult> { 23 | db.run(move |conn| submissions::table.load(conn)).await 24 | } 25 | 26 | pub async fn list_user_submissions(db: &Db, id: i32) -> QueryResult> { 27 | db.run(move |conn| { 28 | submissions::table 29 | .filter(submissions::user.eq(id)) 30 | .load(conn) 31 | }) 32 | .await 33 | } 34 | 35 | pub async fn list_challenge_submissions(db: &Db, id: i32) -> QueryResult> { 36 | db.run(move |conn| { 37 | submissions::table 38 | .filter(submissions::challenge.eq(id)) 39 | .load(conn) 40 | }) 41 | .await 42 | } 43 | 44 | pub async fn list_user_challenge_submissions( 45 | db: &Db, 46 | user: i32, 47 | challenge: i32, 48 | ) -> QueryResult> { 49 | db.run(move |conn| { 50 | submissions::table 51 | .filter( 52 | submissions::user 53 | .eq(user) 54 | .and(submissions::challenge.eq(challenge)), 55 | ) 56 | .load(conn) 57 | }) 58 | .await 59 | } 60 | -------------------------------------------------------------------------------- /migrations/2024-09-28-125657_init/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | 3 | CREATE TABLE "users" ( 4 | "id" INTEGER, 5 | "username" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "email" TEXT NOT NULL, 8 | "contact" TEXT NOT NULL, 9 | "enabled" BOOLEAN NOT NULL, 10 | "role" TEXT NOT NULL, 11 | PRIMARY KEY("id") 12 | ); 13 | 14 | CREATE TABLE "problemsets" ( 15 | "id" INTEGER, 16 | "name" TEXT NOT NULL, 17 | PRIMARY KEY("id") 18 | ); 19 | 20 | CREATE TABLE "challenges" ( 21 | "id" INTEGER, 22 | "name" TEXT NOT NULL, 23 | "description" TEXT NOT NULL, 24 | "path" TEXT NOT NULL, 25 | "initial" REAL NOT NULL, 26 | "points" REAL NOT NULL, 27 | "problemset" INTEGER, 28 | "attachments" TEXT NOT NULL, 29 | "flag" TEXT NOT NULL, 30 | "dynamic" BOOLEAN NOT NULL, 31 | "public" BOOLEAN NOT NULL, 32 | PRIMARY KEY("id"), 33 | FOREIGN KEY("problemset") REFERENCES "problemsets"("id") ON DELETE SET NULL 34 | ); 35 | 36 | CREATE TABLE "artifacts" ( 37 | "id" INTEGER, 38 | "user" INTEGER, 39 | "challenge" INTEGER NOT NULL, 40 | "flag" TEXT NOT NULL, 41 | "info" TEXT NOT NULL, 42 | "path" TEXT NOT NULL, 43 | FOREIGN KEY("user") REFERENCES "users"("id") ON DELETE CASCADE, 44 | FOREIGN KEY("challenge") REFERENCES "challenges"("id") ON DELETE CASCADE, 45 | PRIMARY KEY("id") 46 | ); 47 | 48 | CREATE TABLE "solved" ( 49 | "id" INTEGER, 50 | "submission" INTEGER NOT NULL, 51 | "factor" REAL NOT NULL, 52 | PRIMARY KEY("id"), 53 | FOREIGN KEY("submission") REFERENCES "submissions"("id") ON DELETE CASCADE 54 | ); 55 | 56 | CREATE TABLE "submissions" ( 57 | "id" INTEGER, 58 | "user" INTEGER NOT NULL, 59 | "challenge" INTEGER NOT NULL, 60 | "time" TIMESTAMP NOT NULL, 61 | "flag" TEXT NOT NULL, 62 | PRIMARY KEY("id"), 63 | FOREIGN KEY("user") REFERENCES "users"("id") ON DELETE CASCADE, 64 | FOREIGN KEY("challenge") REFERENCES "challenges"("id") ON DELETE CASCADE 65 | ); 66 | -------------------------------------------------------------------------------- /src/pages/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod challenge; 2 | pub mod root; 3 | pub mod scoreboard; 4 | pub mod user; 5 | 6 | use std::fmt::Display; 7 | 8 | use rocket::fairing::AdHoc; 9 | use rocket_dyn_templates::{context, Template}; 10 | 11 | use crate::{db::models::User, functions::event::is_available}; 12 | 13 | use super::{Error, Result}; 14 | 15 | #[allow(clippy::result_large_err)] 16 | #[inline] 17 | fn check_event_availability(user: Option<&User>) -> Result<()> { 18 | if !is_available(user) { 19 | return Err(Error::redirect( 20 | uri!(user::ROOT, user::index), 21 | "禁止在比赛开始前访问题目或调用相关接口", 22 | )); 23 | } 24 | 25 | Ok(()) 26 | } 27 | 28 | fn show_error(msg: &str) -> Template { 29 | Template::render("core/error", context! {msg}) 30 | } 31 | 32 | trait ResultResponseExt { 33 | #[allow(dead_code)] 34 | fn resp_unwrap(self) -> Result; 35 | fn resp_expect(self, msg: &str) -> Result; 36 | } 37 | 38 | impl ResultResponseExt for std::result::Result 39 | where 40 | E: Display, 41 | { 42 | fn resp_unwrap(self) -> Result { 43 | self.map_err(|err| Error::Page(show_error(&format!("{err}")))) 44 | } 45 | 46 | fn resp_expect(self, msg: &str) -> Result { 47 | self.map_err(|err| Error::Page(show_error(&format!("{msg}: {err}")))) 48 | } 49 | } 50 | 51 | trait OptionResponseExt { 52 | #[allow(dead_code)] 53 | fn resp_expect(self, msg: &str) -> Result; 54 | } 55 | 56 | impl OptionResponseExt for Option { 57 | fn resp_expect(self, msg: &str) -> Result { 58 | self.ok_or_else(|| Error::Page(show_error(msg))) 59 | } 60 | } 61 | 62 | pub fn stage() -> AdHoc { 63 | AdHoc::on_ignite("Core Pages", |rocket| async { 64 | rocket 65 | .attach(challenge::stage()) 66 | .attach(root::stage()) 67 | .attach(scoreboard::stage()) 68 | .attach(user::stage()) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/responder.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use rocket::{ 4 | http::{hyper::header::CONTENT_DISPOSITION, ContentType}, 5 | response::{Responder, Result as ResponseResult}, 6 | Request, Response, 7 | }; 8 | use tokio::{ 9 | fs::File, 10 | io::{AsyncRead, AsyncSeek, Result as IoResult}, 11 | }; 12 | 13 | pub struct NamedFile 14 | where 15 | T: AsyncRead + AsyncSeek + Send, 16 | { 17 | path: PathBuf, 18 | file: T, 19 | } 20 | 21 | impl NamedFile { 22 | pub async fn open>(path: P) -> IoResult { 23 | Ok(Self { 24 | path: path.as_ref().to_path_buf(), 25 | file: File::open(path).await?, 26 | }) 27 | } 28 | } 29 | 30 | impl NamedFile 31 | where 32 | T: AsyncRead + AsyncSeek + Send, 33 | { 34 | pub fn with_name(name: &str, file: T) -> Self 35 | where 36 | T: AsyncRead + AsyncSeek + Send, 37 | { 38 | Self { 39 | path: PathBuf::from(name), 40 | file, 41 | } 42 | } 43 | } 44 | 45 | impl<'r, T> Responder<'r, 'static> for NamedFile 46 | where 47 | T: AsyncRead + AsyncSeek + Send + 'static, 48 | { 49 | fn respond_to(self, _: &'r Request<'_>) -> ResponseResult<'static> { 50 | let mut response = Response::build().sized_body(None, self.file).ok()?; 51 | 52 | if let Some(ext) = self.path.extension().and_then(|ext| ext.to_str()) { 53 | if let Some(content_type) = ContentType::from_extension(ext) { 54 | response.set_header(content_type); 55 | } 56 | } 57 | 58 | let content_disposition = self 59 | .path 60 | .file_name() 61 | .and_then(|name| name.to_str()) 62 | .map(|name| format!("attachment; filename=\"{}\"", name)) 63 | .unwrap_or_else(|| "attachment".to_string()); 64 | 65 | response.set_raw_header(CONTENT_DISPOSITION.as_str(), content_disposition); 66 | 67 | Ok(response) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/activity/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod challenge; 2 | 3 | use std::{collections::HashMap, fs::File, io::Read, sync::LazyLock}; 4 | 5 | use anyhow::Result; 6 | use koto::runtime::KValue; 7 | use serde::{Deserialize, Serialize}; 8 | use tokio::sync::{Mutex, RwLock}; 9 | 10 | use crate::{configs::activity::CONFIG, utils::script::KotoScript}; 11 | 12 | static SCRIPTS: LazyLock>>> = LazyLock::new(|| { 13 | let mut scripts = HashMap::new(); 14 | 15 | for info in CONFIG.scripts.iter() { 16 | let mut file = File::open(&info.path).unwrap(); 17 | let mut code = String::new(); 18 | file.read_to_string(&mut code).unwrap(); 19 | 20 | let script = KotoScript::compile(&code).unwrap(); 21 | scripts.insert(info.path.clone(), Mutex::new(script)); 22 | } 23 | 24 | RwLock::new(scripts) 25 | }); 26 | 27 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 28 | pub enum ActivityKind { 29 | Solved, 30 | } 31 | 32 | impl ActivityKind { 33 | fn function_name(&self) -> &'static str { 34 | match self { 35 | ActivityKind::Solved => "solved", 36 | } 37 | } 38 | } 39 | 40 | fn as_koto_value(value: T) -> Result { 41 | let json = serde_json::to_value(value)?; 42 | Ok(koto_json::json_value_to_koto_value(&json)?) 43 | } 44 | 45 | async fn broadcast<'a>(kind: ActivityKind, args: &[KValue]) { 46 | let scripts = SCRIPTS.read().await; 47 | 48 | for info in CONFIG.scripts.iter() { 49 | if info.kinds.contains(&kind) { 50 | let mut script = scripts 51 | .get(&info.path) 52 | .expect("script should exists here.") 53 | .lock() 54 | .await; 55 | 56 | let function = kind.function_name(); 57 | 58 | if let Err(e) = script.call_function(function, args) { 59 | log::error!(target: "activity", "call function {function} on {} failed: {e:?}", info.path); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "attackr" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | [lints.clippy] 7 | result_large_err = "allow" 8 | 9 | [profile.release] 10 | lto = true 11 | codegen-units = 1 12 | opt-level = 3 13 | panic = "abort" 14 | strip = "symbols" 15 | 16 | [dependencies] 17 | anyhow = "1.0.86" 18 | argon2 = { version = "0.5.3", features = ["std"] } 19 | async-tempfile = { version = "0.6.0", features = ["uuid"] } 20 | bollard = { version = "0.17.0", features = ["buildkit"] } 21 | diesel = { version = "2.2.2", features = ["returning_clauses_for_sqlite_3_35", "time"] } 22 | diesel-derive-enum = { version = "2.1.0", features = ["sqlite"] } 23 | diesel_migrations = "2.2.0" 24 | either = { version = "1.13.0", features = ["serde"] } 25 | flate2 = "1.0.33" 26 | futures-util = "0.3.30" 27 | itertools = "0.13.0" 28 | koto = "0.14.1" 29 | koto_json = { version = "0.14.1", optional = true } 30 | koto_random = { version = "0.14.1", optional = true } 31 | koto_tempfile = { version = "0.14.1", optional = true } 32 | log = "0.4.22" 33 | moka = { version = "0.12.8", features = ["future"] } 34 | num_enum = "0.7.3" 35 | rand = { version = "0.9.1", default-features = false, features = ["thread_rng", "std"] } 36 | rocket = { version = "0.5.1", features = ["json"] } 37 | rocket_dyn_templates = { version = "0.2.0", features = ["minijinja"] } 38 | rocket_sync_db_pools = { version = "0.1.0", features = ["diesel_sqlite_pool"] } 39 | serde = { version = "1.0.207", features = ["derive"] } 40 | serde_json = "1.0.127" 41 | serde_yml = "0.0.12" 42 | sha2 = "0.10.8" 43 | strum = { version = "0.26.3", features = ["derive"] } 44 | tar = "0.4.41" 45 | time = { version = "0.3.36", features = ["serde", "serde-human-readable"] } 46 | tokio = { version = "1.40.0", features = ["process", "rt"] } 47 | uuid = { version = "1.10.0", features = ["v4"] } 48 | validator = { version = "0.18.1", features = ["derive"] } 49 | 50 | [features] 51 | koto_exec = [] 52 | koto_json = ["dep:koto_json"] 53 | koto_random = ["dep:koto_random"] 54 | koto_tempfile = ["dep:koto_tempfile"] 55 | activity = ["koto_json"] 56 | -------------------------------------------------------------------------------- /src/pages/admin/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod artifact; 2 | pub mod challenge; 3 | pub mod difficulty; 4 | pub mod problemset; 5 | pub mod root; 6 | pub mod submission; 7 | pub mod user; 8 | 9 | use std::fmt::Display; 10 | 11 | use rocket::fairing::AdHoc; 12 | use rocket_dyn_templates::{context, Template}; 13 | 14 | use crate::db::models::{User, UserRole}; 15 | 16 | use super::{Error, Result}; 17 | 18 | #[allow(clippy::result_large_err)] 19 | #[inline] 20 | fn check_permission(user: &User) -> Result<()> { 21 | if user.role < UserRole::Administrator { 22 | return Err(Error::redirect( 23 | uri!(super::core::user::ROOT, super::core::user::index), 24 | "禁止 Administrator 以下权限组访问管理界面或调用相关接口", 25 | )); 26 | } 27 | 28 | Ok(()) 29 | } 30 | 31 | fn show_error(msg: &str) -> Template { 32 | Template::render("admin/error", context! {msg}) 33 | } 34 | 35 | trait ResultResponseExt { 36 | #[allow(dead_code)] 37 | fn resp_unwrap(self) -> Result; 38 | fn resp_expect(self, msg: &str) -> Result; 39 | } 40 | 41 | impl ResultResponseExt for std::result::Result 42 | where 43 | E: Display, 44 | { 45 | fn resp_unwrap(self) -> Result { 46 | self.map_err(|err| Error::Page(show_error(&format!("{err}")))) 47 | } 48 | 49 | fn resp_expect(self, msg: &str) -> Result { 50 | self.map_err(|err| Error::Page(show_error(&format!("{msg}: {err}")))) 51 | } 52 | } 53 | 54 | trait OptionResponseExt { 55 | fn resp_expect(self, msg: &str) -> Result; 56 | } 57 | 58 | impl OptionResponseExt for Option { 59 | fn resp_expect(self, msg: &str) -> Result { 60 | self.ok_or_else(|| Error::Page(show_error(msg))) 61 | } 62 | } 63 | 64 | pub fn stage() -> AdHoc { 65 | AdHoc::on_ignite("Admin Pages", |rocket| async { 66 | rocket 67 | .attach(artifact::stage()) 68 | .attach(challenge::stage()) 69 | .attach(difficulty::stage()) 70 | .attach(problemset::stage()) 71 | .attach(root::stage()) 72 | .attach(submission::stage()) 73 | .attach(user::stage()) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/db/types.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Debug, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use diesel::{ 7 | expression::AsExpression, 8 | serialize::{IsNull, ToSql}, 9 | sql_types, 10 | sqlite::Sqlite, 11 | Queryable, 12 | }; 13 | use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer}; 14 | 15 | #[derive(Debug, Clone, AsExpression)] 16 | #[diesel(sql_type = sql_types::Text)] 17 | pub struct Json(pub T); 18 | 19 | impl From for Json { 20 | fn from(value: T) -> Self { 21 | Self(value) 22 | } 23 | } 24 | 25 | impl Serialize for Json 26 | where 27 | T: Serialize, 28 | { 29 | fn serialize(&self, serializer: S) -> std::result::Result 30 | where 31 | S: Serializer, 32 | { 33 | self.0.serialize(serializer) 34 | } 35 | } 36 | 37 | impl<'de, T> Deserialize<'de> for Json 38 | where 39 | T: Deserialize<'de>, 40 | { 41 | fn deserialize(deserializer: D) -> std::result::Result 42 | where 43 | D: Deserializer<'de>, 44 | { 45 | Ok(Self(T::deserialize(deserializer)?)) 46 | } 47 | } 48 | 49 | impl Queryable for Json 50 | where 51 | T: DeserializeOwned, 52 | { 53 | type Row = String; 54 | 55 | fn build(row: Self::Row) -> diesel::deserialize::Result { 56 | Ok(serde_json::from_str(&row)?) 57 | } 58 | } 59 | 60 | impl ToSql for Json 61 | where 62 | T: Serialize + Debug, 63 | { 64 | fn to_sql<'b>( 65 | &'b self, 66 | out: &mut diesel::serialize::Output<'b, '_, Sqlite>, 67 | ) -> diesel::serialize::Result { 68 | let json = serde_json::to_string(self)?; 69 | out.set_value(json); 70 | 71 | Ok(IsNull::No) 72 | } 73 | } 74 | 75 | impl Deref for Json { 76 | type Target = T; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.0 80 | } 81 | } 82 | 83 | impl DerefMut for Json { 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.0 86 | } 87 | } 88 | 89 | impl AsRef for Json { 90 | fn as_ref(&self) -> &T { 91 | &self.0 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/db/query/user.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use anyhow::Result as AnyResult; 4 | use diesel::QueryResult; 5 | use validator::Validate; 6 | 7 | use crate::db::{ 8 | models::{User, UserRole}, 9 | schema::users, 10 | Db, 11 | }; 12 | 13 | pub async fn add_user(db: &Db, user: User) -> AnyResult { 14 | user.validate()?; 15 | 16 | Ok(db 17 | .run(move |conn| { 18 | diesel::insert_into(users::table) 19 | .values(&user) 20 | .returning(users::id) 21 | .get_result(conn) 22 | }) 23 | .await 24 | .map(|id: Option| id.expect("returning guarantees id present"))?) 25 | } 26 | 27 | pub async fn update_user(db: &Db, user: User) -> AnyResult<()> { 28 | user.validate()?; 29 | 30 | db.run(move |conn| { 31 | diesel::update(users::table.filter(users::id.eq(user.id))) 32 | .set(&user) 33 | .execute(conn) 34 | }) 35 | .await?; 36 | 37 | Ok(()) 38 | } 39 | 40 | pub async fn get_user(db: &Db, id: i32) -> QueryResult { 41 | db.run(move |conn| users::table.filter(users::id.eq(id)).first(conn)) 42 | .await 43 | } 44 | 45 | pub async fn list_users(db: &Db) -> QueryResult> { 46 | db.run(move |conn| users::table.load(conn)).await 47 | } 48 | 49 | pub async fn list_active_challengers(db: &Db) -> QueryResult> { 50 | db.run(move |conn| { 51 | users::table 52 | .filter( 53 | users::role 54 | .eq(UserRole::Challenger) 55 | .and(users::enabled.eq(true)), 56 | ) 57 | .load(conn) 58 | }) 59 | .await 60 | } 61 | 62 | pub async fn get_user_by_username(db: &Db, username: String) -> QueryResult { 63 | db.run(move |conn| { 64 | users::table 65 | .filter(users::username.eq(username)) 66 | .first(conn) 67 | }) 68 | .await 69 | } 70 | 71 | pub async fn delete_user(db: &Db, id: i32) -> QueryResult<()> { 72 | db.run(move |conn| { 73 | diesel::delete(users::table) 74 | .filter(users::id.eq(id)) 75 | .execute(conn) 76 | }) 77 | .await?; 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /templates/admin/challenge/publish.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

公开题目

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for info in challenges %} 25 | {% set problemset = info.problemset %} 26 | {% set difficulty = info.difficulty %} 27 | {% set challenge = info.challenge %} 28 | 29 | 30 | 31 | 38 | 45 | 52 | 53 | 57 | 60 | 61 | {% endfor %} 62 | 63 |
ID名称附属题集难度类型初始分分数公开
{{ challenge.id }}{{ challenge.name }} 32 | {% if problemset %} 33 | {{ problemset.name }} 34 | {% else %} 35 | none 36 | {% endif %} 37 | 39 | {% if difficulty %} 40 | {{ difficulty.name }} 41 | {% else %} 42 | none 43 | {% endif %} 44 | 46 | {% if challenge.dynamic %} 47 | 动态 48 | {% else %} 49 | 静态 50 | {% endif %} 51 | {{ challenge.initial }} 54 | {% from 'functions/points' import display as display_points %} 55 | {{ display_points(challenge.points) }} 56 | 58 | 59 |
64 |
65 | 66 |
67 | {% endblock %} -------------------------------------------------------------------------------- /templates/core/challenge/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "core/base" %} 2 | 3 | {% block content %} 4 | {% from 'functions/points' import display as display_points %} 5 | 6 | {% for problemset, info in info | groupby("problemset") %} 7 |
8 |

9 | {% if problemset %} 10 | {{ problemset.name }} 11 | {% else %} 12 | (未分类) 13 | {% endif %} 14 |

15 |

16 | 进度: {{ info | map(attribute="user_solved") | select | length }} / {{ info | length }} 17 | 18 | 得分: {{ display_points(info | map(attribute="points") | sumint) }} / {{ display_points(info | 19 | map(attribute="challenge") | map(attribute="points") | sumint) }} pts 20 |

21 |
22 | {% for info in info %} 23 | {% set challenge = info.challenge %} 24 | {% set solved = info.solved %} 25 | {% set user_solved = info.user_solved %} 26 | {% set difficulty = info.difficulty %} 27 |
28 |
29 |
30 | {% from "core/components/progress" import show_progress %} 31 |

{{ show_progress(user_solved is not none) }}

32 |
33 | {% if difficulty %} 34 | [{{ difficulty.name }}] 35 | {% endif %} 36 | {{ challenge.name }} 37 |
38 |
39 | {{ display_points(info.points) }} 40 | / 41 | {{ display_points(challenge.points) }} 42 | pts 43 |
44 | 查看 45 |
46 | 49 |
50 |
51 | {% endfor %} 52 |
53 |
54 | {% endfor %} 55 | {% endblock %} -------------------------------------------------------------------------------- /src/db/query/challenge.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use anyhow::Result as AnyResult; 4 | use diesel::QueryResult; 5 | use validator::Validate; 6 | 7 | use crate::db::{models::Challenge, schema::challenges, Db}; 8 | 9 | pub async fn add_challenge(db: &Db, challenge: Challenge) -> AnyResult { 10 | challenge.validate()?; 11 | 12 | Ok(db 13 | .run(move |conn| { 14 | diesel::insert_into(challenges::table) 15 | .values(&challenge) 16 | .returning(challenges::id) 17 | .get_result(conn) 18 | }) 19 | .await 20 | .map(|id: Option| id.expect("returning guarantees id present"))?) 21 | } 22 | 23 | pub async fn update_challenge(db: &Db, challenge: Challenge) -> AnyResult<()> { 24 | challenge.validate()?; 25 | 26 | db.run(move |conn| { 27 | diesel::update(challenges::table.filter(challenges::id.eq(challenge.id))) 28 | .set(&challenge) 29 | .execute(conn) 30 | }) 31 | .await?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub async fn publish_challenge(db: &Db, id: i32) -> QueryResult<()> { 37 | db.run(move |conn| { 38 | diesel::update(challenges::table.filter(challenges::id.eq(id))) 39 | .set(challenges::public.eq(true)) 40 | .execute(conn) 41 | }) 42 | .await?; 43 | 44 | Ok(()) 45 | } 46 | 47 | pub async fn get_challenge(db: &Db, id: i32) -> QueryResult { 48 | db.run(move |conn| challenges::table.filter(challenges::id.eq(id)).first(conn)) 49 | .await 50 | } 51 | 52 | pub async fn list_problemset_challenges(db: &Db, id: i32) -> QueryResult> { 53 | db.run(move |conn| { 54 | challenges::table 55 | .filter(challenges::problemset.eq(id)) 56 | .load(conn) 57 | }) 58 | .await 59 | } 60 | 61 | pub async fn list_challenges(db: &Db) -> QueryResult> { 62 | db.run(move |conn| challenges::table.load(conn)).await 63 | } 64 | 65 | pub async fn list_private_challenges(db: &Db) -> QueryResult> { 66 | db.run(move |conn| { 67 | challenges::table 68 | .filter(challenges::public.eq(false)) 69 | .load(conn) 70 | }) 71 | .await 72 | } 73 | 74 | pub async fn delete_challenge(db: &Db, id: i32) -> QueryResult<()> { 75 | db.run(move |conn| { 76 | diesel::delete(challenges::table) 77 | .filter(challenges::id.eq(id)) 78 | .execute(conn) 79 | }) 80 | .await?; 81 | 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /src/db/query/artifact.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use anyhow::Result as AnyResult; 4 | use diesel::QueryResult; 5 | use validator::Validate; 6 | 7 | use crate::db::{models::Artifact, schema::artifacts, Db}; 8 | 9 | pub async fn update_artifact(db: &Db, artifact: Artifact) -> AnyResult<()> { 10 | artifact.validate()?; 11 | 12 | db.run(move |conn| { 13 | diesel::replace_into(artifacts::table) 14 | .values(&artifact) 15 | .execute(conn) 16 | }) 17 | .await?; 18 | 19 | Ok(()) 20 | } 21 | 22 | pub async fn get_static_artifact(db: &Db, challenge: i32) -> QueryResult { 23 | db.run(move |conn| { 24 | artifacts::table 25 | .filter( 26 | artifacts::user 27 | .is_null() 28 | .and(artifacts::challenge.eq(challenge)), 29 | ) 30 | .first(conn) 31 | }) 32 | .await 33 | } 34 | 35 | pub async fn get_dynamic_artifact(db: &Db, challenge: i32, user: i32) -> QueryResult { 36 | db.run(move |conn| { 37 | artifacts::table 38 | .filter( 39 | artifacts::user 40 | .eq(user) 41 | .and(artifacts::challenge.eq(challenge)), 42 | ) 43 | .first(conn) 44 | }) 45 | .await 46 | } 47 | 48 | pub async fn get_artifact(db: &Db, challenge: i32, user: Option) -> QueryResult { 49 | if let Some(user) = user { 50 | get_dynamic_artifact(db, challenge, user).await 51 | } else { 52 | get_static_artifact(db, challenge).await 53 | } 54 | } 55 | 56 | pub async fn get_artifact_by_id(db: &Db, id: i32) -> QueryResult { 57 | db.run(move |conn| artifacts::table.filter(artifacts::id.eq(id)).first(conn)) 58 | .await 59 | } 60 | 61 | pub async fn delete_artifact(db: &Db, id: i32) -> QueryResult<()> { 62 | db.run(move |conn| { 63 | diesel::delete(artifacts::table) 64 | .filter(artifacts::id.eq(id)) 65 | .execute(conn) 66 | }) 67 | .await?; 68 | 69 | Ok(()) 70 | } 71 | 72 | pub async fn list_artifacts(db: &Db) -> QueryResult> { 73 | db.run(move |conn| artifacts::table.load(conn)).await 74 | } 75 | 76 | pub async fn list_user_artifacts(db: &Db, id: i32) -> QueryResult> { 77 | db.run(move |conn| artifacts::table.filter(artifacts::user.eq(id)).load(conn)) 78 | .await 79 | } 80 | 81 | pub async fn list_challenge_artifacts(db: &Db, id: i32) -> QueryResult> { 82 | db.run(move |conn| { 83 | artifacts::table 84 | .filter(artifacts::challenge.eq(id)) 85 | .load(conn) 86 | }) 87 | .await 88 | } 89 | -------------------------------------------------------------------------------- /src/utils/script.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "koto_exec")] 2 | use {koto::runtime::ErrorKind, std::process::Command}; 3 | 4 | use anyhow::{anyhow, Result}; 5 | use koto::prelude::*; 6 | 7 | #[cfg(feature = "koto_exec")] 8 | fn exec(ctx: &mut CallContext) -> koto::Result { 9 | match ctx.args() { 10 | [KValue::Str(program), KValue::Tuple(targs)] => { 11 | let mut args = Vec::with_capacity(targs.len()); 12 | 13 | for arg in targs.iter() { 14 | match arg { 15 | KValue::Str(val) => args.push(val.as_str()), 16 | unexpected => return type_error("expected str type arg.", unexpected), 17 | } 18 | } 19 | 20 | let output = Command::new(program.as_str()) 21 | .args(args) 22 | .output() 23 | .map_err(|err| ErrorKind::StringError(format!("{err:?}")))?; 24 | 25 | let stdout = KValue::Str(String::from_utf8_lossy(&output.stdout).to_string().into()); 26 | let stderr = KValue::Str(String::from_utf8_lossy(&output.stderr).to_string().into()); 27 | 28 | Ok(KValue::Tuple((&[stdout, stderr]).into())) 29 | } 30 | unexpected => type_error_with_slice("expected program and args.", unexpected), 31 | } 32 | } 33 | 34 | pub struct KotoScript { 35 | koto: Koto, 36 | } 37 | 38 | impl KotoScript { 39 | pub fn compile(script: &str) -> Result { 40 | let mut koto = Koto::new(); 41 | 42 | #[cfg(any( 43 | feature = "koto_exec", 44 | feature = "koto_json", 45 | feature = "koto_random", 46 | feature = "koto_tempfile" 47 | ))] 48 | let prelude = koto.prelude(); 49 | 50 | #[cfg(feature = "koto_exec")] 51 | prelude.add_fn("exec", exec); 52 | 53 | #[cfg(feature = "koto_json")] 54 | prelude.insert("json", koto_json::make_module()); 55 | #[cfg(feature = "koto_random")] 56 | prelude.insert("random", koto_random::make_module()); 57 | #[cfg(feature = "koto_tempfile")] 58 | prelude.insert("tempfile", koto_tempfile::make_module()); 59 | 60 | koto.compile_and_run(script)?; 61 | 62 | Ok(Self { koto }) 63 | } 64 | 65 | pub fn get(&self, name: &str) -> Option { 66 | let exported = self.koto.exports(); 67 | exported.get(name) 68 | } 69 | 70 | pub fn call_function<'a, A>(&mut self, name: &str, args: A) -> Result 71 | where 72 | A: Into>, 73 | { 74 | let function = self 75 | .get(name) 76 | .ok_or_else(|| anyhow!("function '{name}' not found."))?; 77 | 78 | let ret = self.koto.call_function(function, args)?; 79 | 80 | Ok(ret) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /templates/admin/submission/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

提交记录

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 29 |
30 |
31 | 50 |
51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {% for data in submissions | reverse %} 66 | {% set submission = data.submission %} 67 | 68 | 69 | 70 | 71 | 72 | 76 | 77 | {% endfor %} 78 | 79 |
ID用户名题目提交 Flag提交时间
{{ submission.id }}{{ data.user.username }}{{ data.challenge.name }}{{ submission.flag }} 73 | {% from "functions/time" import display as display_time %} 74 | {{ display_time(submission.time) }} 75 |
80 |
81 | {% endblock %} -------------------------------------------------------------------------------- /src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | mod admin; 2 | mod core; 3 | mod error; 4 | 5 | use std::fmt::Display; 6 | 7 | use rocket::{ 8 | fairing::AdHoc, 9 | http::{uri::Reference, CookieJar}, 10 | response::{Flash, Redirect}, 11 | }; 12 | use rocket_dyn_templates::{minijinja, Template}; 13 | 14 | use crate::{ 15 | configs, 16 | db::{models::User, Db}, 17 | utils, 18 | }; 19 | 20 | async fn auth_session(db: &Db, jar: &CookieJar<'_>) -> Result { 21 | crate::functions::user::auth_session(db, jar) 22 | .await 23 | .flash_expect(uri!(core::user::ROOT, core::user::login_page), "认证失败") 24 | } 25 | 26 | pub fn stage() -> AdHoc { 27 | AdHoc::on_ignite("Pages", |rocket| async { 28 | rocket 29 | .attach(Template::custom(|engines| { 30 | engines.minijinja.add_global( 31 | "event", 32 | minijinja::Value::from_serialize(&*configs::event::CONFIG), 33 | ); 34 | engines.minijinja.add_filter("sumint", utils::jinja::sum); 35 | engines.minijinja.set_trim_blocks(true); 36 | engines.minijinja.set_lstrip_blocks(true); 37 | })) 38 | .attach(admin::stage()) 39 | .attach(core::stage()) 40 | .attach(error::stage()) 41 | }) 42 | } 43 | 44 | pub type Result = std::result::Result; 45 | 46 | #[allow(clippy::large_enum_variant)] 47 | #[derive(Responder)] 48 | pub enum Error { 49 | Page(Template), 50 | Redirect(Flash), 51 | } 52 | 53 | impl Error { 54 | pub fn redirect>>(uri: U, msg: &str) -> Self { 55 | Self::Redirect(Flash::error(Redirect::to(uri), msg)) 56 | } 57 | } 58 | 59 | pub trait ResultFlashExt { 60 | #[allow(dead_code)] 61 | fn flash_unwrap>>(self, uri: U) -> Result; 62 | fn flash_expect>>(self, uri: U, msg: &str) -> Result; 63 | } 64 | 65 | impl ResultFlashExt for std::result::Result 66 | where 67 | E: Display, 68 | { 69 | fn flash_unwrap>>(self, uri: U) -> Result { 70 | self.map_err(|err| Error::redirect(uri, &format!("{err}"))) 71 | } 72 | 73 | fn flash_expect>>(self, uri: U, msg: &str) -> Result { 74 | self.map_err(|err| Error::redirect(uri, &format!("{msg}: {err}"))) 75 | } 76 | } 77 | 78 | pub trait OptionFlashExt { 79 | #[allow(dead_code)] 80 | fn flash_expect>>(self, uri: U, msg: &str) -> Result; 81 | } 82 | 83 | impl OptionFlashExt for Option { 84 | fn flash_expect>>(self, uri: U, msg: &str) -> Result { 85 | self.ok_or_else(|| Error::redirect(uri, msg)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/db/schema.rs: -------------------------------------------------------------------------------- 1 | diesel::table! { 2 | artifacts (id) { 3 | id -> Nullable, 4 | user -> Nullable, 5 | challenge -> Integer, 6 | flag -> Text, 7 | info -> Text, 8 | path -> Text, 9 | } 10 | } 11 | 12 | diesel::table! { 13 | challenges (id) { 14 | id -> Nullable, 15 | name -> Text, 16 | description -> Text, 17 | path -> Text, 18 | initial -> Double, 19 | points -> Double, 20 | problemset -> Nullable, 21 | attachments -> Text, 22 | flag -> Text, 23 | dynamic -> Bool, 24 | public -> Bool, 25 | difficulty -> Nullable, 26 | } 27 | } 28 | 29 | diesel::table! { 30 | difficulties (id) { 31 | id -> Nullable, 32 | name -> Text, 33 | color -> Text, 34 | } 35 | } 36 | 37 | diesel::table! { 38 | problemsets (id) { 39 | id -> Nullable, 40 | name -> Text, 41 | } 42 | } 43 | 44 | diesel::table! { 45 | submissions (id) { 46 | id -> Nullable, 47 | user -> Integer, 48 | challenge -> Integer, 49 | flag -> Text, 50 | time -> Timestamp, 51 | } 52 | } 53 | 54 | diesel::table! { 55 | use crate::db::models::UserRoleMapping; 56 | use diesel::sql_types::{Nullable, Integer, Text, Bool}; 57 | 58 | users (id) { 59 | id -> Nullable, 60 | username -> Text, 61 | password -> Text, 62 | contact -> Text, 63 | email -> Text, 64 | enabled -> Bool, 65 | role -> UserRoleMapping, 66 | nickname -> Nullable, 67 | } 68 | } 69 | 70 | diesel::table! { 71 | scores (id) { 72 | id -> Nullable, 73 | user -> Integer, 74 | challenge -> Integer, 75 | time -> Timestamp, 76 | points -> Double, 77 | } 78 | } 79 | 80 | diesel::table! { 81 | solved (id) { 82 | id -> Nullable, 83 | submission -> Integer, 84 | score -> Nullable, 85 | } 86 | } 87 | 88 | diesel::joinable!(artifacts -> challenges (challenge)); 89 | diesel::joinable!(artifacts -> users (user)); 90 | diesel::joinable!(challenges -> difficulties (difficulty)); 91 | diesel::joinable!(challenges -> problemsets (problemset)); 92 | diesel::joinable!(scores -> challenges (challenge)); 93 | diesel::joinable!(scores -> users (user)); 94 | diesel::joinable!(solved -> scores (score)); 95 | diesel::joinable!(solved -> submissions (submission)); 96 | diesel::joinable!(submissions -> challenges (challenge)); 97 | diesel::joinable!(submissions -> users (user)); 98 | 99 | diesel::allow_tables_to_appear_in_same_query!( 100 | artifacts, 101 | challenges, 102 | difficulties, 103 | problemsets, 104 | scores, 105 | solved, 106 | submissions, 107 | users, 108 | ); 109 | -------------------------------------------------------------------------------- /templates/admin/challenge/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

题目列表

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 重新计算分数 11 |
12 |
13 | 添加 14 |
15 |
16 | 批量公开 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for info in challenges %} 37 | {% set problemset = info.problemset %} 38 | {% set difficulty = info.difficulty %} 39 | {% set challenge = info.challenge %} 40 | 41 | 42 | 43 | 50 | 57 | 64 | 65 | 69 | 70 | 81 | 82 | {% endfor %} 83 | 84 |
ID名称附属题集难度类型初始分分数公开操作
{{ challenge.id }}{{ challenge.name }} 44 | {% if problemset %} 45 | {{ problemset.name }} 46 | {% else %} 47 | none 48 | {% endif %} 49 | 51 | {% if difficulty %} 52 | {{ difficulty.name }} 53 | {% else %} 54 | none 55 | {% endif %} 56 | 58 | {% if challenge.dynamic %} 59 | 动态 60 | {% else %} 61 | 静态 62 | {% endif %} 63 | {{ challenge.initial }} 66 | {% from 'functions/points' import display as display_points %} 67 | {{ display_points(challenge.points) }} 68 | {{ challenge.public }} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
85 |
86 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/user/edit.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

修改用户信息

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 用户名 11 | 13 |
14 |
15 | 密码 16 | 17 |
18 |
19 | 昵称 20 | 22 |
23 |
24 | 联系方式 25 | 26 |
27 |
28 | Email 29 | 30 |
31 |
32 | 权限组 33 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 | 50 | 51 |
52 | 53 | 73 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/challenge/edit.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "admin/base" %} 2 | 3 | {% block header %} 4 |

修改题目信息

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 名称 11 | 12 |
13 |
14 | 介绍 15 | 17 |
18 |
19 | 分数 20 | 22 |
23 |
24 | 附属题集 25 | 35 |
36 |
37 | 难度 38 | 48 |
49 |
50 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 78 | {% endblock %} -------------------------------------------------------------------------------- /src/configs/challenge.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 3 | ops::RangeInclusive, 4 | path::PathBuf, 5 | sync::LazyLock, 6 | time::Duration, 7 | }; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | use validator::{Validate, ValidationError}; 11 | 12 | use crate::core::conductor::DockerRunOptions; 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct MappedAddr { 16 | pub addr: IpAddr, 17 | #[serde(default)] 18 | pub ports: Option>, 19 | } 20 | 21 | fn default_expiry() -> Option { 22 | Some(Duration::from_secs(30 * 60)) 23 | } 24 | 25 | fn validate_docker_config(config: &DockerConfig) -> Result<(), ValidationError> { 26 | for addr in &config.mapped_addrs { 27 | if let Some(mapped_ports) = &addr.ports { 28 | if let Some(ports) = &config.options.ports { 29 | if ports.len() != mapped_ports.len() { 30 | return Err(ValidationError::new( 31 | "'mapped_ports' must be the same length as 'options.ports'", 32 | )); 33 | } 34 | } else { 35 | return Err(ValidationError::new( 36 | "'mapped_ports' is set without 'options.port' set", 37 | )); 38 | } 39 | } 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | fn default_mapped_addrs() -> Vec { 46 | vec![ 47 | MappedAddr { 48 | addr: IpAddr::V4(Ipv4Addr::LOCALHOST), 49 | ports: None, 50 | }, 51 | MappedAddr { 52 | addr: IpAddr::V6(Ipv6Addr::LOCALHOST), 53 | ports: None, 54 | }, 55 | ] 56 | } 57 | 58 | #[derive(Debug, Clone, Serialize, Deserialize, Validate)] 59 | #[validate(schema(function = "validate_docker_config"))] 60 | pub struct DockerConfig { 61 | #[serde(default = "default_mapped_addrs")] 62 | pub mapped_addrs: Vec, 63 | #[serde(default = "default_expiry")] 64 | pub expiry: Option, 65 | #[serde(default)] 66 | #[validate(nested)] 67 | pub options: DockerRunOptions, 68 | } 69 | 70 | impl Default for DockerConfig { 71 | fn default() -> Self { 72 | Self { 73 | mapped_addrs: default_mapped_addrs(), 74 | expiry: default_expiry(), 75 | options: Default::default(), 76 | } 77 | } 78 | } 79 | 80 | fn default_challenge_root() -> PathBuf { 81 | "challenges".into() 82 | } 83 | 84 | fn default_artifact_root() -> PathBuf { 85 | "artifacts".into() 86 | } 87 | 88 | #[derive(Debug, Clone, Serialize, Deserialize, Validate)] 89 | pub struct Config { 90 | #[serde(default = "default_challenge_root")] 91 | pub challenge_root: PathBuf, 92 | #[serde(default = "default_artifact_root")] 93 | pub artifact_root: PathBuf, 94 | #[serde(default)] 95 | #[validate(nested)] 96 | pub docker: DockerConfig, 97 | #[serde(default)] 98 | pub dynpoints: Option, 99 | #[serde(default)] 100 | pub clear_on_solved: bool, 101 | #[serde(default)] 102 | pub show_uncategorized: bool, 103 | } 104 | 105 | pub static CONFIG: LazyLock = LazyLock::new(|| super::load_config("challenge")); 106 | -------------------------------------------------------------------------------- /src/pages/admin/submission.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rocket::{ 4 | fairing::AdHoc, 5 | http::{uri::Origin, CookieJar}, 6 | request::FlashMessage, 7 | }; 8 | use rocket_dyn_templates::{context, Template}; 9 | 10 | use crate::{ 11 | db::{ 12 | query::{ 13 | challenge::list_challenges, 14 | submission::{ 15 | list_challenge_submissions, list_submissions, list_user_challenge_submissions, 16 | list_user_submissions, 17 | }, 18 | user::list_users, 19 | }, 20 | Db, 21 | }, 22 | pages::{auth_session, Result}, 23 | }; 24 | 25 | use super::{check_permission, OptionResponseExt, ResultResponseExt}; 26 | 27 | #[allow(clippy::declare_interior_mutable_const)] 28 | pub const ROOT: Origin<'static> = uri!("/admin/submission"); 29 | 30 | #[get("/?&")] 31 | async fn index( 32 | jar: &CookieJar<'_>, 33 | db: Db, 34 | flash: Option>, 35 | user: Option, 36 | challenge: Option, 37 | ) -> Result