Skip to content

Commit

Permalink
refactor(problem): support safely filter assets (#40)
Browse files Browse the repository at this point in the history
* refactor(problem): support safely filter assets

* chore: fix code lint

* fix(lint): fix code lint

* chore: fix test code lint
  • Loading branch information
fu050409 authored Dec 3, 2024
1 parent fb62147 commit 2af9a74
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 70 deletions.
5 changes: 5 additions & 0 deletions .changes/refactor-problem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"algohub-server": patch:feat
---

Refactor problem strucutre to support multiple test cases and safely handle input/output files.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 69 additions & 16 deletions src/models/problem.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;

use crate::routes::problem::CreateProblem;

use super::UserRecordId;

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -11,7 +9,31 @@ pub struct Sample {
pub output: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct TestCase {
pub input: Thing,
pub output: Thing,
}

impl From<UserTestCase<'_>> for TestCase {
fn from(value: UserTestCase<'_>) -> Self {
TestCase {
input: Thing::from(("asset", value.input)),
output: Thing::from(("asset", value.output)),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProblemVisibility {
ContestOnly,
Public,
Private,
Internal,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct Problem {
pub id: Option<Thing>,

Expand All @@ -24,19 +46,52 @@ pub struct Problem {

pub time_limit: u64,
pub memory_limit: u64,
pub test_cases: Vec<Sample>,
pub test_cases: Vec<TestCase>,

pub creator: Thing,
pub owner: Thing,
pub categories: Vec<String>,
pub tags: Vec<String>,

pub private: bool,
pub visibility: ProblemVisibility,

pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserTestCase<'r> {
pub input: &'r str,
pub output: &'r str,
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CreateProblem<'r> {
pub id: &'r str,
pub token: &'r str,

pub title: &'r str,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
pub samples: Vec<Sample>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,

pub owner: UserRecordId,
pub time_limit: u64,
pub memory_limit: u64,
pub test_cases: Vec<UserTestCase<'r>>,

pub categories: Vec<String>,
pub tags: Vec<String>,

pub visibility: ProblemVisibility,
}

impl From<CreateProblem<'_>> for Problem {
fn from(val: CreateProblem<'_>) -> Self {
Problem {
Expand All @@ -49,20 +104,20 @@ impl From<CreateProblem<'_>> for Problem {
hint: val.hint,
time_limit: val.time_limit,
memory_limit: val.memory_limit,
test_cases: val.test_cases,
test_cases: val.test_cases.into_iter().map(Into::into).collect(),
creator: ("account", val.id).into(),
owner: val.owner.into(),
categories: val.categories,
tags: val.tags,
private: val.private,
visibility: val.visibility,
created_at: chrono::Local::now().naive_local(),
updated_at: chrono::Local::now().naive_local(),
}
}
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ProblemDetail {
pub struct UserProblem {
pub id: String,

pub title: String,
Expand All @@ -74,22 +129,21 @@ pub struct ProblemDetail {

pub time_limit: u64,
pub memory_limit: u64,
pub test_cases: Vec<Sample>,

pub creator: UserRecordId,
pub creator: String,
pub owner: UserRecordId,
pub categories: Vec<String>,
pub tags: Vec<String>,

pub private: bool,
pub visibility: ProblemVisibility,

pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

impl From<Problem> for ProblemDetail {
impl From<Problem> for UserProblem {
fn from(value: Problem) -> Self {
ProblemDetail {
UserProblem {
id: value.id.unwrap().id.to_string(),
title: value.title,
description: value.description,
Expand All @@ -99,12 +153,11 @@ impl From<Problem> for ProblemDetail {
hint: value.hint,
time_limit: value.time_limit,
memory_limit: value.memory_limit,
test_cases: value.test_cases,
creator: value.creator.into(),
creator: value.creator.id.to_string(),
owner: value.owner.into(),
categories: value.categories,
tags: value.tags,
private: value.private,
visibility: value.visibility,
created_at: value.created_at,
updated_at: value.updated_at,
}
Expand Down
70 changes: 28 additions & 42 deletions src/routes/problem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,14 @@ use crate::{
models::{
account::Account,
error::Error,
problem::{Problem, ProblemDetail, Sample},
problem::{CreateProblem, Problem, ProblemVisibility, UserProblem},
response::Response,
Credentials, OwnedCredentials, UserRecordId,
Credentials, OwnedCredentials, OwnedId,
},
utils::{account, problem, session},
Result,
};

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CreateProblem<'r> {
pub id: &'r str,
pub token: &'r str,

pub title: &'r str,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
pub samples: Vec<Sample>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,

pub owner: UserRecordId,
pub time_limit: u64,
pub memory_limit: u64,
pub test_cases: Vec<Sample>,

pub categories: Vec<String>,
pub tags: Vec<String>,

pub private: bool,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct ProblemResponse {
Expand All @@ -51,7 +24,7 @@ pub struct ProblemResponse {
pub async fn create(
db: &State<Surreal<Client>>,
problem: Json<CreateProblem<'_>>,
) -> Result<ProblemResponse> {
) -> Result<OwnedId> {
if !session::verify(db, problem.id, problem.token).await {
return Err(Error::Unauthorized(Json("Invalid token".into())));
}
Expand All @@ -66,7 +39,7 @@ pub async fn create(
Ok(Json(Response {
success: true,
message: "Problem created successfully".to_string(),
data: Some(ProblemResponse {
data: Some(OwnedId {
id: problem.id.unwrap().id.to_string(),
}),
}))
Expand All @@ -77,26 +50,39 @@ pub async fn get(
db: &State<Surreal<Client>>,
id: &str,
auth: Json<Option<Credentials<'_>>>,
) -> Result<ProblemDetail> {
) -> Result<UserProblem> {
let problem = problem::get::<Problem>(db, id)
.await
.map_err(|e| Error::ServerError(Json(e.to_string().into())))?
.ok_or(Error::NotFound(Json(
"Problem with specified id not found".into(),
)))?;

let has_permission = if problem.private {
if let Some(auth) = auth.as_ref() {
if !session::verify(db, auth.id, auth.token).await {
return Err(Error::Unauthorized(Json("Invalid credentials".into())));
} else {
auth.id == problem.owner.id.to_string()
}
let authed_id = if let Some(auth) = auth.into_inner() {
if !session::verify(db, auth.id, auth.token).await {
return Err(Error::Unauthorized(Json("Invalid credentials".into())));
} else {
false
Some(auth.id)
}
} else {
true
None
};

let has_permission = if authed_id.is_none() && problem.visibility != ProblemVisibility::Public {
false
} else {
match problem.visibility {
ProblemVisibility::ContestOnly => {
// Check for contest access
todo!()
}
ProblemVisibility::Public => true,
ProblemVisibility::Private => problem.owner.id.to_string() == authed_id.unwrap(),
ProblemVisibility::Internal => {
// Check for internal access
todo!()
}
}
};

if !has_permission {
Expand Down Expand Up @@ -124,7 +110,7 @@ pub struct ListProblem {
pub async fn list(
db: &State<Surreal<Client>>,
data: Json<ListProblem>,
) -> Result<Vec<ProblemDetail>> {
) -> Result<Vec<UserProblem>> {
let authed_id = if let Some(auth) = &data.auth {
if !session::verify(db, &auth.id, &auth.token).await {
return Err(Error::Unauthorized(Json("Invalid token".into())));
Expand Down
2 changes: 1 addition & 1 deletion src/utils/problem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use serde::Deserialize;
use surrealdb::{engine::remote::ws::Client, Surreal};

use crate::{models::problem::Problem, routes::problem::CreateProblem};
use crate::models::problem::{CreateProblem, Problem};

pub async fn create(db: &Surreal<Client>, problem: CreateProblem<'_>) -> Result<Option<Problem>> {
Ok(db
Expand Down
8 changes: 4 additions & 4 deletions tests/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async fn test_category() -> Result<()> {

assert!(success);

let mut new_category_id: Vec<String> = Vec::new();
let mut new_category_ids: Vec<String> = Vec::new();

for i in 0..10 {
let response = client
Expand Down Expand Up @@ -67,7 +67,7 @@ async fn test_category() -> Result<()> {

assert!(success);
println!("Created category: {}", data.id);
new_category_id.push(data.id);
new_category_ids.push(data.id);
}

let response = client
Expand All @@ -93,9 +93,9 @@ async fn test_category() -> Result<()> {
assert!(success);
println!("Listed categories: {:#?}", data);

for i in 0..10 {
for new_category_id in new_category_ids.iter().take(10) {
let response = client
.post(format!("/category/delete/{}", new_category_id[i]))
.post(format!("/category/delete/{}", new_category_id))
.json(&CreateCategory {
id: &id,
token: &token,
Expand Down
10 changes: 5 additions & 5 deletions tests/problem.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use algohub_server::{
models::{
account::Register,
problem::ProblemDetail,
problem::{CreateProblem, ProblemVisibility, UserProblem},
response::{Empty, Response},
OwnedCredentials, Token, UserRecordId,
},
routes::problem::{CreateProblem, ListProblem, ProblemResponse},
routes::problem::{ListProblem, ProblemResponse},
};
use anyhow::Result;
use rocket::local::asynchronous::Client;
Expand Down Expand Up @@ -62,7 +62,7 @@ async fn test_problem() -> Result<()> {
test_cases: vec![],
categories: vec![],
tags: vec![],
private: true,
visibility: ProblemVisibility::Public,
})
.dispatch()
.await;
Expand Down Expand Up @@ -98,7 +98,7 @@ async fn test_problem() -> Result<()> {
message: _,
data,
} = response
.into_json::<Response<Vec<ProblemDetail>>>()
.into_json::<Response<Vec<UserProblem>>>()
.await
.unwrap();
let data = data.unwrap();
Expand All @@ -123,7 +123,7 @@ async fn test_problem() -> Result<()> {
message: _,
data,
} = response
.into_json::<Response<Vec<ProblemDetail>>>()
.into_json::<Response<Vec<UserProblem>>>()
.await
.unwrap();
let data = data.unwrap();
Expand Down

0 comments on commit 2af9a74

Please sign in to comment.