generated from fastn-stack/fastn-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
moved contest backend from private repo
- Loading branch information
1 parent
bbf71ad
commit 1e9b410
Showing
11 changed files
with
355 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
dotenv_if_exists .env | ||
use flake |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
)])) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"), | ||
} | ||
} |
Oops, something went wrong.