Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(problem): support safely filter assets #40

Merged
merged 6 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading