use askama::Template;
use axum::{
Form, Router,
extract::{Path, State},
http::{StatusCode, Uri},
response::{Html, IntoResponse, Redirect, Response},
routing::{get, post},
};
use ocre::{Ctx, Error, FieldError, Flash, OptionExt, Result, Session, render};
use serde::Deserialize;
use crate::auth::{CurrentUser, OptionalUser};
use crate::models::event::{self, Event, EventChanges, NewEvent};
use crate::models::question::{self, Question};
use crate::models::user::User;
use crate::questions::{self, QuestionForm};
pub fn routes() -> Router<Ctx> {
Router::new()
.route("/events", get(index).post(create))
.route("/events/new", get(new))
.route("/events/{id}", get(show).post(update))
.route("/events/{id}/edit", get(edit))
.route("/events/{id}/delete", post(delete))
}
pub mod paths {
use std::fmt::Display;
pub fn index() -> &'static str {
"/events"
}
pub fn new() -> &'static str {
"/events/new"
}
pub fn show(id: impl Display) -> String {
format!("/events/{id}")
}
pub fn edit(id: impl Display) -> String {
format!("/events/{id}/edit")
}
pub fn delete(id: impl Display) -> String {
format!("/events/{id}/delete")
}
}
struct Id(i64, String);
impl axum::extract::FromRequestParts<Ctx> for Id {
type Rejection = ocre::Error;
async fn from_request_parts(parts: &mut axum::http::request::Parts, ctx: &Ctx) -> Result<Self, Self::Rejection> {
let Path(params) =
Path::<Vec<(String, String)>>::from_request_parts(parts, ctx).await.map_err(|_| ocre::Error::NotFound)?;
let key = params.into_iter().find(|(name, _)| name == "id").map(|(_, value)| value).or_404()?;
let record = event::find_by_public_id(ctx, &key).await?.or_404()?;
Ok(Self(record.id, key))
}
}
async fn hosted(ctx: &Ctx, user: &User, id: i64) -> Result<Event> {
let record = event::find(ctx, id).await?.or_404()?;
if record.user_id != user.id {
return Err(Error::Forbidden);
}
Ok(record)
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct EventForm {
pub name: String,
}
impl EventForm {
fn from_record(record: &Event) -> Self {
Self { name: record.name.clone() }
}
fn to_new(&self, user_id: i64) -> Result<NewEvent> {
let new = NewEvent { name: self.name.clone(), user_id };
new.validate().finish()?;
Ok(new)
}
fn to_changes(&self) -> Result<EventChanges> {
let changes = EventChanges { name: Some(self.name.clone()), user_id: None };
changes.validate().finish()?;
Ok(changes)
}
}
#[derive(Template)]
#[template(path = "events/index.html")]
struct IndexView {
flash: Flash,
events: Vec<Event>,
}
#[derive(Template)]
#[template(path = "events/show.html")]
struct ShowView {
flash: Flash,
event: Event,
host: bool,
link: String,
questions: Vec<Question>,
form: QuestionForm,
errors: Vec<FieldError>,
}
impl ShowView {
fn channel(&self) -> String {
if self.host { questions::host_channel(&self.event) } else { questions::channel(&self.event) }
}
}
#[derive(Template)]
#[template(path = "events/new.html")]
struct NewView {
form: EventForm,
errors: Vec<FieldError>,
}
#[derive(Template)]
#[template(path = "events/edit.html")]
struct EditView {
id: String,
form: EventForm,
errors: Vec<FieldError>,
}
async fn index(State(ctx): State<Ctx>, CurrentUser(user): CurrentUser, flash: Flash) -> Result<Html<String>> {
render(&IndexView { flash, events: event::for_users(&ctx, &[user.id]).await? })
}
async fn show(
State(ctx): State<Ctx>,
flash: Flash,
OptionalUser(user): OptionalUser,
uri: Uri,
Id(id, ..): Id,
) -> Result<Html<String>> {
let record = event::find(&ctx, id).await?.or_404()?;
room(&ctx, flash, &uri, record, user, QuestionForm::default(), vec![]).await
}
pub async fn room(
ctx: &Ctx,
flash: Flash,
uri: &Uri,
event: Event,
user: Option<User>,
form: QuestionForm,
errors: Vec<FieldError>,
) -> Result<Html<String>> {
let host = user.is_some_and(|user| user.id == event.user_id);
let link = format!("{}{}", crate::auth::origin(uri), paths::show(&event.public_id));
let questions = question::for_event(ctx, event.id).await?;
render(&ShowView { flash, event, host, link, questions, form, errors })
}
async fn new(CurrentUser(_): CurrentUser) -> Result<Html<String>> {
render(&NewView { form: EventForm::default(), errors: vec![] })
}
async fn create(
State(ctx): State<Ctx>,
CurrentUser(user): CurrentUser,
session: Session,
Form(form): Form<EventForm>,
) -> Result<Response> {
let created = match form.to_new(user.id) {
Ok(new) => event::create(&ctx, new).await,
Err(err) => Err(err),
};
match created {
Ok(record) => {
session.flash("notice", "Your event is ready: share its link.")?;
Ok(Redirect::to(&paths::show(&record.public_id)).into_response())
}
Err(Error::Invalid(errors)) => {
Ok((StatusCode::UNPROCESSABLE_ENTITY, render(&NewView { form, errors })?).into_response())
}
Err(err) => Err(err),
}
}
async fn edit(State(ctx): State<Ctx>, CurrentUser(user): CurrentUser, Id(id, key): Id) -> Result<Html<String>> {
let record = hosted(&ctx, &user, id).await?;
render(&EditView { id: key, form: EventForm::from_record(&record), errors: vec![] })
}
async fn update(
State(ctx): State<Ctx>,
CurrentUser(user): CurrentUser,
session: Session,
Id(id, key): Id,
Form(form): Form<EventForm>,
) -> Result<Response> {
hosted(&ctx, &user, id).await?;
let updated = match form.to_changes() {
Ok(changes) => event::update(&ctx, id, changes).await,
Err(err) => Err(err),
};
match updated {
Ok(record) => {
let record = record.or_404()?;
session.flash("notice", "Event was successfully updated.")?;
Ok(Redirect::to(&paths::show(&record.public_id)).into_response())
}
Err(Error::Invalid(errors)) => {
Ok((StatusCode::UNPROCESSABLE_ENTITY, render(&EditView { id: key, form, errors })?).into_response())
}
Err(err) => Err(err),
}
}
async fn delete(
State(ctx): State<Ctx>,
CurrentUser(user): CurrentUser,
session: Session,
Id(id, ..): Id,
) -> Result<Redirect> {
hosted(&ctx, &user, id).await?;
if !event::delete(&ctx, id).await? {
return Err(Error::NotFound);
}
session.flash("notice", "Event was successfully destroyed.")?;
Ok(Redirect::to(paths::index()))
}