Initial (full)
This commit is contained in:
parent
94cec33b46
commit
9dd708d276
8 changed files with 4810 additions and 0 deletions
390
src/routes.rs
Normal file
390
src/routes.rs
Normal file
|
@ -0,0 +1,390 @@
|
|||
use crate::{State, model::Entry};
|
||||
use axum::{
|
||||
Extension, Json, Router,
|
||||
extract::Path,
|
||||
response::{Html, IntoResponse},
|
||||
routing::{get, get_service, post},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tera::Context;
|
||||
use tetratto_core::{
|
||||
model::{
|
||||
ApiReturn, Error,
|
||||
apps::{AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery},
|
||||
},
|
||||
sdk::{DataClient, SimplifiedQuery},
|
||||
};
|
||||
use tetratto_shared::{
|
||||
hash::{hash, salt},
|
||||
unix_epoch_timestamp,
|
||||
};
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
.nest_service(
|
||||
"/public",
|
||||
get_service(tower_http::services::ServeDir::new("./public")),
|
||||
)
|
||||
.fallback(not_found_request)
|
||||
// pages
|
||||
.route("/", get(index_request))
|
||||
.route("/{slug}", get(view_request))
|
||||
.route("/{slug}/edit", get(editor_request))
|
||||
// api
|
||||
.route("/api/v1/render", post(render_request))
|
||||
.route("/api/v1/entries", post(create_request))
|
||||
.route("/api/v1/entries/{slug}", post(edit_request))
|
||||
}
|
||||
|
||||
fn default_context(data: &DataClient) -> Context {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert(
|
||||
"name",
|
||||
&std::env::var("NAME").unwrap_or("Attobin".to_string()),
|
||||
);
|
||||
ctx.insert("tetratto", &data.host);
|
||||
ctx
|
||||
}
|
||||
|
||||
// pages
|
||||
async fn not_found_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let (ref data, ref tera) = *data.read().await;
|
||||
|
||||
let mut ctx = default_context(&data);
|
||||
ctx.insert(
|
||||
"error",
|
||||
&Error::GeneralNotFound("page".to_string()).to_string(),
|
||||
);
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
|
||||
async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
|
||||
let (ref data, ref tera) = *data.read().await;
|
||||
Html(tera.render("index.lisp", &default_context(&data)).unwrap())
|
||||
}
|
||||
|
||||
async fn view_request(
|
||||
Extension(data): Extension<State>,
|
||||
Path(slug): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let (ref data, ref tera) = *data.read().await;
|
||||
let entry = match data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries('{}')", slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => match r {
|
||||
AppDataQueryResult::One(r) => serde_json::from_str::<Entry>(&r.value).unwrap(),
|
||||
AppDataQueryResult::Many(_) => unreachable!(),
|
||||
},
|
||||
Err(_) => {
|
||||
let mut ctx = default_context(&data);
|
||||
ctx.insert(
|
||||
"error",
|
||||
&Error::GeneralNotFound("entry".to_string()).to_string(),
|
||||
);
|
||||
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
let (views_id, views) = match data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => match r {
|
||||
AppDataQueryResult::One(r) => (r.id, r.value.parse::<usize>().unwrap()),
|
||||
AppDataQueryResult::Many(_) => unreachable!(),
|
||||
},
|
||||
Err(e) => {
|
||||
let mut ctx = default_context(&data);
|
||||
ctx.insert("error", &e.to_string());
|
||||
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
// count view
|
||||
if let Err(e) = data.update(views_id, (views + 1).to_string()).await {
|
||||
let mut ctx = default_context(&data);
|
||||
ctx.insert("error", &e.to_string());
|
||||
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
|
||||
// ...
|
||||
let mut ctx = default_context(&data);
|
||||
|
||||
ctx.insert("entry", &entry);
|
||||
ctx.insert("views", &views);
|
||||
|
||||
Html(tera.render("view.lisp", &ctx).unwrap())
|
||||
}
|
||||
|
||||
async fn editor_request(
|
||||
Extension(data): Extension<State>,
|
||||
Path(slug): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let (ref data, ref tera) = *data.read().await;
|
||||
let entry = match data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries('{}')", slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => match r {
|
||||
AppDataQueryResult::One(r) => serde_json::from_str::<Entry>(&r.value).unwrap(),
|
||||
AppDataQueryResult::Many(_) => unreachable!(),
|
||||
},
|
||||
Err(_) => {
|
||||
let mut ctx = default_context(&data);
|
||||
ctx.insert(
|
||||
"error",
|
||||
&Error::GeneralNotFound("entry".to_string()).to_string(),
|
||||
);
|
||||
|
||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
||||
}
|
||||
};
|
||||
|
||||
// ...
|
||||
let mut ctx = default_context(&data);
|
||||
ctx.insert("entry", &entry);
|
||||
|
||||
Html(tera.render("edit.lisp", &ctx).unwrap())
|
||||
}
|
||||
|
||||
// api
|
||||
#[derive(Deserialize)]
|
||||
struct RenderMarkdown {
|
||||
content: String,
|
||||
}
|
||||
|
||||
async fn render_request(Json(req): Json<RenderMarkdown>) -> impl IntoResponse {
|
||||
tetratto_shared::markdown::render_markdown(&req.content)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateEntry {
|
||||
content: String,
|
||||
#[serde(default = "default_random")]
|
||||
slug: String,
|
||||
#[serde(default = "default_random")]
|
||||
edit_code: String,
|
||||
}
|
||||
|
||||
fn default_random() -> String {
|
||||
salt()
|
||||
}
|
||||
|
||||
async fn create_request(
|
||||
Extension(data): Extension<State>,
|
||||
Json(req): Json<CreateEntry>,
|
||||
) -> impl IntoResponse {
|
||||
let (ref data, _) = *data.read().await;
|
||||
|
||||
// check lengths
|
||||
if req.slug.len() < 2 {
|
||||
return Json(Error::DataTooShort("slug".to_string()).into());
|
||||
}
|
||||
|
||||
if req.slug.len() > 32 {
|
||||
return Json(Error::DataTooLong("slug".to_string()).into());
|
||||
}
|
||||
|
||||
// check for existing
|
||||
if data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries('{}')", req.slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Json(Error::MiscError("Slug already in use".to_string()).into());
|
||||
}
|
||||
|
||||
// create
|
||||
let created = unix_epoch_timestamp();
|
||||
let salt = salt();
|
||||
|
||||
if let Err(e) = data
|
||||
.insert(
|
||||
format!("entries('{}')", req.slug),
|
||||
serde_json::to_string(&Entry {
|
||||
slug: req.slug.clone(),
|
||||
edit_code: hash(req.edit_code.clone() + &salt),
|
||||
salt,
|
||||
created,
|
||||
edited: created,
|
||||
content: req.content,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
if let Err(e) = data
|
||||
.insert(format!("entries.views('{}')", req.slug), 0.to_string())
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
// return
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: Some((req.slug, req.edit_code)),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditEntry {
|
||||
content: String,
|
||||
edit_code: String,
|
||||
#[serde(default)]
|
||||
new_slug: Option<String>,
|
||||
#[serde(default)]
|
||||
new_edit_code: Option<String>,
|
||||
#[serde(default)]
|
||||
delete: bool,
|
||||
}
|
||||
|
||||
async fn edit_request(
|
||||
Extension(data): Extension<State>,
|
||||
Path(slug): Path<String>,
|
||||
Json(req): Json<EditEntry>,
|
||||
) -> impl IntoResponse {
|
||||
let (ref data, _) = *data.read().await;
|
||||
|
||||
let (id, mut entry) = match data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries('{}')", slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => match r {
|
||||
AppDataQueryResult::One(r) => (r.id, serde_json::from_str::<Entry>(&r.value).unwrap()),
|
||||
AppDataQueryResult::Many(_) => unreachable!(),
|
||||
},
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
// check edit code
|
||||
if hash(req.edit_code.clone() + &entry.salt) != entry.edit_code {
|
||||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
// handle delete
|
||||
if req.delete {
|
||||
let views_id = match data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => match r {
|
||||
AppDataQueryResult::One(r) => r.id,
|
||||
AppDataQueryResult::Many(_) => unreachable!(),
|
||||
},
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
return match data.remove(id).await {
|
||||
Ok(_) => match data.remove(views_id).await {
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: None,
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
},
|
||||
Err(e) => Json(e.into()),
|
||||
};
|
||||
}
|
||||
|
||||
// check edited slug and edit code
|
||||
if let Some(new_slug) = req.new_slug {
|
||||
if new_slug.len() < 2 {
|
||||
return Json(Error::DataTooShort("slug".to_string()).into());
|
||||
}
|
||||
|
||||
if new_slug.len() > 32 {
|
||||
return Json(Error::DataTooLong("slug".to_string()).into());
|
||||
}
|
||||
|
||||
// check for existing
|
||||
if data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries('{}')", new_slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Json(Error::MiscError("Slug already in use".to_string()).into());
|
||||
}
|
||||
|
||||
let views_id = match data
|
||||
.query(&SimplifiedQuery {
|
||||
query: AppDataSelectQuery::KeyIs(format!("entries.views('{}')", slug)),
|
||||
mode: AppDataSelectMode::One(0),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(r) => match r {
|
||||
AppDataQueryResult::One(r) => r.id,
|
||||
AppDataQueryResult::Many(_) => unreachable!(),
|
||||
},
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
// rename
|
||||
if let Err(e) = data.rename(id, format!("entries('{}')", new_slug)).await {
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
if let Err(e) = data
|
||||
.rename(views_id, format!("entries.views('{}')", new_slug))
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
entry.slug = new_slug;
|
||||
}
|
||||
|
||||
if let Some(new_edit_code) = req.new_edit_code {
|
||||
entry.salt = salt();
|
||||
entry.edit_code = hash(new_edit_code + &entry.salt);
|
||||
}
|
||||
|
||||
// update
|
||||
entry.content = req.content;
|
||||
entry.edited = unix_epoch_timestamp();
|
||||
|
||||
if let Err(e) = data
|
||||
.update(id, serde_json::to_string(&entry).unwrap())
|
||||
.await
|
||||
{
|
||||
return Json(e.into());
|
||||
}
|
||||
|
||||
// return
|
||||
Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Success".to_string(),
|
||||
payload: Some(entry.slug),
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue