Skip to content

Commit

Permalink
moved contest backend from private repo
Browse files Browse the repository at this point in the history
  • Loading branch information
siddhantk232 committed Apr 3, 2024
1 parent bbf71ad commit 1e9b410
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 39 deletions.
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dotenv_if_exists .env
use flake
2 changes: 2 additions & 0 deletions ec/Cargo.lock

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

2 changes: 2 additions & 0 deletions ec/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
http = "1.0"
bytes = "1.0"
diesel = "2"
thiserror = "1"
3 changes: 3 additions & 0 deletions ec/migrations/2024-04-03-105201_ec_tables/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS ec_contest_submission;
DROP TABLE IF EXISTS ec_contest;
22 changes: 22 additions & 0 deletions ec/migrations/2024-04-03-105201_ec_tables/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS ec_contest (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT NOT NULL,
start_at TIMESTAMP WITH TIME ZONE NOT NULL,
end_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);

CREATE TABLE IF NOT EXISTS ec_contest_submission (
id BIGSERIAL PRIMARY KEY,
-- we require fastn auth to be enabled for this to work
user_id BIGINT REFERENCES fastn_user(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
deploy_url TEXT NOT NULL,
source_url TEXT NOT NULL,
message TEXT NOT NULL,
is_winner BOOLEAN DEFAULT FALSE NOT NULL,
contest_id BIGINT REFERENCES ec_contest(id) ON DELETE CASCADE NOT NULL
);
5 changes: 5 additions & 0 deletions ec/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
extern crate self as ec;

mod route;
mod schema;
mod urls;

pub use route::contest::Contest;
pub use route::contest::ContestError;

#[no_mangle]
pub extern "C" fn main_ft() {
Expand Down
76 changes: 76 additions & 0 deletions ec/src/route/contest/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
pub mod submission;
pub use submission::{DeleteSubmissionPayload, SubmissionPayload, Submissions};

pub struct Contest {
in_: ft_sdk::In,
ud: ft_sdk::UserData,
conn: ft_sdk::PgConnection,
}

impl ft_sdk::Layout for Contest {
type Error = ContestError;

fn from_in(in_: ft_sdk::In, _ty: ft_sdk::RequestType) -> Result<Self, Self::Error> {
let conn = ft_sdk::default_pg()?;

if in_.ud.is_none() {
return Err(Self::Error::Unauthorized(
"You must be logged in to view this page".to_string(),
));
}

let ud = in_.ud.clone().expect("user must exist in now");

Ok(Self { in_, conn, ud })
}

fn json(&mut self, page: serde_json::Value) -> Result<serde_json::Value, Self::Error> {
Ok(serde_json::json!({
"page": page,
}))
}

fn render_error(err: Self::Error) -> http::Response<bytes::Bytes> {
match err {
ContestError::FormError(errors) => {
ft_sdk::println!("form error: {errors:?}");
ft_sdk::json_response(serde_json::json!({"errors": errors}))
}
ContestError::Sdk(error) => {
ft_sdk::server_error!("sdk error: {error:?}")
}
ContestError::Diesel(error) => {
ft_sdk::server_error!("diesel error: {error:?}")
}
ContestError::CantDeserializeInput(message) => {
ft_sdk::server_error!("serde error: {message:?}")
}
ContestError::Unauthorized(message) => {
ft_sdk::not_found!("unauthorized error: {message}")
}
}
}
}

#[derive(Debug, thiserror::Error)]
pub enum ContestError {
#[error("form error: {0:?}")]
FormError(std::collections::HashMap<String, String>),
#[error("sdk error: {0}")]
Sdk(#[from] ft_sdk::Error),
#[error("Diesel error: {0}")]
Diesel(#[from] diesel::result::Error),
#[error("cant deserialize input: {0}")]
CantDeserializeInput(#[from] serde_json::Error),
#[error("not authorised: {0}")]
Unauthorized(String),
}

impl ContestError {
pub fn form_error(field: &str, error: &str) -> Self {
Self::FormError(std::collections::HashMap::from([(
field.to_string(),
error.to_string(),
)]))
}
}
201 changes: 201 additions & 0 deletions ec/src/route/contest/submission.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
#[derive(serde::Serialize, Debug, diesel::Selectable, diesel::Queryable)]
#[diesel(table_name = ec::schema::ec_contest_submission)]
pub struct Submission {
id: i64,
title: String,
deploy_url: String,
source_url: String,
message: String,
user_id: i64,
is_winner: bool,
}

#[derive(serde::Serialize, Debug)]
pub struct Submissions {
submissions: Vec<Submission>,
is_admin: bool,
}

impl Submissions {
fn get_all(conn: &mut ft_sdk::PgConnection, is_admin: bool) -> Result<Self, ec::ContestError> {
use diesel::prelude::*;
use ec::schema::ec_contest_submission;

let subs: Vec<_> = ec_contest_submission::table
.select(Submission::as_select())
.load(conn)?;

Ok(Self {
submissions: subs,
is_admin,
})
}

/// get all submissions for a user
fn get_by_user(
conn: &mut ft_sdk::PgConnection,
user_id: i64,
is_admin: bool,
) -> Result<Self, ec::ContestError> {
use diesel::prelude::*;
use ec::schema::ec_contest_submission;

let subs: Vec<_> = ec_contest_submission::table
.filter(ec_contest_submission::user_id.eq(user_id))
.select(Submission::as_select())
.load(conn)?;

Ok(Self {
submissions: subs,
is_admin,
})
}

/// get a submission by id
fn get_by_id(
conn: &mut ft_sdk::PgConnection,
id: i64,
) -> Result<Option<Submission>, ec::ContestError> {
use diesel::prelude::*;
use ec::schema::ec_contest_submission;

Ok(ec_contest_submission::table
.filter(ec_contest_submission::id.eq(id))
.select(Submission::as_select())
.first(conn)
.optional()?)
}
}

impl ft_sdk::Page<ec::Contest, ec::ContestError> for Submissions {
fn page(c: &mut ec::Contest) -> Result<Self, ec::ContestError> {
let is_admin = c.ud.email.contains("fifthtry.com") && c.ud.verified_email;

if is_admin {
return Self::get_all(&mut c.conn, is_admin);
}

return Self::get_by_user(&mut c.conn, c.ud.id, is_admin);
}
}

#[derive(serde::Serialize, Debug, diesel::Insertable)]
#[diesel(table_name = ec::schema::ec_contest_submission)]
pub struct SubmissionPayload {
title: String,
deploy_url: String,
source_url: String,
message: String,
is_winner: bool,
user_id: i64,
contest_id: i64,
}

impl ft_sdk::Action<ec::Contest, ec::ContestError> for SubmissionPayload {
fn validate(c: &mut ec::Contest) -> Result<Self, ec::ContestError> {
use diesel::prelude::*;
use ec::schema::ec_contest;
pub use ft_sdk::JsonBodyExt;

let body = c.in_.req.json_body()?;

let title = get_required_json_field(&body, "title")?;
let deploy_url = get_required_json_field(&body, "deploy_url")?;
let source_url = get_required_json_field(&body, "source_url")?;
let message = get_required_json_field(&body, "message")?;

// WARN: assumes a contest with id: 1 has been populated in the db
// see: `scripts/setup-contest.py` in ft repo
let contest_id: i64 = ec_contest::table
.select(ec_contest::id)
.first(&mut c.conn)?;

Ok(Self {
title,
deploy_url,
source_url,
message,
// TODO: add method to set this
is_winner: false,
contest_id,
user_id: c.ud.id,
})
}

fn action(&self, c: &mut ec::Contest) -> Result<ft_sdk::ActionOutput, ec::ContestError> {
use diesel::prelude::*;
use ec::schema::ec_contest_submission;

let affected = diesel::insert_into(ec_contest_submission::table)
.values(self)
.execute(&mut c.conn);

ft_sdk::println!("stored submissions, affected: {:?}", affected);

Ok(ft_sdk::ActionOutput::Redirect(
ec::urls::contest_submissions_url(),
))
}
}

pub struct DeleteSubmissionPayload {
id: i64,
}

impl ft_sdk::Action<ec::Contest, ec::ContestError> for DeleteSubmissionPayload {
fn validate(c: &mut ec::Contest) -> Result<Self, ec::ContestError> {
pub use ft_sdk::JsonBodyExt;

let body = c.in_.req.json_body()?;
let id = get_required_json_field(&body, "id")?;
let id = id
.parse::<i64>()
.map_err(|_| ec::ContestError::form_error("id", "id must be a number"))?;

let sub = Submissions::get_by_id(&mut c.conn, id)?;

if sub.is_none() {
return Err(ec::ContestError::form_error("id", "submission not found"));
}

let sub = sub.expect("sub is not none");

// only the user who submitted can delete
if sub.user_id != c.ud.id {
return Err(ec::ContestError::form_error("id", "submission not found"));
}

Ok(Self { id })
}

fn action(&self, c: &mut ec::Contest) -> Result<ft_sdk::ActionOutput, ec::ContestError> {
use diesel::prelude::*;
use ec::schema::ec_contest_submission;

let affected = diesel::delete(
ec_contest_submission::table.filter(ec_contest_submission::id.eq(self.id)),
)
.execute(&mut c.conn);

ft_sdk::println!("deleted submissions, affected: {:?}", affected);

Ok(ft_sdk::ActionOutput::Redirect(
ec::urls::contest_submissions_url(),
))
}
}

fn get_required_json_field(body: &ft_sdk::JsonBody, key: &str) -> Result<String, ec::ContestError> {
let val = body.field::<String>(key)?.ok_or_else(|| {
ec::ContestError::form_error(key, format!("{} is required", key).as_str())
})?;

if val.is_empty() {
return Err(ec::ContestError::form_error(
key,
format!("{} is required", key).as_str(),
));
}

Ok(val)
}
49 changes: 10 additions & 39 deletions ec/src/route/mod.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,17 @@
pub mod contest;

pub fn route(r: http::Request<bytes::Bytes>) -> http::Response<bytes::Bytes> {
use ft_sdk::Layout;

match r.uri().path() {
// site
"/wasm-test/" => Dummy::page::<Hello>(r),
t => ft_sdk::not_found!("no route for {t}"),
}
}

struct Dummy {}

impl ft_sdk::Layout for Dummy {
type Error = ft_sdk::Error;

fn from_in(_in_: ft_sdk::In, _ty: ft_sdk::RequestType) -> Result<Self, Self::Error> {
ft_sdk::println!("from_in 1");

Ok(Self {})
}
"/ft2/contest/submissions/" => contest::Contest::page::<contest::Submissions>(r),
"/ft2/contest/submissions/new/" => {
contest::Contest::action::<contest::SubmissionPayload>(r)
}
"/ft2/contest/submissions/delete/" => {
contest::Contest::action::<contest::DeleteSubmissionPayload>(r)
}

fn json(&mut self, page: serde_json::Value) -> Result<serde_json::Value, Self::Error> {
Ok(serde_json::json!({"page": page}))
}

fn render_error(e: Self::Error) -> http::Response<bytes::Bytes> {
ft_sdk::println!("rendering error: {e:?}");
ft_sdk::json_response(serde_json::json!({
"error": e.to_string(),
}))
}
}

#[derive(serde::Serialize)]
pub struct Hello {
msg: String,
}

impl ft_sdk::Page<Dummy, ft_sdk::Error> for Hello {
fn page(_i: &mut Dummy) -> Result<Self, ft_sdk::Error> {
ft_sdk::println!("hello wasm");
Ok(Hello {
msg: "hello from ft_sdk".into(),
})
t => ft_sdk::not_found!("no route for {t}"),
}
}
Loading

0 comments on commit 1e9b410

Please sign in to comment.