Switched from warp to tide

This commit is contained in:
Pascal Engélibert 2020-12-17 10:05:26 +01:00
parent 9baa8c1a0f
commit 6c0af083d5
10 changed files with 1336 additions and 828 deletions

1314
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,18 +5,16 @@ authors = ["Pascal Engélibert <tuxmain@zettascript.org>"]
edition = "2018"
[dependencies]
async-std = { version = "1.6.0", features = ["attributes"] }
bincode = "1.3.1"
bs58 = "0.4.0"
dirs = "3.0.1"
handlebars = "3.5.1"
hex = "0.4.2"
http = "0.2.1"
rand = "0.7.3"
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.60"
sha2 = "0.9.2"
sled = "0.34.6"
structopt = "0.3.21"
tokio = { version = "0.2.24", features = ["macros", "rt-threaded"] }
urlencoding = "1.1.1"
warp = { version = "0.2.5", default-features = false }
tide = { version = "0.15.0", default-features = false, features = ["h1-server", "cookies", "logger"] }

View File

@ -3,6 +3,7 @@
mod cli;
mod config;
mod db;
mod queries;
mod server;
mod static_files;
mod templates;
@ -10,7 +11,7 @@ mod utils;
use structopt::StructOpt;
#[tokio::main]
#[async_std::main]
async fn main() {
let opt = cli::MainOpt::from_args();

50
src/queries.rs Normal file
View File

@ -0,0 +1,50 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NewAdQuery {
pub author: String,
pub price: String,
pub psw: String,
pub pubkey: String,
pub quantity: String,
pub title: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct RmAdQuery {
pub ad: String,
pub psw: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AdminRmAdQuery {
pub ad: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AdminLoginQuery {
pub psw: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "a")]
pub enum Query {
#[serde(rename = "new_ad")]
NewAdQuery(NewAdQuery),
#[serde(rename = "rm_ad")]
RmAdQuery(RmAdQuery),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "a")]
pub enum AdminQuery {
#[serde(rename = "login")]
LoginQuery(AdminLoginQuery),
#[serde(rename = "rm_ad")]
RmAdQuery(AdminRmAdQuery),
}
#[derive(Debug, Deserialize)]
struct SelectAdQuery {
pub ad: String,
}

View File

@ -1,430 +1,350 @@
use super::{cli, config, db, static_files, templates, utils::*};
use super::{cli, config, db, queries::*, static_files, templates::*, utils::*};
use handlebars::to_json;
use serde::{Deserialize, Serialize};
use serde_json::value::Value as Json;
use sha2::{Digest, Sha512Trunc256};
use std::{
convert::{TryFrom, TryInto},
sync::{Arc, RwLock},
sync::Arc,
};
use warp::Filter;
use warp::Reply;
type PasswordHash = [u8; 32];
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)]
struct AdId([u8; 16]);
impl AdId {
fn random() -> Self {
Self(rand::random())
}
}
impl AsRef<[u8]> for AdId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl TryFrom<&[u8]> for AdId {
type Error = std::array::TryFromSliceError;
fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
Ok(Self(v.try_into()?))
}
}
pub async fn start_server(
config: config::Config,
dbs: db::Dbs,
templates: templates::Templates<'static>,
templates: Templates<'static>,
opt: cli::MainOpt,
) {
let templates = Arc::new(templates);
let cache_ads = Arc::new(RwLock::new(to_json(
dbs.ads
.iter()
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
Some(AdWithId {
id: hex::encode(ad_id.as_ref()),
ad: bincode::deserialize::<Ad>(&ad).ok()?,
})
})
.collect::<Vec<AdWithId>>(),
)));
let config = Arc::new(config);
let dbs = Arc::new(dbs);
let handle_index = {
let cache_ads = cache_ads.clone();
let mut app = tide::new();
app.at("/static")
.serve_dir(opt.dir.0.join(static_files::STATIC_DIR))
.unwrap();
app.at("/").get({
let templates = templates.clone();
move |errors: &[ErrorTemplate], new_ad_form_refill| {
warp::reply::html(
templates
.hb
.render(
"index.html",
&IndexTemplate {
lang: "fr",
errors,
ads: &*cache_ads.read().unwrap(),
new_ad_form_refill,
},
)
.unwrap_or_else(|e| e.to_string()),
)
.into_response()
}
};
let handle_admin_login = {
let dbs = dbs.clone();
move |req: tide::Request<()>| serve_index(req, templates.clone(), dbs.clone(), &[], None)
});
app.at("/").post({
let templates = templates.clone();
move |errors: &[ErrorTemplate]| {
warp::reply::html(
templates
.hb
.render(
"admin_login.html",
&AdminLoginTemplate { lang: "fr", errors },
)
.unwrap_or_else(|e| e.to_string()),
)
.into_response()
}
};
let handle_admin = {
let cache_ads = cache_ads.clone();
move |errors: &[ErrorTemplate]| {
warp::reply::html(
templates
.hb
.render(
"admin.html",
&AdminTemplate {
lang: "fr",
errors,
ads: &*cache_ads.read().unwrap(),
},
)
.unwrap_or_else(|e| e.to_string()),
)
.into_response()
}
};
let route_static = warp::path("static")
.and(warp::get())
.and(warp::fs::dir(opt.dir.0.join(static_files::STATIC_DIR)));
let handle_new_ad = {
let handle_index = handle_index.clone();
let dbs = dbs.clone();
let cache_ads = cache_ads.clone();
move |query: NewAdQuery| {
let mut hasher = Sha512Trunc256::new();
hasher.update(&query.psw);
dbs.ads
.insert(
AdId::random(),
bincode::serialize(&Ad {
pubkey: if query.pubkey.is_empty() {
None
} else {
Some(match format_pubkey(&query.pubkey) {
Ok(pubkey) => pubkey,
Err(e) => return handle_index(&[ErrorTemplate{text: match e {
PubkeyDecodeError::BadChecksum => "La somme de contrôle de la clé publique est incorrecte.",
PubkeyDecodeError::BadFormat => "Le format de la clé publique est incorrect.",
}}], Some(query))
})
},
author: query.author,
password: hasher.finalize()[..].try_into().unwrap(),
price: query.price,
quantity: query.quantity,
time: 0,
title: query.title,
})
.unwrap(),
)
.unwrap();
dbs.ads.flush().unwrap();
let mut cache_ads = cache_ads.write().unwrap();
*cache_ads = to_json(
dbs.ads
.iter()
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
Some(AdWithId {
id: hex::encode(ad_id.as_ref()),
ad: bincode::deserialize::<Ad>(&ad).ok()?,
})
})
.collect::<Vec<AdWithId>>(),
);
drop(cache_ads);
redirect_302("/").into_response()
}
};
let handle_rm_ad = {
let handle_index = handle_index.clone();
move |req: tide::Request<()>| handle_post_index(req, templates.clone(), dbs.clone())
});
app.at("/ad/:ad").get({
let templates = templates.clone();
let dbs = dbs.clone();
let cache_ads = cache_ads.clone();
move |query: RmAdQuery| {
let mut hasher = Sha512Trunc256::new();
hasher.update(query.psw);
let password: PasswordHash = hasher.finalize()[..].try_into().unwrap();
/*query
.ads
.into_iter()
.filter_map(|x| Some(AdId::try_from(hex::decode(x).ok()?.as_ref()).ok()?))
.for_each(|ad_id| {
if let Some(raw) = dbs.ads.get(&ad_id).unwrap() {
if let Ok(ad) = bincode::deserialize::<Ad>(&raw) {
if ad.password == password {
dbs.ads.remove(&ad_id).unwrap();
}
}
}
});*/
if let Ok(ad_id) = hex::decode(query.ad) {
if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) {
if let Some(raw) = dbs.ads.get(&ad_id).unwrap() {
if let Ok(ad) = bincode::deserialize::<Ad>(&raw) {
if ad.password == password {
dbs.ads.remove(&ad_id).unwrap();
dbs.ads.flush().unwrap();
let mut cache_ads = cache_ads.write().unwrap();
*cache_ads = to_json(
dbs.ads
.iter()
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
Some(AdWithId {
id: hex::encode(ad_id.as_ref()),
ad: bincode::deserialize::<Ad>(&ad).ok()?,
})
})
.collect::<Vec<AdWithId>>(),
);
} else {
return handle_index(
&[ErrorTemplate {
text: "Le mot de passe de l'annonce est incorrect.",
}],
None,
);
}
}
}
}
}
redirect_302("/").into_response()
move |req: tide::Request<()>| serve_index(req, templates.clone(), dbs.clone(), &[], None)
});
app.at("/admin").get({
let config = config.clone();
let templates = templates.clone();
let dbs = dbs.clone();
move |req: tide::Request<()>| {
handle_admin(req, config.clone(), templates.clone(), dbs.clone())
}
};
});
app.at("/admin").post({
let config = config.clone();
move |req: tide::Request<()>| {
handle_post_admin(req, config.clone(), templates.clone(), dbs.clone())
}
});
app.at("/admin/logout").get(handle_admin_logout);
app.listen(config.listen).await.unwrap();
}
let handle_admin_rm_ad = {
move |query: AdminRmAdQuery| {
if let Ok(ad_id) = hex::decode(query.ad) {
if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) {
if dbs.ads.remove(&ad_id).unwrap().is_some() {
dbs.ads.flush().unwrap();
let mut cache_ads = cache_ads.write().unwrap();
*cache_ads = to_json(
async fn serve_index<'a>(
req: tide::Request<()>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
errors: &[ErrorTemplate<'a>],
new_ad_form_refill: Option<NewAdQuery>,
) -> tide::Result<tide::Response> {
let selected_ad = req.param("ad").ok();
Ok(tide::Response::builder(200)
.content_type(tide::http::mime::HTML)
.body(
templates
.hb
.render(
"index.html",
&IndexTemplate {
lang: "fr",
errors,
ads: &to_json(
dbs.ads
.iter()
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
let ad_id = hex::encode(ad_id.as_ref());
Some(AdWithId {
id: hex::encode(ad_id.as_ref()),
ad: bincode::deserialize::<Ad>(&ad).ok()?,
selected: selected_ad.map_or(false, |i| i == ad_id),
id: ad_id,
})
})
.collect::<Vec<AdWithId>>(),
);
),
new_ad_form_refill,
},
)
.unwrap_or_else(|e| e.to_string()),
)
.build())
}
async fn serve_admin<'a>(
req: tide::Request<()>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
errors: &[ErrorTemplate<'a>],
) -> tide::Result<tide::Response> {
let selected_ad = req.param("ad").ok();
Ok(tide::Response::builder(200)
.content_type(tide::http::mime::HTML)
.body(
templates
.hb
.render(
"admin.html",
&AdminTemplate {
lang: "fr",
errors,
ads: &to_json(
dbs.ads
.iter()
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
let ad_id = hex::encode(ad_id.as_ref());
Some(AdWithId {
ad: bincode::deserialize::<Ad>(&ad).ok()?,
selected: selected_ad.map_or(false, |i| i == ad_id),
id: ad_id,
})
})
.collect::<Vec<AdWithId>>(),
),
},
)
.unwrap_or_else(|e| e.to_string()),
)
.build())
}
async fn serve_admin_login<'a>(
_req: tide::Request<()>,
templates: Arc<Templates<'static>>,
errors: &[ErrorTemplate<'a>],
) -> tide::Result<tide::Response> {
Ok(tide::Response::builder(200)
.content_type(tide::http::mime::HTML)
.body(
templates
.hb
.render(
"admin_login.html",
&AdminLoginTemplate { lang: "fr", errors },
)
.unwrap_or_else(|e| e.to_string()),
)
.build())
}
async fn handle_post_index(
mut req: tide::Request<()>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
) -> tide::Result<tide::Response> {
match req.body_form::<Query>().await? {
Query::NewAdQuery(query) => handle_new_ad(req, templates.clone(), dbs.clone(), query).await,
Query::RmAdQuery(query) => handle_rm_ad(req, templates.clone(), dbs.clone(), query).await,
}
}
async fn handle_new_ad(
req: tide::Request<()>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
query: NewAdQuery,
) -> tide::Result<tide::Response> {
let mut hasher = Sha512Trunc256::new();
hasher.update(&query.psw);
dbs.ads
.insert(
AdId::random(),
bincode::serialize(&Ad {
pubkey: if query.pubkey.is_empty() {
None
} else {
Some(match format_pubkey(&query.pubkey) {
Ok(pubkey) => pubkey,
Err(e) => return serve_index(
req,
templates,
dbs,
&[ErrorTemplate {
text: match e {
PubkeyDecodeError::BadChecksum => {
"La somme de contrôle de la clé publique est incorrecte."
}
PubkeyDecodeError::BadFormat => {
"Le format de la clé publique est incorrect."
}
},
}],
Some(query),
)
.await,
})
},
author: query.author,
password: hasher.finalize()[..].try_into().unwrap(),
price: query.price,
quantity: query.quantity,
time: 0,
title: query.title,
})
.unwrap(),
)
.unwrap();
dbs.ads.flush().unwrap();
Ok(tide::Redirect::new("/").into())
}
async fn handle_rm_ad(
req: tide::Request<()>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
query: RmAdQuery,
) -> tide::Result<tide::Response> {
let mut hasher = Sha512Trunc256::new();
hasher.update(query.psw);
let password: PasswordHash = hasher.finalize()[..].try_into().unwrap();
/*query
.ads
.into_iter()
.filter_map(|x| Some(AdId::try_from(hex::decode(x).ok()?.as_ref()).ok()?))
.for_each(|ad_id| {
if let Some(raw) = dbs.ads.get(&ad_id).unwrap() {
if let Ok(ad) = bincode::deserialize::<Ad>(&raw) {
if ad.password == password {
dbs.ads.remove(&ad_id).unwrap();
}
}
}
});*/
if let Ok(ad_id) = hex::decode(query.ad) {
if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) {
if let Some(raw) = dbs.ads.get(&ad_id).unwrap() {
if let Ok(ad) = bincode::deserialize::<Ad>(&raw) {
if ad.password == password {
dbs.ads.remove(&ad_id).unwrap();
dbs.ads.flush().unwrap();
} else {
return serve_index(
req,
templates,
dbs,
&[ErrorTemplate {
text: "Le mot de passe de l'annonce est incorrect.",
}],
None,
)
.await;
}
}
}
redirect_302("/admin").into_response()
}
};
}
Ok(tide::Redirect::new("/").into())
}
let route_index = warp::path::end().and(warp::get().map(move || handle_index(&[], None)).or(
warp::post().and(
warp::body::form::<Query>().map(move |query: Query| match query {
Query::NewAdQuery(query) => handle_new_ad(query),
Query::RmAdQuery(query) => handle_rm_ad(query),
}),
),
));
let route_admin = warp::path("admin").and(
warp::path::end()
.and(
warp::cookie("admin")
.and(warp::body::form::<AdminQuery>())
.map({
let handle_admin_login = handle_admin_login.clone();
let config = config.clone();
move |psw, query| {
if config.admin_passwords.contains(&psw) {
let AdminQuery::RmAdQuery(query) = query;
handle_admin_rm_ad(query)
} else {
handle_admin_login(&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}])
}
}
})
.or(warp::cookie("admin").map({
let handle_admin = handle_admin.clone();
let handle_admin_login = handle_admin_login.clone();
let config = config.clone();
move |psw| {
if config.admin_passwords.contains(&psw) {
handle_admin(&[])
} else {
handle_admin_login(&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}])
}
}
}))
.or(warp::path::end().map({
let handle_admin_login = handle_admin_login.clone();
move || handle_admin_login(&[])
})),
async fn handle_admin(
req: tide::Request<()>,
config: Arc<config::Config>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
) -> tide::Result<tide::Response> {
if let Some(psw) = req.cookie("admin") {
if config.admin_passwords.contains(&String::from(psw.value())) {
serve_admin(req, templates, dbs, &[]).await
} else {
serve_admin_login(
req,
templates,
&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}],
)
.or(warp::path("login").and(warp::path::end()).and(
warp::post()
.and(warp::get().map(move || handle_admin(&[])))
.or(warp::body::form::<AdminLoginQuery>().map({
let config = config.clone();
move |query: AdminLoginQuery| {
if config.admin_passwords.contains(&query.psw) {
warp::reply::with_header(
redirect_302("/admin").into_response(),
"Set-Cookie",
format!(
"admin={}; path=/; HttpOnly",
urlencoding::encode(&query.psw)
),
)
.into_response()
} else {
handle_admin_login(&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}])
.into_response()
}
.await
}
} else {
serve_admin_login(req, templates, &[]).await
}
}
async fn handle_post_admin(
mut req: tide::Request<()>,
config: Arc<config::Config>,
templates: Arc<Templates<'static>>,
dbs: Arc<db::Dbs>,
) -> tide::Result<tide::Response> {
if let Some(psw) = req.cookie("admin") {
if config.admin_passwords.contains(&String::from(psw.value())) {
match req.body_form::<AdminQuery>().await? {
AdminQuery::RmAdQuery(query) => {
if let Ok(ad_id) = hex::decode(query.ad) {
if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) {
dbs.ads.remove(&ad_id).unwrap();
dbs.ads.flush().unwrap();
}
})),
))
.or(warp::path("logout").and(warp::path::end()).map(|| {
warp::reply::with_header(
redirect_302("/").into_response(),
"Set-Cookie",
"admin=; HttpOnly; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
)
.into_response()
})),
);
warp::serve(route_static.or(route_index).or(route_admin))
.run(config.listen)
.await;
}
Ok(tide::Redirect::new("/admin").into())
}
_ => serve_admin(req, templates, dbs, &[]).await,
}
} else {
serve_admin_login(
req,
templates,
&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}],
)
.await
}
} else if let AdminQuery::LoginQuery(query) = req.body_form::<AdminQuery>().await? {
if config.admin_passwords.contains(&query.psw) {
serve_admin(req, templates, dbs, &[]).await.map(|mut r| {
let mut cookie = tide::http::Cookie::new("admin", query.psw);
cookie.set_http_only(Some(true));
cookie.set_path("/");
r.insert_cookie(cookie);
r
})
} else {
serve_admin_login(
req,
templates,
&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}],
)
.await
}
} else {
serve_admin_login(req, templates, &[]).await
}
}
#[derive(Serialize)]
struct IndexTemplate<'a> {
lang: &'a str,
errors: &'a [ErrorTemplate<'a>],
ads: &'a Json,
new_ad_form_refill: Option<NewAdQuery>,
}
#[derive(Serialize)]
struct AdminLoginTemplate<'a> {
lang: &'a str,
errors: &'a [ErrorTemplate<'a>],
}
#[derive(Serialize)]
struct AdminTemplate<'a> {
lang: &'a str,
errors: &'a [ErrorTemplate<'a>],
ads: &'a Json,
}
#[derive(Serialize)]
struct ErrorTemplate<'a> {
text: &'a str,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct NewAdQuery {
author: String,
price: String,
psw: String,
pubkey: String,
quantity: String,
title: String,
}
#[derive(Clone, Debug, Deserialize)]
struct RmAdQuery {
ad: String,
psw: String,
}
#[derive(Clone, Debug, Deserialize)]
struct AdminRmAdQuery {
ad: String,
}
#[derive(Clone, Debug, Deserialize)]
struct AdminLoginQuery {
psw: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "a")]
enum Query {
#[serde(rename = "new_ad")]
NewAdQuery(NewAdQuery),
#[serde(rename = "rm_ad")]
RmAdQuery(RmAdQuery),
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "a")]
enum AdminQuery {
#[serde(rename = "rm_ad")]
RmAdQuery(AdminRmAdQuery),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Ad {
author: String,
password: PasswordHash,
price: String,
pubkey: Option<String>,
quantity: String,
time: u64,
title: String,
}
#[derive(Clone, Debug, Serialize)]
struct AdWithId {
id: String,
ad: Ad,
async fn handle_admin_logout(req: tide::Request<()>) -> tide::Result<tide::Response> {
let mut r: tide::Response = tide::Redirect::new("/").into();
if let Some(mut cookie) = req.cookie("admin") {
cookie.set_path("/");
r.remove_cookie(cookie);
}
Ok(r)
}

View File

@ -1,4 +1,8 @@
use super::queries::*;
use handlebars::Handlebars;
use serde::Serialize;
use serde_json::value::Value as Json;
use std::path::Path;
const TEMPLATES_DIR: &str = "templates";
@ -41,3 +45,29 @@ pub fn load_templates<'reg>(dir: &Path) -> Templates<'reg> {
}
Templates { hb }
}
#[derive(Serialize)]
pub struct IndexTemplate<'a> {
pub lang: &'a str,
pub errors: &'a [ErrorTemplate<'a>],
pub ads: &'a Json,
pub new_ad_form_refill: Option<NewAdQuery>,
}
#[derive(Serialize)]
pub struct AdminLoginTemplate<'a> {
pub lang: &'a str,
pub errors: &'a [ErrorTemplate<'a>],
}
#[derive(Serialize)]
pub struct AdminTemplate<'a> {
pub lang: &'a str,
pub errors: &'a [ErrorTemplate<'a>],
pub ads: &'a Json,
}
#[derive(Serialize)]
pub struct ErrorTemplate<'a> {
pub text: &'a str,
}

View File

@ -1,4 +1,49 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::convert::{TryFrom, TryInto};
pub type PasswordHash = [u8; 32];
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)]
pub struct AdId([u8; 16]);
impl AdId {
pub fn random() -> Self {
Self(rand::random())
}
}
impl AsRef<[u8]> for AdId {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl TryFrom<&[u8]> for AdId {
type Error = std::array::TryFromSliceError;
fn try_from(v: &[u8]) -> Result<Self, Self::Error> {
Ok(Self(v.try_into()?))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ad {
pub author: String,
pub password: PasswordHash,
pub price: String,
pub pubkey: Option<String>,
pub quantity: String,
pub time: u64,
pub title: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct AdWithId {
pub id: String,
pub ad: Ad,
pub selected: bool,
}
pub enum PubkeyDecodeError {
BadChecksum,
@ -39,10 +84,3 @@ pub fn format_pubkey(raw: &str) -> Result<String, PubkeyDecodeError> {
&checksum[..3]
))
}
pub fn redirect_302(url: &str) -> warp::reply::WithStatus<warp::reply::WithHeader<&str>> {
warp::reply::with_status(
warp::reply::with_header("", "Location", url),
http::status::StatusCode::FOUND,
)
}

View File

@ -38,7 +38,7 @@
{{#each ads}}
<tr>
<td><input type="radio" name="ad" value="{{this.id}}" aria-label="Sélectionner l'annonce" required/></td>
<td onclick="ad_detail(event,'{{this.id}}')" title="Afficher le détail"><a href="ad/{{this.id}}">{{this.ad.title}}</a></td>
<td onclick="ad_detail(event,'{{this.id}}')" title="Afficher le détail"><a href="/admin/ad/{{this.id}}">{{this.ad.title}}</a></td>
<td>{{this.ad.quantity}}</td>
<td>{{this.ad.author}}</td>
</tr>

View File

@ -28,9 +28,10 @@
</div>
{{/if}}
<form action="/admin/login" method="post">
<form action="/admin" method="post">
<input type="hidden" name="a" value="login" autocomplete="off"/>
<label for="f_psw">Mot de passe&nbsp;:</label>
<input type="password" id="f_psw" name="psw"/>
<input type="password" id="f_psw" name="psw" autofocus/>
<input type="submit" value="S'authentifier"/>
</form>
</main>

View File

@ -42,11 +42,11 @@
{{#each ads}}
<tr>
<td><input type="radio" name="ad" value="{{this.id}}" aria-label="Sélectionner l'annonce" required/></td>
<td onclick="ad_detail(event,'{{this.id}}')" title="Afficher le détail"><a href="ad/{{this.id}}">{{this.ad.title}}</a></td>
<td onclick="ad_detail(event,'{{this.id}}')" title="Afficher le détail"><a href="/ad/{{this.id}}">{{this.ad.title}}</a></td>
<td>{{this.ad.quantity}}</td>
<td>{{this.ad.author}}</td>
</tr>
<tr id="ad-detail-{{this.id}}" class="ad-detail ad-detail-no">
<tr id="ad-detail-{{this.id}}" class="ad-detail{{#unless this.selected}} ad-detail-no{{/unless}}">
<td colspan="4"><p>
{{#if this.ad.pubkey}}Clé publique&nbsp;: {{this.ad.pubkey}}<br/>{{/if}}
Prix&nbsp;: {{this.ad.price}}