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

View File

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

View File

@ -3,17 +3,29 @@ use std::path::Path;
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)]
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 {
let db = sled::open(path.join(DB_DIR)).expect("Cannot open db");
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 utils;
use structopt::StructOpt;
use utils::*;
use clap::Parser;
#[async_std::main]
async fn main() {
let opt = cli::MainOpt::from_args();
let opt = cli::MainOpt::parse();
match opt.cmd {
cli::MainSubcommand::Init => {
@ -29,9 +31,25 @@ async fn main() {
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");
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),
db::load_dbs(&opt.dir.0),
dbs,
templates::load_templates(&opt.dir.0),
)
}

View File

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

View File

@ -1,11 +1,11 @@
use super::{cli, config::*, db::*, queries::*, static_files, templates::*, utils::*};
use handlebars::to_json;
use sha2::{Digest, Sha512Trunc256};
use std::{
convert::{TryFrom, TryInto},
sync::Arc,
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use handlebars::to_json;
use std::{convert::TryFrom, sync::Arc};
pub async fn start_server(
config: Config,
@ -45,6 +45,44 @@ pub async fn start_server(
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({
let config = config.clone();
let templates = templates.clone();
@ -129,11 +167,12 @@ async fn serve_index<'a>(
errors,
},
ads: &to_json(
dbs.ads
.iter()
dbs.ad_by_group
.scan_prefix(ROOT_GROUP_ID)
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
let ad_id = hex::encode(ad_id.as_ref());
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),
@ -142,6 +181,16 @@ async fn serve_index<'a>(
})
.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,
},
)
@ -150,6 +199,171 @@ async fn serve_index<'a>(
.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>(
req: tide::Request<()>,
config: Arc<Config>,
@ -173,11 +387,12 @@ async fn serve_admin<'a>(
errors,
},
ads: &to_json(
dbs.ads
.iter()
dbs.ad_by_group
.scan_prefix(ROOT_GROUP_ID)
.filter_map(|x| {
let (ad_id, ad) = x.ok()?;
let ad_id = hex::encode(ad_id.as_ref());
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),
@ -186,6 +401,16 @@ async fn serve_admin<'a>(
})
.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()),
@ -226,11 +451,11 @@ async fn handle_post_index(
templates: Arc<Templates<'static>>,
dbs: Dbs,
) -> tide::Result<tide::Response> {
match req.body_form::<Query>().await? {
Query::NewAdQuery(query) => {
match req.body_form::<IndexQuery>().await? {
IndexQuery::NewAd(query) => {
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
}
}
@ -243,9 +468,7 @@ async fn handle_new_ad(
dbs: Dbs,
query: NewAdQuery,
) -> tide::Result<tide::Response> {
let mut hasher = Sha512Trunc256::new();
hasher.update(&query.psw);
dbs.ads
dbs.ad
.insert(
AdId::random(),
bincode::serialize(&Ad {
@ -275,7 +498,10 @@ async fn handle_new_ad(
})
},
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,
quantity: query.quantity,
time: 0,
@ -284,7 +510,7 @@ async fn handle_new_ad(
.unwrap(),
)
.unwrap();
dbs.ads.flush_async().await.unwrap();
dbs.ad.flush_async().await.unwrap();
Ok(tide::Redirect::new(&config.root_url).into())
}
@ -295,30 +521,18 @@ async fn handle_rm_ad(
dbs: 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 Some(raw) = dbs.ad.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(password) = PasswordHash::new(&ad.password) {
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 {
return serve_index(
req,
@ -336,6 +550,7 @@ async fn handle_rm_ad(
}
}
}
}
Ok(tide::Redirect::new(&config.root_url).into())
}
@ -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(
mut req: tide::Request<()>,
config: Arc<Config>,
@ -373,16 +613,58 @@ async fn handle_post_admin(
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) => {
AdminQuery::RmAd(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.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())
}
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,
}
} else {
@ -396,7 +678,7 @@ async fn handle_post_admin(
)
.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) {
serve_admin(req, config.clone(), templates, dbs, &[])
.await

View File

@ -7,12 +7,17 @@ use std::path::Path;
const TEMPLATES_DIR: &str = "templates";
static TEMPLATE_FILES: &[(&str, &str)] = &[
("index.html", include_str!("../templates/index.html")),
("admin.html", include_str!("../templates/admin.html")),
(
"admin_group.html",
include_str!("../templates/admin_group.html"),
),
(
"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> {
@ -59,6 +64,7 @@ pub struct IndexTemplate<'a> {
#[serde(flatten)]
pub common: CommonTemplate<'a>,
pub ads: &'a Json,
pub groups: &'a Json,
pub new_ad_form_refill: Option<NewAdQuery>,
}
@ -73,6 +79,27 @@ pub struct AdminTemplate<'a> {
#[serde(flatten)]
pub common: CommonTemplate<'a>,
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)]

View File

@ -2,7 +2,16 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
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)]
pub struct AdId([u8; 16]);
@ -30,7 +39,7 @@ impl TryFrom<&[u8]> for AdId {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Ad {
pub author: String,
pub password: PasswordHash,
pub password: String,
pub price: String,
pub pubkey: Option<String>,
pub quantity: String,

View File

@ -27,6 +27,10 @@
</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">
@ -62,6 +66,19 @@
{{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="" 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>
@ -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://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>
<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://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>
<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>
{{#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">
@ -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://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>
<p><a href="{{root_url}}admin">Administration</a></p>