Groups
This commit is contained in:
parent
c782124bcd
commit
296ec44fe4
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
|
@ -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"] }
|
||||||
|
|
19
src/cli.rs
19
src/cli.rs
|
@ -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,
|
||||||
|
|
18
src/db.rs
18
src/db.rs
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,38 +20,37 @@ 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,
|
||||||
pub psw: 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)]
|
#[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)]
|
||||||
|
|
372
src/server.rs
372
src/server.rs
|
@ -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,30 +521,18 @@ 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,
|
||||||
|
@ -336,6 +550,7 @@ async fn handle_rm_ad(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(tide::Redirect::new(&config.root_url).into())
|
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(
|
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
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
13
src/utils.rs
13
src/utils.rs
|
@ -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,
|
||||||
|
|
|
@ -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 :</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>) :</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>. 🦀 É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>. 🦀 É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 : Claudia Peters, FreeImages.com</p>
|
Image de fond : Claudia Peters, FreeImages.com</p>
|
||||||
|
|
||||||
<p><a href="{{root_url}}">Accueil</a> – <a href="{{root_url}}admin/logout">Verrouiller</a></p>
|
<p><a href="{{root_url}}">Accueil</a> – <a href="{{root_url}}admin/logout">Verrouiller</a></p>
|
||||||
|
|
|
@ -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}} – Administration – {{title}}</h1>
|
||||||
|
|
||||||
|
{{#if errors}}
|
||||||
|
<div id="errors">
|
||||||
|
<span>Oups, il y a un problème :</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 : {{this.ad.pubkey}}<br/>{{/if}}
|
||||||
|
Prix : {{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 :</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>) :</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>. 🦀 É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 : Claudia Peters, FreeImages.com</p>
|
||||||
|
|
||||||
|
<p><a href="{{root_url}}">Accueil</a> – <a href="{{root_url}}admin/logout">Verrouiller</a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>. 🦀 É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>. 🦀 É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 : Claudia Peters, FreeImages.com</p>
|
Image de fond : Claudia Peters, FreeImages.com</p>
|
||||||
|
|
||||||
<p><a href="{{root_url}}">Accueil</a></p>
|
<p><a href="{{root_url}}">Accueil</a></p>
|
||||||
|
|
|
@ -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 :</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 : {{this.ad.pubkey}}<br/>{{/if}}
|
||||||
|
Prix : {{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 :</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 :</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é :</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 :</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 :</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 :</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 :</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>. 🦀 É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 : Claudia Peters, FreeImages.com</p>
|
||||||
|
|
||||||
|
<p><a href="{{root_url}}admin">Administration</a></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>. 🦀 É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>. 🦀 É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 : Claudia Peters, FreeImages.com</p>
|
Image de fond : Claudia Peters, FreeImages.com</p>
|
||||||
|
|
||||||
<p><a href="{{root_url}}admin">Administration</a></p>
|
<p><a href="{{root_url}}admin">Administration</a></p>
|
||||||
|
|
Loading…
Reference in New Issue