diff --git a/src/main.rs b/src/main.rs index 4ce7a5e..f72a183 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,7 @@ async fn main() -> anyhow::Result<()> { .route("/history", get(history_page)) .route("/dashboard", get(dashboard_page)) .route("/s/{token}", get(shared_paper)) + .route("/paper/{id}", get(paper_page)) .route("/api/attempt/{attempt}/state", get(attempt_state)) .route("/api/attempt/{attempt}/submit", post(submit_api)) .nest_service("/static", get_service(ServeDir::new(static_dir))) @@ -575,7 +576,26 @@ async fn dashboard_page(State(ctx): State, h: HeaderMap) -> axum::respon hx(views::dashboard(&ctx, &name, &tests, &weak, ¬es, (tests.len() as i32, marks, gained, lost), &mistakes), StatusCode::OK).into_response() } -async fn shared_paper(State(ctx): State, Path(token): Path) -> axum::response::Response { +/// Owner view of a paper: redirects to the attempt screen so the paper can +/// be taken. Private — only the owning user may resolve their paper id. +async fn paper_page(State(ctx): State, h: HeaderMap, Path(id): Path) -> axum::response::Response { + let (uid, _name) = match ctx.require_user(&h).await { + Ok(v) => v, + Err(r) => return r, + }; + let owned: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM papers WHERE id=? AND user_id=?") + .bind(&id) + .bind(&uid) + .fetch_one(&ctx.pool) + .await + .unwrap_or(0); + if owned == 0 { + return hx(views::error(&ctx, 404, ""), StatusCode::NOT_FOUND).into_response(); + } + Redirect::to(&format!("{}/attempt/{id}", ctx.base)).into_response() +} + +async fn shared_paper(State(ctx): State, h: HeaderMap, Path(token): Path) -> axum::response::Response { let row = sqlx::query("SELECT id, title, duration_min FROM papers WHERE share_token=?").bind(&token).fetch_optional(&ctx.pool).await; let (id, title, duration) = match row { Ok(Some(r)) => { @@ -588,7 +608,8 @@ async fn shared_paper(State(ctx): State, Path(token): Path) -> a Ok(v) => v, Err(_) => return hx(views::error(&ctx, 500, ""), StatusCode::INTERNAL_SERVER_ERROR).into_response(), }; - hx(views::shared(&ctx, &title, duration, &questions), StatusCode::OK).into_response() + let signed = auth::user_id(&ctx.pool, &h).await.is_some(); + hx(views::shared(&ctx, &title, duration, &questions, &id, &token, signed), StatusCode::OK).into_response() } const TERMS: &str = include_str!("../content/terms.html"); diff --git a/src/views.rs b/src/views.rs index 29ccaf3..4a36ea8 100644 --- a/src/views.rs +++ b/src/views.rs @@ -904,7 +904,7 @@ pub fn dashboard(ctx: &Ctx, user: &str, tests: &[TestRow], weak: &[(Bucket, Stri shell(ctx, Some(user), "Dashboard", body) } -pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question]) -> String { +pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question], paper_id: &str, token: &str, signed: bool) -> String { let qs: String = questions .iter() .enumerate() @@ -928,12 +928,26 @@ pub fn shared(ctx: &Ctx, title: &str, duration: i64, questions: &[Question]) -> ) }) .collect(); + let cta = if signed { + format!( + r#"
"#, + b = ctx.base, + id = esc(paper_id) + ) + } else { + format!( + r#"

sign in to take this paper

"#, + b = ctx.base, + tok = esc(token) + ) + }; let body = format!( - r#"

{title}

{count} questions · {dur} minutes · no answers or scores on this page.

{qs}
"#, + r#"

{title}

{count} questions · {dur} minutes · no answers or scores on this page.

{cta}
{qs}
"#, title = esc(title), count = questions.len(), dur = duration, - qs = qs + qs = qs, + cta = cta ); shell(ctx, None, title, body) } \ No newline at end of file