This commit is contained in:
Pascal Engélibert 2022-02-25 15:41:50 +01:00
parent c782124bcd
commit 296ec44fe4
Signed by: tuxmain
GPG Key ID: 3504BC6D362F7DCA
14 changed files with 1144 additions and 547 deletions

893
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,19 +2,20 @@
name = "gmarche" name = "gmarche"
version = "0.1.0" version = "0.1.0"
authors = ["Pascal Engélibert <tuxmain@zettascript.org>"] authors = ["Pascal Engélibert <tuxmain@zettascript.org>"]
edition = "2018" edition = "2021"
[dependencies] [dependencies]
async-std = { version = "1.8.0", features = ["attributes"] } argon2 = "0.3.4"
bincode = "1.3.1" async-std = { version = "1.10.0", features = ["attributes"] }
bincode = "1.3.3"
bs58 = "0.4.0" bs58 = "0.4.0"
dirs = "3.0.1" clap = { version = "3.1.1", default-features = false, features = ["derive", "std"] }
handlebars = "3.5.2" directories = "4.0.1"
hex = "0.4.2" handlebars = "4.2.1"
rand = "0.8.1" hex = "0.4.3"
serde = { version = "1.0.118", features = ["derive"] } rand = "0.8.5"
serde_json = "1.0.61" serde = { version = "1.0.136", features = ["derive"] }
sha2 = "0.9.2" serde_json = "1.0.79"
sled = "0.34.6" sha2 = "0.10.2"
structopt = "0.3.21" sled = "0.34.7"
tide = { version = "0.15.0", default-features = false, features = ["h1-server", "cookies", "logger"] } tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }

View File

@ -1,8 +1,8 @@
use clap::Parser;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use structopt::StructOpt;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ConfigPath(pub PathBuf); pub struct ConfigPath(pub PathBuf);
@ -10,9 +10,10 @@ pub struct ConfigPath(pub PathBuf);
impl Default for ConfigPath { impl Default for ConfigPath {
fn default() -> ConfigPath { fn default() -> ConfigPath {
ConfigPath( ConfigPath(
dirs::config_dir() directories::ProjectDirs::from("tk", "txmn", "gmarche").map_or_else(
.unwrap_or_else(|| Path::new(".config").to_path_buf()) || Path::new(".config").to_path_buf(),
.join("gmarche"), |o| o.config_dir().into(),
),
) )
} }
} }
@ -34,19 +35,19 @@ impl ToString for ConfigPath {
} }
} }
#[derive(Clone, Debug, StructOpt)] #[derive(Clone, Debug, Parser)]
#[structopt(name = "gmarche")] #[clap(name = "gmarche")]
pub struct MainOpt { pub struct MainOpt {
/// Directory /// Directory
#[structopt(short, long, parse(from_os_str), default_value)] #[clap(short, long, parse(from_os_str), default_value_t=ConfigPath::default())]
pub dir: ConfigPath, pub dir: ConfigPath,
/// Subcommand /// Subcommand
#[structopt(subcommand)] #[clap(subcommand)]
pub cmd: MainSubcommand, pub cmd: MainSubcommand,
} }
#[derive(Clone, Debug, StructOpt)] #[derive(Clone, Debug, Parser)]
pub enum MainSubcommand { pub enum MainSubcommand {
/// Initialize config & db /// Initialize config & db
Init, Init,

View File

@ -3,17 +3,29 @@ use std::path::Path;
const DB_DIR: &str = "db"; const DB_DIR: &str = "db";
const DB_NAME_ADS: &str = "ads"; const DB_NAME_AD: &str = "ad";
const DB_NAME_AD_BY_GROUP: &str = "ad_by_group";
const DB_NAME_GROUP: &str = "group";
const DB_NAME_GROUP_BY_GROUP: &str = "group_by_group";
const DB_NAME_GROUP_BY_NAME: &str = "group_by_name";
#[derive(Clone)] #[derive(Clone)]
pub struct Dbs { pub struct Dbs {
pub ads: Tree, pub ad: Tree,
pub ad_by_group: Tree,
pub group: Tree,
pub group_by_group: Tree,
pub group_by_name: Tree,
} }
pub fn load_dbs(path: &Path) -> Dbs { pub fn load_dbs(path: &Path) -> Dbs {
let db = sled::open(path.join(DB_DIR)).expect("Cannot open db"); let db = sled::open(path.join(DB_DIR)).expect("Cannot open db");
Dbs { Dbs {
ads: db.open_tree(DB_NAME_ADS).unwrap(), ad: db.open_tree(DB_NAME_AD).unwrap(),
ad_by_group: db.open_tree(DB_NAME_AD_BY_GROUP).unwrap(),
group: db.open_tree(DB_NAME_GROUP).unwrap(),
group_by_group: db.open_tree(DB_NAME_GROUP_BY_GROUP).unwrap(),
group_by_name: db.open_tree(DB_NAME_GROUP_BY_NAME).unwrap(),
} }
} }

View File

@ -9,11 +9,13 @@ mod static_files;
mod templates; mod templates;
mod utils; mod utils;
use structopt::StructOpt; use utils::*;
use clap::Parser;
#[async_std::main] #[async_std::main]
async fn main() { async fn main() {
let opt = cli::MainOpt::from_args(); let opt = cli::MainOpt::parse();
match opt.cmd { match opt.cmd {
cli::MainSubcommand::Init => { cli::MainSubcommand::Init => {
@ -29,9 +31,25 @@ async fn main() {
fn init_all<'reg>(opt: cli::MainOpt) -> (config::Config, db::Dbs, templates::Templates<'reg>) { fn init_all<'reg>(opt: cli::MainOpt) -> (config::Config, db::Dbs, templates::Templates<'reg>) {
std::fs::create_dir_all(&opt.dir.0).expect("Cannot create dir"); std::fs::create_dir_all(&opt.dir.0).expect("Cannot create dir");
static_files::init_static_files(&opt.dir.0); static_files::init_static_files(&opt.dir.0);
let dbs = db::load_dbs(&opt.dir.0);
if !dbs.group.contains_key(&ROOT_GROUP_ID).unwrap() {
dbs.group
.insert(
ROOT_GROUP_ID,
bincode::serialize(&Group {
name: String::new(),
parent: ROOT_GROUP_ID,
title: String::from("ĞMarché"),
})
.unwrap(),
)
.unwrap();
}
( (
config::read_config(&opt.dir.0), config::read_config(&opt.dir.0),
db::load_dbs(&opt.dir.0), dbs,
templates::load_templates(&opt.dir.0), templates::load_templates(&opt.dir.0),
) )
} }

View File

@ -1,5 +1,15 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize)]
pub struct AdminLoginQuery {
pub psw: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct AdminRmAdQuery {
pub ad: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct NewAdQuery { pub struct NewAdQuery {
pub author: String, pub author: String,
@ -10,6 +20,13 @@ pub struct NewAdQuery {
pub title: String, pub title: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AdminNewGroupQuery {
pub parent: String,
pub name: String,
pub title: String,
}
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct RmAdQuery { pub struct RmAdQuery {
pub ad: String, pub ad: String,
@ -17,31 +34,23 @@ pub struct RmAdQuery {
} }
#[derive(Clone, Debug, Deserialize)] #[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")] #[serde(tag = "a")]
pub enum Query { pub enum IndexQuery {
#[serde(rename = "new_ad")] #[serde(rename = "new_ad")]
NewAdQuery(NewAdQuery), NewAd(NewAdQuery),
#[serde(rename = "rm_ad")] #[serde(rename = "rm_ad")]
RmAdQuery(RmAdQuery), RmAd(RmAdQuery),
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(tag = "a")] #[serde(tag = "a")]
pub enum AdminQuery { pub enum AdminQuery {
#[serde(rename = "login")] #[serde(rename = "login")]
LoginQuery(AdminLoginQuery), Login(AdminLoginQuery),
#[serde(rename = "new_group")]
NewGroup(AdminNewGroupQuery),
#[serde(rename = "rm_ad")] #[serde(rename = "rm_ad")]
RmAdQuery(AdminRmAdQuery), RmAd(AdminRmAdQuery),
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]

View File

@ -1,11 +1,11 @@
use super::{cli, config::*, db::*, queries::*, static_files, templates::*, utils::*}; use super::{cli, config::*, db::*, queries::*, static_files, templates::*, utils::*};
use handlebars::to_json; use argon2::{
use sha2::{Digest, Sha512Trunc256}; password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
use std::{ Argon2,
convert::{TryFrom, TryInto},
sync::Arc,
}; };
use handlebars::to_json;
use std::{convert::TryFrom, sync::Arc};
pub async fn start_server( pub async fn start_server(
config: Config, config: Config,
@ -45,6 +45,44 @@ pub async fn start_server(
handle_post_index(req, config.clone(), templates.clone(), dbs.clone()) handle_post_index(req, config.clone(), templates.clone(), dbs.clone())
} }
}); });
app.at(&format!("{}g/:group", config.root_url)).get({
let config = config.clone();
let templates = templates.clone();
let dbs = dbs.clone();
move |req: tide::Request<()>| {
serve_group(
req,
config.clone(),
templates.clone(),
dbs.clone(),
&[],
None,
)
}
});
app.at(&format!("{}g/:group/:ad", config.root_url)).get({
let config = config.clone();
let templates = templates.clone();
let dbs = dbs.clone();
move |req: tide::Request<()>| {
serve_group(
req,
config.clone(),
templates.clone(),
dbs.clone(),
&[],
None,
)
}
});
app.at(&format!("{}admin/g/:group", config.root_url)).get({
let config = config.clone();
let templates = templates.clone();
let dbs = dbs.clone();
move |req: tide::Request<()>| {
handle_admin_group(req, config.clone(), templates.clone(), dbs.clone())
}
});
app.at(&format!("{}ad/:ad", config.root_url)).post({ app.at(&format!("{}ad/:ad", config.root_url)).post({
let config = config.clone(); let config = config.clone();
let templates = templates.clone(); let templates = templates.clone();
@ -129,11 +167,12 @@ async fn serve_index<'a>(
errors, errors,
}, },
ads: &to_json( ads: &to_json(
dbs.ads dbs.ad_by_group
.iter() .scan_prefix(ROOT_GROUP_ID)
.filter_map(|x| { .filter_map(|x| {
let (ad_id, ad) = x.ok()?; let (k, ad) = x.ok()?;
let ad_id = hex::encode(ad_id.as_ref()); let ad_id =
hex::encode(AdId::try_from(&k.as_ref()[16..32]).ok()?);
Some(AdWithId { Some(AdWithId {
ad: bincode::deserialize::<Ad>(&ad).ok()?, ad: bincode::deserialize::<Ad>(&ad).ok()?,
selected: selected_ad.map_or(false, |i| i == ad_id), selected: selected_ad.map_or(false, |i| i == ad_id),
@ -142,6 +181,16 @@ async fn serve_index<'a>(
}) })
.collect::<Vec<AdWithId>>(), .collect::<Vec<AdWithId>>(),
), ),
groups: &to_json(
dbs.group_by_group
.scan_prefix(ROOT_GROUP_ID)
.keys()
.filter_map(|k| GroupId::try_from(&k.ok()?.as_ref()[16..32]).ok())
.filter_map(|subgroup_id| {
bincode::deserialize(&dbs.group.get(subgroup_id).ok()??).ok()
})
.collect::<Vec<Group>>(),
),
new_ad_form_refill, new_ad_form_refill,
}, },
) )
@ -150,6 +199,171 @@ async fn serve_index<'a>(
.build()) .build())
} }
async fn serve_group<'a>(
req: tide::Request<()>,
config: Arc<Config>,
templates: Arc<Templates<'static>>,
dbs: Dbs,
errors: &[ErrorTemplate<'a>],
new_ad_form_refill: Option<NewAdQuery>,
) -> tide::Result<tide::Response> {
if let Ok(group_name) = req.param("group") {
if let Some(group_id) = dbs.group_by_name.get(group_name).unwrap() {
let selected_ad = req.param("ad").ok();
Ok(tide::Response::builder(200)
.content_type(tide::http::mime::HTML)
.body(
templates
.hb
.render(
"group.html",
&GroupTemplate {
common: CommonTemplate {
lang: "fr",
root_url: &config.root_url,
title: &config.title,
errors,
},
ads: &to_json(
dbs.ad_by_group
.scan_prefix(group_id.clone())
.filter_map(|x| {
let (k, ad) = x.ok()?;
let ad_id = hex::encode(
AdId::try_from(&k.as_ref()[16..32]).ok()?,
);
Some(AdWithId {
ad: bincode::deserialize::<Ad>(&ad).ok()?,
selected: selected_ad.map_or(false, |i| i == ad_id),
id: ad_id,
})
})
.collect::<Vec<AdWithId>>(),
),
groups: &to_json(
dbs.group_by_group
.scan_prefix(group_id)
.keys()
.filter_map(|k| {
GroupId::try_from(&k.ok()?.as_ref()[16..32]).ok()
})
.filter_map(|subgroup_id| {
bincode::deserialize(&dbs.group.get(subgroup_id).ok()??)
.ok()
})
.collect::<Vec<Group>>(),
),
new_ad_form_refill,
},
)
.unwrap_or_else(|e| e.to_string()),
)
.build())
} else {
serve_index(
req,
config,
templates,
dbs,
&[ErrorTemplate {
text: "Le groupe demandé n'existe pas.",
}],
None,
)
.await
}
} else {
serve_index(req, config, templates, dbs, &[], None).await
}
}
async fn serve_admin_group<'a>(
req: tide::Request<()>,
config: Arc<Config>,
templates: Arc<Templates<'static>>,
dbs: Dbs,
errors: &[ErrorTemplate<'a>],
new_group_form_refill: Option<AdminNewGroupQuery>,
) -> tide::Result<tide::Response> {
if let Ok(group_name) = req.param("group") {
if let Some(group_id) = dbs.group_by_name.get(group_name).unwrap() {
if let Some(group) = dbs.group.get(&group_id).unwrap() {
if let Ok(group) = bincode::deserialize::<Group>(&group) {
let selected_ad = req.param("ad").ok();
return Ok(tide::Response::builder(200)
.content_type(tide::http::mime::HTML)
.body(
templates
.hb
.render(
"admin_group.html",
&AdminGroupTemplate {
common: CommonTemplate {
lang: "fr",
root_url: &config.root_url,
title: &config.title,
errors,
},
ads: &to_json(
dbs.ad_by_group
.scan_prefix(group_id.clone())
.filter_map(|x| {
let (k, ad) = x.ok()?;
let ad_id = hex::encode(
AdId::try_from(&k.as_ref()[16..32]).ok()?,
);
Some(AdWithId {
ad: bincode::deserialize::<Ad>(&ad).ok()?,
selected: selected_ad
.map_or(false, |i| i == ad_id),
id: ad_id,
})
})
.collect::<Vec<AdWithId>>(),
),
group: &to_json(group),
groups: &to_json(
dbs.group_by_group
.scan_prefix(group_id)
.keys()
.filter_map(|k| {
GroupId::try_from(&k.ok()?.as_ref()[16..32])
.ok()
})
.filter_map(|subgroup_id| {
bincode::deserialize(
&dbs.group.get(subgroup_id).ok()??,
)
.ok()
})
.collect::<Vec<Group>>(),
),
new_group_form_refill,
},
)
.unwrap_or_else(|e| e.to_string()),
)
.build());
}
}
}
serve_admin(
req,
config,
templates,
dbs,
&[ErrorTemplate {
text: "Le groupe demandé n'existe pas.",
}],
)
.await
} else {
serve_admin(req, config, templates, dbs, &[]).await
}
}
async fn serve_admin<'a>( async fn serve_admin<'a>(
req: tide::Request<()>, req: tide::Request<()>,
config: Arc<Config>, config: Arc<Config>,
@ -173,11 +387,12 @@ async fn serve_admin<'a>(
errors, errors,
}, },
ads: &to_json( ads: &to_json(
dbs.ads dbs.ad_by_group
.iter() .scan_prefix(ROOT_GROUP_ID)
.filter_map(|x| { .filter_map(|x| {
let (ad_id, ad) = x.ok()?; let (k, ad) = x.ok()?;
let ad_id = hex::encode(ad_id.as_ref()); let ad_id =
hex::encode(AdId::try_from(&k.as_ref()[16..32]).ok()?);
Some(AdWithId { Some(AdWithId {
ad: bincode::deserialize::<Ad>(&ad).ok()?, ad: bincode::deserialize::<Ad>(&ad).ok()?,
selected: selected_ad.map_or(false, |i| i == ad_id), selected: selected_ad.map_or(false, |i| i == ad_id),
@ -186,6 +401,16 @@ async fn serve_admin<'a>(
}) })
.collect::<Vec<AdWithId>>(), .collect::<Vec<AdWithId>>(),
), ),
groups: &to_json(
dbs.group_by_group
.scan_prefix(ROOT_GROUP_ID)
.keys()
.filter_map(|k| GroupId::try_from(&k.ok()?.as_ref()[16..32]).ok())
.filter_map(|subgroup_id| {
bincode::deserialize(&dbs.group.get(subgroup_id).ok()??).ok()
})
.collect::<Vec<Group>>(),
),
}, },
) )
.unwrap_or_else(|e| e.to_string()), .unwrap_or_else(|e| e.to_string()),
@ -226,11 +451,11 @@ async fn handle_post_index(
templates: Arc<Templates<'static>>, templates: Arc<Templates<'static>>,
dbs: Dbs, dbs: Dbs,
) -> tide::Result<tide::Response> { ) -> tide::Result<tide::Response> {
match req.body_form::<Query>().await? { match req.body_form::<IndexQuery>().await? {
Query::NewAdQuery(query) => { IndexQuery::NewAd(query) => {
handle_new_ad(req, config.clone(), templates.clone(), dbs.clone(), query).await handle_new_ad(req, config.clone(), templates.clone(), dbs.clone(), query).await
} }
Query::RmAdQuery(query) => { IndexQuery::RmAd(query) => {
handle_rm_ad(req, config.clone(), templates.clone(), dbs.clone(), query).await handle_rm_ad(req, config.clone(), templates.clone(), dbs.clone(), query).await
} }
} }
@ -243,9 +468,7 @@ async fn handle_new_ad(
dbs: Dbs, dbs: Dbs,
query: NewAdQuery, query: NewAdQuery,
) -> tide::Result<tide::Response> { ) -> tide::Result<tide::Response> {
let mut hasher = Sha512Trunc256::new(); dbs.ad
hasher.update(&query.psw);
dbs.ads
.insert( .insert(
AdId::random(), AdId::random(),
bincode::serialize(&Ad { bincode::serialize(&Ad {
@ -275,7 +498,10 @@ async fn handle_new_ad(
}) })
}, },
author: query.author, author: query.author,
password: hasher.finalize()[..].try_into().unwrap(), password: Argon2::default()
.hash_password(query.psw.as_bytes(), &SaltString::generate(&mut OsRng))
.unwrap()
.to_string(),
price: query.price, price: query.price,
quantity: query.quantity, quantity: query.quantity,
time: 0, time: 0,
@ -284,7 +510,7 @@ async fn handle_new_ad(
.unwrap(), .unwrap(),
) )
.unwrap(); .unwrap();
dbs.ads.flush_async().await.unwrap(); dbs.ad.flush_async().await.unwrap();
Ok(tide::Redirect::new(&config.root_url).into()) Ok(tide::Redirect::new(&config.root_url).into())
} }
@ -295,42 +521,31 @@ async fn handle_rm_ad(
dbs: Dbs, dbs: Dbs,
query: RmAdQuery, query: RmAdQuery,
) -> tide::Result<tide::Response> { ) -> 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) = hex::decode(query.ad) {
if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) { if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) {
if let Some(raw) = dbs.ads.get(&ad_id).unwrap() { if let Some(raw) = dbs.ad.get(&ad_id).unwrap() {
if let Ok(ad) = bincode::deserialize::<Ad>(&raw) { if let Ok(ad) = bincode::deserialize::<Ad>(&raw) {
if ad.password == password { if let Ok(password) = PasswordHash::new(&ad.password) {
dbs.ads.remove(&ad_id).unwrap(); if Argon2::default()
.verify_password(query.psw.as_bytes(), &password)
.is_ok()
{
dbs.ad.remove(&ad_id).unwrap();
dbs.ads.flush_async().await.unwrap(); dbs.ad.flush_async().await.unwrap();
} else { } else {
return serve_index( return serve_index(
req, req,
config, config,
templates, templates,
dbs, dbs,
&[ErrorTemplate { &[ErrorTemplate {
text: "Le mot de passe de l'annonce est incorrect.", text: "Le mot de passe de l'annonce est incorrect.",
}], }],
None, None,
) )
.await; .await;
}
} }
} }
} }
@ -364,6 +579,31 @@ async fn handle_admin(
} }
} }
async fn handle_admin_group(
req: tide::Request<()>,
config: Arc<Config>,
templates: Arc<Templates<'static>>,
dbs: Dbs,
) -> tide::Result<tide::Response> {
if let Some(psw) = req.cookie("admin") {
if config.admin_passwords.contains(&String::from(psw.value())) {
serve_admin_group(req, config, templates, dbs, &[], None).await
} else {
serve_admin_login(
req,
config,
templates,
&[ErrorTemplate {
text: "Mot de passe administrateur invalide",
}],
)
.await
}
} else {
serve_admin_login(req, config, templates, &[]).await
}
}
async fn handle_post_admin( async fn handle_post_admin(
mut req: tide::Request<()>, mut req: tide::Request<()>,
config: Arc<Config>, config: Arc<Config>,
@ -373,16 +613,58 @@ async fn handle_post_admin(
if let Some(psw) = req.cookie("admin") { if let Some(psw) = req.cookie("admin") {
if config.admin_passwords.contains(&String::from(psw.value())) { if config.admin_passwords.contains(&String::from(psw.value())) {
match req.body_form::<AdminQuery>().await? { match req.body_form::<AdminQuery>().await? {
AdminQuery::RmAdQuery(query) => { AdminQuery::RmAd(query) => {
if let Ok(ad_id) = hex::decode(query.ad) { if let Ok(ad_id) = hex::decode(query.ad) {
if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) { if let Ok(ad_id) = AdId::try_from(ad_id.as_ref()) {
dbs.ads.remove(&ad_id).unwrap(); dbs.ad.remove(&ad_id).unwrap();
dbs.ads.flush_async().await.unwrap(); dbs.ad.flush_async().await.unwrap();
} }
} }
Ok(tide::Redirect::new(&format!("{}admin", config.root_url)).into()) Ok(tide::Redirect::new(&format!("{}admin", config.root_url)).into())
} }
AdminQuery::NewGroup(query) => {
if let Some(Ok(parent_group_id)) = if query.parent.is_empty() {
Some(Ok(ROOT_GROUP_ID))
} else {
dbs.group_by_name
.get(&query.parent)
.unwrap()
.map(|o| GroupId::try_from(o.as_ref()))
} {
if !dbs.group_by_name.contains_key(&query.name).unwrap() {
let group_id = rand::random::<GroupId>();
dbs.group
.insert(
group_id,
bincode::serialize(&Group {
parent: parent_group_id,
name: query.name.clone(),
title: query.title,
})
.unwrap(),
)
.unwrap();
dbs.group_by_name
.insert(query.name.clone(), &group_id)
.unwrap();
dbs.group_by_group
.insert([parent_group_id, group_id].concat(), &[])
.unwrap();
return Ok(tide::Redirect::new(&format!(
"{}admin/g/{}",
config.root_url, query.name
))
.into());
}
return Ok(tide::Redirect::new(&format!(
"{}admin/g/{}",
config.root_url, query.parent
))
.into());
}
Ok(tide::Redirect::new(&format!("{}admin", config.root_url)).into())
}
_ => serve_admin(req, config, templates, dbs, &[]).await, _ => serve_admin(req, config, templates, dbs, &[]).await,
} }
} else { } else {
@ -396,7 +678,7 @@ async fn handle_post_admin(
) )
.await .await
} }
} else if let AdminQuery::LoginQuery(query) = req.body_form::<AdminQuery>().await? { } else if let AdminQuery::Login(query) = req.body_form::<AdminQuery>().await? {
if config.admin_passwords.contains(&query.psw) { if config.admin_passwords.contains(&query.psw) {
serve_admin(req, config.clone(), templates, dbs, &[]) serve_admin(req, config.clone(), templates, dbs, &[])
.await .await

View File

@ -7,12 +7,17 @@ use std::path::Path;
const TEMPLATES_DIR: &str = "templates"; const TEMPLATES_DIR: &str = "templates";
static TEMPLATE_FILES: &[(&str, &str)] = &[ static TEMPLATE_FILES: &[(&str, &str)] = &[
("index.html", include_str!("../templates/index.html")),
("admin.html", include_str!("../templates/admin.html")), ("admin.html", include_str!("../templates/admin.html")),
( (
"admin_group.html",
include_str!("../templates/admin_group.html"),
),
(
"admin_login.html", "admin_login.html",
include_str!("../templates/admin_login.html"), include_str!("../templates/admin_login.html"),
), ),
("group.html", include_str!("../templates/group.html")),
("index.html", include_str!("../templates/index.html")),
]; ];
pub struct Templates<'reg> { pub struct Templates<'reg> {
@ -59,6 +64,7 @@ pub struct IndexTemplate<'a> {
#[serde(flatten)] #[serde(flatten)]
pub common: CommonTemplate<'a>, pub common: CommonTemplate<'a>,
pub ads: &'a Json, pub ads: &'a Json,
pub groups: &'a Json,
pub new_ad_form_refill: Option<NewAdQuery>, pub new_ad_form_refill: Option<NewAdQuery>,
} }
@ -73,6 +79,27 @@ pub struct AdminTemplate<'a> {
#[serde(flatten)] #[serde(flatten)]
pub common: CommonTemplate<'a>, pub common: CommonTemplate<'a>,
pub ads: &'a Json, pub ads: &'a Json,
pub groups: &'a Json,
}
// TODO add subgroups, parent groups, title
#[derive(Serialize)]
pub struct GroupTemplate<'a> {
#[serde(flatten)]
pub common: CommonTemplate<'a>,
pub ads: &'a Json,
pub groups: &'a Json,
pub new_ad_form_refill: Option<NewAdQuery>,
}
#[derive(Serialize)]
pub struct AdminGroupTemplate<'a> {
#[serde(flatten)]
pub common: CommonTemplate<'a>,
pub ads: &'a Json,
pub group: &'a Json,
pub groups: &'a Json,
pub new_group_form_refill: Option<AdminNewGroupQuery>,
} }
#[derive(Serialize)] #[derive(Serialize)]

View File

@ -2,7 +2,16 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
pub type PasswordHash = [u8; 32]; pub type GroupId = [u8; 16];
pub static ROOT_GROUP_ID: GroupId = [0; 16];
#[derive(Deserialize, Serialize)]
pub struct Group {
pub name: String,
pub parent: GroupId,
pub title: String,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize)]
pub struct AdId([u8; 16]); pub struct AdId([u8; 16]);
@ -30,7 +39,7 @@ impl TryFrom<&[u8]> for AdId {
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ad { pub struct Ad {
pub author: String, pub author: String,
pub password: PasswordHash, pub password: String,
pub price: String, pub price: String,
pub pubkey: Option<String>, pub pubkey: Option<String>,
pub quantity: String, pub quantity: String,

View File

@ -27,6 +27,10 @@
</div> </div>
{{/if}} {{/if}}
{{#each groups}}
<a href="{{root_url}}admin/g/{{this.name}}">{{this.title}}</a><br/>
{{/each}}
{{#if ads}} {{#if ads}}
<span>Cliquez sur une annonce pour afficher le détail.</span> <span>Cliquez sur une annonce pour afficher le détail.</span>
<form method="post"> <form method="post">
@ -62,6 +66,19 @@
{{else}} {{else}}
<p>Il n'y a pas encore d'annonce ici.</p> <p>Il n'y a pas encore d'annonce ici.</p>
{{/if}} {{/if}}
<form method="post" action="{{root_url}}admin">
<input type="hidden" name="a" value="new_group" autocomplete="off"/>
<input type="hidden" name="parent" value="" autocomplete="off"/>
<fieldset>
<legend>Nouveau groupe</legend>
<label for="f_new_title">Titre&nbsp;:</label>
<input type="text" id="f_new_title" name="title" placeholder="Marché de Juneville"{{#if new_group_form_refill}} value="{{new_group_form_refill.title}}"{{/if}} required/><br/>
<label for="f_new_name">Identifiant (utilisé dans les URL, <code>[-_.a-zA-Z0-9]{1,64}</code>)&nbsp;:</label>
<input type="text" id="f_new_name" name="name" placeholder="juneville" maxlength="64"{{#if new_group_form_refill}} value="{{new_group_form_refill.name}}"{{/if}} required/><br/>
<input type="submit" value="Créer"/>
</fieldset>
</form>
</main> </main>
<footer> <footer>
@ -70,7 +87,7 @@
<p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p> <p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p>
<p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/> <p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/>
CopyLeft 2020 Pascal Engélibert<br/> CopyLeft 2020-2022 Pascal Engélibert<br/>
Image de fond&nbsp;: Claudia Peters, FreeImages.com</p> Image de fond&nbsp;: Claudia Peters, FreeImages.com</p>
<p><a href="{{root_url}}">Accueil</a> &#8211; <a href="{{root_url}}admin/logout">Verrouiller</a></p> <p><a href="{{root_url}}">Accueil</a> &#8211; <a href="{{root_url}}admin/logout">Verrouiller</a></p>

View File

@ -0,0 +1,97 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset="utf-8"/>
<title>{{group.title}} | Administration | {{title}}</title>
<link rel="stylesheet" href="{{root_url}}static/style1.css"/>
<link rel="shortcut icon" href="{{root_url}}static/icon.png"/>
<script type="text/javascript" src="{{root_url}}static/script1.js"></script>
</head>
<body>
<div class="center page">
<header>
<a href="{{root_url}}"><img id="banner" alt="Bannière {{title}}" src="{{root_url}}static/banner.jpg"/></a>
</header>
<main>
<h1>{{group.title}} &#8211; Administration &#8211; {{title}}</h1>
{{#if errors}}
<div id="errors">
<span>Oups, il y a un problème&nbsp;:</span>
<ul>
{{#each errors}}
<li>{{this.text}}</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#each groups}}
<a href="{{root_url}}admin/g/{{this.name}}">{{this.title}}</a><br/>
{{/each}}
{{#if ads}}
<span>Cliquez sur une annonce pour afficher le détail.</span>
<form method="post">
<table id="ads">
<thead>
<tr><td></td><th>Annonce</th><th>Quantité</th><th>Vendeur</th></tr>
</thead>
<tbody>
{{#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="{{../root_url}}admin/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{{#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}}
</p></td>
</tr>
{{/each}}
</tbody>
</table>
<br/>
<fieldset>
<legend>Supprimer l'annonce sélectionnée</legend>
<button type="submit" name="a" value="rm_ad">Supprimer</button>
</fieldset>
</form>
<br/>
{{else}}
<p>Il n'y a pas encore d'annonce ici.</p>
{{/if}}
<form method="post" action="{{root_url}}admin">
<input type="hidden" name="a" value="new_group" autocomplete="off"/>
<input type="hidden" name="parent" value="{{group.name}}" autocomplete="off"/>
<fieldset>
<legend>Nouveau groupe</legend>
<label for="f_new_title">Titre&nbsp;:</label>
<input type="text" id="f_new_title" name="title" placeholder="Marché de Juneville"{{#if new_group_form_refill}} value="{{new_group_form_refill.title}}"{{/if}} required/><br/>
<label for="f_new_name">Identifiant (utilisé dans les URL, <code>[-_.a-zA-Z0-9]{1,64}</code>)&nbsp;:</label>
<input type="text" id="f_new_name" name="name" placeholder="juneville" maxlength="64"{{#if new_group_form_refill}} value="{{new_group_form_refill.name}}"{{/if}} required/><br/>
<input type="submit" value="Créer"/>
</fieldset>
</form>
</main>
<footer>
<hr style="clear: both;"/>
<p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p>
<p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/>
CopyLeft 2020-2022 Pascal Engélibert<br/>
Image de fond&nbsp;: Claudia Peters, FreeImages.com</p>
<p><a href="{{root_url}}">Accueil</a> &#8211; <a href="{{root_url}}admin/logout">Verrouiller</a></p>
</footer>
</div>
</body>
</html>

View File

@ -42,7 +42,7 @@
<p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p> <p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p>
<p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/> <p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/>
CopyLeft 2020 Pascal Engélibert<br/> CopyLeft 2020-2022 Pascal Engélibert<br/>
Image de fond&nbsp;: Claudia Peters, FreeImages.com</p> Image de fond&nbsp;: Claudia Peters, FreeImages.com</p>
<p><a href="{{root_url}}">Accueil</a></p> <p><a href="{{root_url}}">Accueil</a></p>

109
templates/group.html Normal file
View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset="utf-8"/>
<title>{{group-title}} | {{title}}</title>
<link rel="stylesheet" href="{{root_url}}static/style1.css"/>
<link rel="shortcut icon" href="{{root_url}}static/icon.png"/>
<script type="text/javascript" src="{{root_url}}static/script1.js"></script>
</head>
<body>
<div class="center page">
<header>
<a href="{{root_url}}"><img id="banner" alt="Bannière {{title}}" src="{{root_url}}static/banner.jpg"/></a>
</header>
<main>
<h1>{{title}}</h1>
{{#if errors}}
<div id="errors">
<span>Oups, il y a un problème&nbsp;:</span>
<ul>
{{#each errors}}
<li>{{this.text}}</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#each groups}}
<a href="{{root_url}}g/{{this.name}}">{{this.title}}</a><br/>
{{/each}}
{{#if ads}}
<span>Cliquez sur une annonce pour afficher le détail.</span>
<form method="post">
<table id="ads">
<thead>
<tr><td></td><th>Annonce</th><th>Quantité</th><th>Vendeur</th></tr>
</thead>
<tbody>
{{#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="{{../root_url}}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{{#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}}
</p></td>
</tr>
{{/each}}
</tbody>
</table>
<br/>
<fieldset>
<legend>Supprimer l'annonce sélectionnée</legend>
<label for="f_rm_psw">Mot de passe&nbsp;:</label>
<input type="password" id="f_rm_psw" name="psw"/><br/>
<button type="submit" name="a" value="rm_ad">Supprimer</button>
</fieldset>
</form>
<br/>
{{else}}
<p>Il n'y a pas encore d'annonce ici.</p>
{{/if}}
<img id="stand" alt="Marché" src="{{root_url}}static/standgm.png"/>
<form method="post">
<input type="hidden" name="a" value="new_ad" autocomplete="off"/>
<fieldset>
<legend>Nouvelle annonce</legend>
<label for="f_new_title">Titre&nbsp;:</label>
<input type="text" id="f_new_title" name="title" placeholder="Carottes"{{#if new_ad_form_refill}} value="{{new_ad_form_refill.title}}"{{/if}} required/><br/>
<label for="f_new_quantity">Quantité&nbsp;:</label>
<input type="text" id="f_new_quantity" name="quantity" placeholder="10 kg"{{#if new_ad_form_refill}} value="{{new_ad_form_refill.quantity}}"{{/if}}/><br/>
<label for="f_new_price">Prix&nbsp;:</label>
<input type="text" id="f_new_price" name="price" placeholder="42 DU/kg"{{#if new_ad_form_refill}} value="{{new_ad_form_refill.price}}"{{/if}}/><br/>
<label for="f_new_author">Votre nom&nbsp;:</label>
<input type="text" id="f_new_author" name="author" placeholder="Toto"{{#if new_ad_form_refill}} value="{{new_ad_form_refill.author}}"{{/if}} required/><br/>
<label for="f_new_pubkey">Clé publique&nbsp;:</label>
<input type="text" id="f_new_pubkey" name="pubkey"{{#if new_ad_form_refill}} value="{{new_ad_form_refill.pubkey}}"{{/if}}/><br/>
<label for="f_new_psw">Mot de passe&nbsp;:</label>
<input type="text" id="f_new_psw" name="psw"{{#if new_ad_form_refill}} value="{{new_ad_form_refill.psw}}"{{/if}}/><br/>
<span>Le mot de passe sera demandé pour modifier ou supprimer l'annonce.</span><br/>
<input type="submit" value="Publier"/>
</fieldset>
</form>
</main>
<footer>
<hr style="clear: both;"/>
<p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p>
<p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/>
CopyLeft 2020-2022 Pascal Engélibert<br/>
Image de fond&nbsp;: Claudia Peters, FreeImages.com</p>
<p><a href="{{root_url}}admin">Administration</a></p>
</footer>
</div>
</body>
</html>

View File

@ -29,6 +29,10 @@
<p>Ceci est une démo du nouveau site ĞMarché en développement.</p> <p>Ceci est une démo du nouveau site ĞMarché en développement.</p>
{{#each groups}}
<a href="{{root_url}}g/{{this.name}}">{{this.title}}</a><br/>
{{/each}}
{{#if ads}} {{#if ads}}
<span>Cliquez sur une annonce pour afficher le détail.</span> <span>Cliquez sur une annonce pour afficher le détail.</span>
<form method="post"> <form method="post">
@ -97,7 +101,7 @@
<p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p> <p><a href="https://forum.duniter.org">Toutes les questions techniques ont leur place sur le forum.</a></p>
<p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/> <p><a href="https://git.p2p.legal/tuxmain/gmarche-rs">Code source</a> sous licence <a href="https://www.gnu.org/licenses/licenses.html#AGPL">GNU AGPL v3</a>. &#129408; Écrit en <a href="https://www.rust-lang.org">Rust</a>. Images de Attilax.<br/>
CopyLeft 2020 Pascal Engélibert<br/> CopyLeft 2020-2022 Pascal Engélibert<br/>
Image de fond&nbsp;: Claudia Peters, FreeImages.com</p> Image de fond&nbsp;: Claudia Peters, FreeImages.com</p>
<p><a href="{{root_url}}admin">Administration</a></p> <p><a href="{{root_url}}admin">Administration</a></p>