diff --git a/client/vite.config.ts b/client/vite.config.ts
index 80864b9..157b5a2 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -1,6 +1,15 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
+import { fileURLToPath } from "url";
+import { readFileSync } from "fs";
+
+const file = fileURLToPath(new URL("package.json", import.meta.url));
+const json = readFileSync(file, "utf8");
+const pkg = JSON.parse(json);
export default defineConfig({
plugins: [sveltekit()],
+ define: {
+ PKG: pkg,
+ },
});
diff --git a/server/Cargo.lock b/server/Cargo.lock
index 630e61c..6bd25f7 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -80,6 +80,7 @@ dependencies = [
"matchit",
"memchr",
"mime",
+ "multer",
"percent-encoding",
"pin-project-lite",
"rustversion",
@@ -809,6 +810,24 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "multer"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
+dependencies = [
+ "bytes",
+ "encoding_rs",
+ "futures-util",
+ "http 0.2.11",
+ "httparse",
+ "log",
+ "memchr",
+ "mime",
+ "spin",
+ "version_check",
+]
+
[[package]]
name = "native-tls"
version = "0.2.11"
@@ -1268,6 +1287,12 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
[[package]]
name = "subtle"
version = "2.5.0"
@@ -1641,7 +1666,7 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "watson-server"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"argon2",
"axum",
diff --git a/server/Cargo.toml b/server/Cargo.toml
index ff586a4..8a38578 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "watson-server"
-version = "0.1.0"
+version = "0.1.1"
edition = "2021"
[profile.release]
@@ -13,7 +13,7 @@ dotenv = ["dep:dotenvy"]
[dependencies]
argon2 = "0.5.3"
-axum = "0.7.2"
+axum = { version = "0.7.2", features = ["multipart"] }
chrono = { version = "0.4.31", features = ["serde"] }
diesel = { version = "2.1.0", features = ["postgres", "chrono", "uuid"] }
diesel_migrations = "2.1.0"
diff --git a/server/migrations/2024-01-26-171232_accept_images/down.sql b/server/migrations/2024-01-26-171232_accept_images/down.sql
new file mode 100644
index 0000000..0fed6cb
--- /dev/null
+++ b/server/migrations/2024-01-26-171232_accept_images/down.sql
@@ -0,0 +1,5 @@
+ALTER TABLE problems ALTER COLUMN body SET NOT NULL;
+ALTER TABLE solutions ALTER COLUMN body SET NOT NULL;
+
+ALTER TABLE problems DROP COLUMN img_path;
+ALTER TABLE solutions DROP COLUMN img_path;
diff --git a/server/migrations/2024-01-26-171232_accept_images/up.sql b/server/migrations/2024-01-26-171232_accept_images/up.sql
new file mode 100644
index 0000000..7ef5019
--- /dev/null
+++ b/server/migrations/2024-01-26-171232_accept_images/up.sql
@@ -0,0 +1,5 @@
+ALTER TABLE problems ALTER COLUMN body DROP NOT NULL;
+ALTER TABLE solutions ALTER COLUMN body DROP NOT NULL;
+
+ALTER TABLE problems ADD COLUMN img_path VARCHAR;
+ALTER TABLE solutions ADD COLUMN img_path VARCHAR;
diff --git a/server/src/auth.rs b/server/src/auth.rs
index c7bd14e..fefb064 100644
--- a/server/src/auth.rs
+++ b/server/src/auth.rs
@@ -114,7 +114,7 @@ pub async fn register(
))
.execute(&mut conn)
.map_err(internal_error)?;
- todo!()
+ Ok(())
}
#[derive(Deserialize)]
diff --git a/server/src/main.rs b/server/src/main.rs
index 90c5ea4..df1cfda 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -2,10 +2,17 @@ mod auth;
mod models;
mod schema;
-use std::{cmp::Ordering, collections::HashMap, env, error::Error, net::SocketAddr, sync::Arc};
+use std::{
+ cmp::Ordering, collections::HashMap, env, error::Error, fs, net::SocketAddr, path::PathBuf,
+ sync::Arc,
+};
use axum::{
- http::{HeaderMap, StatusCode},
+ extract::Multipart,
+ http::{
+ header::{AUTHORIZATION, CONTENT_TYPE},
+ HeaderMap, StatusCode,
+ },
middleware,
response::Json,
routing::{get, post, put},
@@ -70,6 +77,7 @@ async fn main() {
.route("/solutions", post(submit_solution))
.route("/modules", get(get_modules))
.route("/leaderboard", get(get_leaderboard))
+ .route("/upload", post(upload))
.route_layer(middleware::from_fn_with_state(
Arc::clone(&sessions),
auth::auth,
@@ -79,7 +87,7 @@ async fn main() {
.layer(
CorsLayer::new()
.allow_methods(Any)
- .allow_headers(Any)
+ .allow_headers([CONTENT_TYPE, AUTHORIZATION])
.allow_origin(origins),
)
.with_state(sessions);
@@ -89,6 +97,28 @@ async fn main() {
axum::serve(listener, app).await.unwrap();
}
+/// Endpoint to upload a file (image) which is then stored on the server.
+async fn upload(mut multipart: Multipart) -> Result
{
+ // For now, you can only upload one file at a time.
+ let Some(field) = multipart.next_field().await.map_err(internal_error)? else {
+ return Err((StatusCode::BAD_REQUEST, "No file uploaded.".to_string()));
+ };
+
+ let name = Uuid::new_v4().to_string() + field.file_name().unwrap_or("");
+ let data = field.bytes().await.map_err(internal_error)?;
+
+ let store_path: PathBuf = [
+ env::var("MEDIA_PATH").expect("MEDIA_PATH must be set"),
+ name.clone(),
+ ]
+ .iter()
+ .collect();
+
+ fs::write(store_path.clone(), data).map_err(internal_error)?;
+
+ Ok(name)
+}
+
async fn get_modules() -> Result, (StatusCode, String)> {
use schema::{modules, topics};
let mut conn = establish_connection();
@@ -164,7 +194,8 @@ async fn get_leaderboard() -> Result>, (StatusCode, S
#[diesel(table_name = schema::solutions)]
struct SubmitSolution {
problem_id: i32,
- body: String,
+ body: Option,
+ img_path: Option,
}
async fn submit_solution(
@@ -189,6 +220,8 @@ async fn create_problem(
let user_id = extract_user_id(&headers)?;
let mut conn = establish_connection();
+ assert!(new_problem.problem.body.is_some() || new_problem.problem.img_path.is_some());
+
let module_id = match new_problem.module {
AddModule::Existing(id) => id,
AddModule::New(title) => diesel::insert_into(modules::table)
@@ -212,10 +245,11 @@ async fn create_problem(
.get_result(&mut conn)
.map_err(internal_error)?;
- if let Some(soln) = new_problem.soln {
+ if new_problem.soln.is_some() || new_problem.soln_img.is_some() {
diesel::insert_into(solutions::table)
.values((
- solutions::body.eq(soln),
+ solutions::body.eq(new_problem.soln),
+ solutions::img_path.eq(new_problem.soln_img),
solutions::problem_id.eq(result.id),
solutions::user_id.eq(user_id),
))
@@ -278,6 +312,7 @@ struct ProblemRequest {
struct ProblemResponse {
problem: Option,
solution: Option,
+ solution_img: Option,
}
async fn request_problem(
@@ -299,7 +334,6 @@ async fn request_problem(
ProblemTopic::belonging_to(&selected_topics)
.inner_join(problems::table.left_join(user_problem::table.inner_join(users::table)))
.filter(users::id.eq(user_id).or(users::id.is_null()))
- //.distinct_on(problems::id)
.select((
problem_topic::topic_id,
(
@@ -405,16 +439,20 @@ async fn request_problem(
}
};
- let solution: Option = next_problem.as_ref().and_then(|problem| {
- Solution::belonging_to(problem)
- .select(solutions::body)
- .first(&mut conn)
- .ok()
- });
+ let (solution, solution_img): (Option, Option) = next_problem
+ .as_ref()
+ .and_then(|problem| {
+ Solution::belonging_to(problem)
+ .select((solutions::body.nullable(), solutions::img_path.nullable()))
+ .first(&mut conn)
+ .ok()
+ })
+ .unwrap_or((None, None));
Ok(Json(ProblemResponse {
problem: next_problem,
solution,
+ solution_img,
}))
}
diff --git a/server/src/models.rs b/server/src/models.rs
index daf2678..917e5d7 100644
--- a/server/src/models.rs
+++ b/server/src/models.rs
@@ -22,12 +22,13 @@ pub struct AccessToken {
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Problem {
pub id: i32,
- pub body: String,
+ pub body: Option,
pub author: Option,
pub source: Option,
pub solnlink: Option,
pub submitted_at: NaiveDateTime,
pub user_id: Option,
+ pub img_path: Option,
}
#[derive(
@@ -40,9 +41,10 @@ pub struct Problem {
pub struct Solution {
pub id: i32,
pub problem_id: i32,
- pub body: String,
+ pub body: Option,
pub submitted_at: NaiveDateTime,
pub user_id: Option,
+ pub img_path: Option,
}
#[derive(Identifiable, Queryable, Selectable, Associations, Debug)]
@@ -100,6 +102,7 @@ pub struct NewProblem {
pub module: AddModule,
pub topic: AddTopic,
pub soln: Option,
+ pub soln_img: Option,
#[serde(flatten)]
pub problem: InsertProblem,
}
@@ -107,10 +110,11 @@ pub struct NewProblem {
#[derive(Insertable, Deserialize)]
#[diesel(table_name = problems)]
pub struct InsertProblem {
- pub body: String,
+ pub body: Option,
pub author: Option,
pub source: Option,
pub solnlink: Option,
+ pub img_path: Option,
}
#[derive(Insertable)]
diff --git a/server/src/schema.rs b/server/src/schema.rs
index a139d4b..08f7df2 100644
--- a/server/src/schema.rs
+++ b/server/src/schema.rs
@@ -25,12 +25,13 @@ diesel::table! {
diesel::table! {
problems (id) {
id -> Int4,
- body -> Text,
+ body -> Nullable,
author -> Nullable,
source -> Nullable,
solnlink -> Nullable,
submitted_at -> Timestamp,
user_id -> Nullable,
+ img_path -> Nullable,
}
}
@@ -38,10 +39,11 @@ diesel::table! {
solutions (id) {
id -> Int4,
problem_id -> Int4,
- body -> Text,
+ body -> Nullable,
author -> Nullable,
submitted_at -> Timestamp,
user_id -> Nullable,
+ img_path -> Nullable,
}
}