add create zone page
gg# modified: src/models/name.rs
This commit is contained in:
parent
1cd58f6ff7
commit
3c5de4cab6
14 changed files with 265 additions and 78 deletions
|
@ -1,10 +1,5 @@
|
||||||
main {
|
|
||||||
margin-top: 20vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 25rem;
|
max-width: 25rem;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ body {
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
@ -14,14 +16,31 @@ input {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-primary: #712da0;
|
||||||
|
--color-hightlight-1: #ffbac6;
|
||||||
|
--color-hightlight-2: #f560f5;
|
||||||
|
--color-contrast: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.feedback {
|
||||||
|
padding: .35rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.feedback.error {
|
||||||
|
background: #fddede;
|
||||||
|
color: #710000;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
background: #712da0;
|
background: var(--color-primary);
|
||||||
color: white;
|
color: var(--color-contrast);
|
||||||
border-left: 5px solid #ffbac6;
|
border-left: 5px solid var(--color-hightlight-1);
|
||||||
border-top: 5px solid #ffbac6;
|
border-top: 5px solid var(--color-hightlight-1);
|
||||||
border-right: 5px solid #f560f5;
|
border-right: 5px solid var(--color-hightlight-2);
|
||||||
border-bottom: 5px solid #f560f5;
|
border-bottom: 5px solid var(--color-hightlight-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="submit"]:hover {
|
input[type="submit"]:hover {
|
||||||
|
@ -36,6 +55,38 @@ form label {
|
||||||
margin-top: .75em;
|
margin-top: .75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
background: var(--color-primary);
|
||||||
|
max-width: 10vw;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
border-right: 5px solid var(--color-hightlight-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
color: var(--color-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li {
|
||||||
|
margin-top: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul ul {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
zone-content table {
|
zone-content table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -104,6 +104,8 @@ impl NomiloCommand for RunServerCommand {
|
||||||
ui::post_login_page,
|
ui::post_login_page,
|
||||||
ui::get_zones_page,
|
ui::get_zones_page,
|
||||||
ui::get_zone_records_page,
|
ui::get_zone_records_page,
|
||||||
|
ui::get_create_zone_page,
|
||||||
|
ui::post_create_zone_page,
|
||||||
])
|
])
|
||||||
.mount("/", static_files)
|
.mount("/", static_files)
|
||||||
.launch().await;
|
.launch().await;
|
||||||
|
|
|
@ -2,6 +2,7 @@ use rocket::http::{Cookie, SameSite, CookieJar};
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::dns::ZoneConnector;
|
||||||
use crate::DbConn;
|
use crate::DbConn;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
|
||||||
|
@ -39,3 +40,35 @@ pub async fn do_login(
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn create_zone(
|
||||||
|
conn: &DbConn,
|
||||||
|
mut dns_api: Box<dyn ZoneConnector>,
|
||||||
|
user_info: models::UserInfo,
|
||||||
|
zone_request: models::CreateZoneRequest,
|
||||||
|
) -> Result<models::Zone, models::ErrorResponse> {
|
||||||
|
user_info.check_admin()?;
|
||||||
|
|
||||||
|
dns_api.zone_exists(zone_request.name.clone(), models::DNSClass::IN.into()).await?;
|
||||||
|
|
||||||
|
let zone = conn.run(move |c| {
|
||||||
|
models::Zone::create_zone(c, zone_request)
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
Ok(zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_zones(
|
||||||
|
conn: &DbConn,
|
||||||
|
user_info: models::UserInfo,
|
||||||
|
) -> Result<Vec<models::Zone>, models::ErrorResponse> {
|
||||||
|
let zones = conn.run(move |c| {
|
||||||
|
if user_info.is_admin() {
|
||||||
|
models::Zone::get_all(c)
|
||||||
|
} else {
|
||||||
|
user_info.get_zones(c)
|
||||||
|
}
|
||||||
|
}).await?;
|
||||||
|
Ok(zones)
|
||||||
|
}
|
||||||
|
|
|
@ -31,8 +31,10 @@ impl DerefMut for DnsClient {
|
||||||
|
|
||||||
impl DnsClient {
|
impl DnsClient {
|
||||||
pub async fn from_config(dns_config: &DnsClientConfig) -> Result<Self, ProtoError> {
|
pub async fn from_config(dns_config: &DnsClientConfig) -> Result<Self, ProtoError> {
|
||||||
|
info!("Creating DNS client for {}", dns_config.server);
|
||||||
let (stream, handle) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(dns_config.server);
|
let (stream, handle) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(dns_config.server);
|
||||||
let signer = if let Some(tsig_config) = dns_config.tsig.as_ref() {
|
let signer = if let Some(tsig_config) = dns_config.tsig.as_ref() {
|
||||||
|
info!("Client configured with TSIG authentication");
|
||||||
Some(Arc::new(TSigner::new(
|
Some(Arc::new(TSigner::new(
|
||||||
tsig_config.key.clone(),
|
tsig_config.key.clone(),
|
||||||
tsig_config.algorithm.clone(),
|
tsig_config.algorithm.clone(),
|
||||||
|
@ -40,6 +42,7 @@ impl DnsClient {
|
||||||
60,
|
60,
|
||||||
)?.into()))
|
)?.into()))
|
||||||
} else {
|
} else {
|
||||||
|
info!("Client configured without authentication");
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,4 +80,3 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,6 +250,7 @@ impl ZoneConnector for DnsConnectorClient {
|
||||||
async fn zone_exists(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<()>
|
async fn zone_exists(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<()>
|
||||||
{
|
{
|
||||||
let response = {
|
let response = {
|
||||||
|
info!("Querying SOA for name {}", zone);
|
||||||
let query = self.client.query(zone.clone(), class, RecordType::SOA);
|
let query = self.client.query(zone.clone(), class, RecordType::SOA);
|
||||||
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::ops::Deref;
|
||||||
|
|
||||||
|
|
||||||
use rocket::request::FromParam;
|
use rocket::request::FromParam;
|
||||||
|
use rocket::form::{self, FromFormField, ValueField};
|
||||||
use serde::{Deserialize, Serialize, Deserializer, Serializer};
|
use serde::{Deserialize, Serialize, Deserializer, Serializer};
|
||||||
use trust_dns_proto::error::ProtoError;
|
use trust_dns_proto::error::ProtoError;
|
||||||
|
|
||||||
|
@ -48,6 +49,13 @@ impl SerdeName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_absolute_name(name: &str) -> Result<AbsoluteName, ProtoError> {
|
||||||
|
let mut name = Name::from_utf8(name)?;
|
||||||
|
if !name.is_fqdn() {
|
||||||
|
name.set_fqdn(true);
|
||||||
|
}
|
||||||
|
Ok(AbsoluteName(SerdeName(name)))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AbsoluteName(SerdeName);
|
pub struct AbsoluteName(SerdeName);
|
||||||
|
@ -56,14 +64,22 @@ impl<'r> FromParam<'r> for AbsoluteName {
|
||||||
type Error = ProtoError;
|
type Error = ProtoError;
|
||||||
|
|
||||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||||
let mut name = Name::from_utf8(¶m)?;
|
let name = parse_absolute_name(param)?;
|
||||||
if !name.is_fqdn() {
|
Ok(name)
|
||||||
name.set_fqdn(true);
|
|
||||||
}
|
|
||||||
Ok(AbsoluteName(SerdeName(name)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'v> FromFormField<'v> for AbsoluteName {
|
||||||
|
fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> {
|
||||||
|
let name = parse_absolute_name(field.value)
|
||||||
|
.map_err(|_| form::Error::validation("Invalid name"))?;
|
||||||
|
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Deref for AbsoluteName {
|
impl Deref for AbsoluteName {
|
||||||
type Target = Name;
|
type Target = Name;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub struct CreateUserRequest {
|
||||||
pub role: Option<Role>
|
pub role: Option<Role>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UserInfo {
|
pub struct UserInfo {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub struct AddZoneMemberRequest {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, FromForm)]
|
||||||
pub struct CreateZoneRequest {
|
pub struct CreateZoneRequest {
|
||||||
pub name: AbsoluteName,
|
pub name: AbsoluteName,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::DbConn;
|
||||||
use crate::dns::{RecordConnector, ZoneConnector};
|
use crate::dns::{RecordConnector, ZoneConnector};
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::{ParseRecordList};
|
use crate::models::{ParseRecordList};
|
||||||
|
use crate::controllers;
|
||||||
|
|
||||||
|
|
||||||
#[get("/zones/<zone>/records")]
|
#[get("/zones/<zone>/records")]
|
||||||
|
@ -130,33 +131,27 @@ pub async fn get_zones(
|
||||||
) -> Result<Json<Vec<models::Zone>>, models::ErrorResponse> {
|
) -> Result<Json<Vec<models::Zone>>, models::ErrorResponse> {
|
||||||
let user_info = user_info?;
|
let user_info = user_info?;
|
||||||
|
|
||||||
let zones = conn.run(move |c| {
|
controllers::get_zones(
|
||||||
if user_info.is_admin() {
|
&conn,
|
||||||
models::Zone::get_all(c)
|
user_info
|
||||||
} else {
|
).await.map(|zones| Json(zones))
|
||||||
user_info.get_zones(c)
|
|
||||||
}
|
|
||||||
}).await?;
|
|
||||||
|
|
||||||
Ok(Json(zones))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/zones", data = "<zone_request>")]
|
#[post("/zones", data = "<zone_request>")]
|
||||||
pub async fn create_zone(
|
pub async fn create_zone(
|
||||||
conn: DbConn,
|
conn: DbConn,
|
||||||
mut dns_api: Box<dyn ZoneConnector>,
|
dns_api: Box<dyn ZoneConnector>,
|
||||||
user_info: Result<models::UserInfo, models::ErrorResponse>,
|
user_info: Result<models::UserInfo, models::ErrorResponse>,
|
||||||
zone_request: Json<models::CreateZoneRequest>,
|
zone_request: Json<models::CreateZoneRequest>,
|
||||||
) -> Result<Json<models::Zone>, models::ErrorResponse> {
|
) -> Result<Json<models::Zone>, models::ErrorResponse> {
|
||||||
user_info?.check_admin()?;
|
let user_info = user_info?;
|
||||||
|
|
||||||
dns_api.zone_exists(zone_request.name.clone(), models::DNSClass::IN.into()).await?;
|
controllers::create_zone(
|
||||||
|
&conn,
|
||||||
let zone = conn.run(move |c| {
|
dns_api,
|
||||||
models::Zone::create_zone(c, zone_request.into_inner())
|
user_info,
|
||||||
}).await?;
|
zone_request.into_inner()
|
||||||
|
).await.map(|zone| Json(zone))
|
||||||
Ok(Json(zone))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,13 @@ use serde_json::{Value, json};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use rocket::http::{Status};
|
use rocket::http::{Status};
|
||||||
use rocket::http::uri::Origin;
|
use rocket::http::uri::Origin;
|
||||||
|
use rocket::form::Form;
|
||||||
|
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
use crate::controllers;
|
||||||
use crate::DbConn;
|
use crate::DbConn;
|
||||||
|
use crate::dns::ZoneConnector;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -44,14 +47,10 @@ pub async fn get_zone_records_page(user_info: models::UserInfo, zone: models::Ab
|
||||||
|
|
||||||
#[get("/zones")]
|
#[get("/zones")]
|
||||||
pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, Status> {
|
pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, Status> {
|
||||||
let zones = conn.run(move |c| {
|
let zones = controllers::get_zones(
|
||||||
if user_info.is_admin() {
|
&conn,
|
||||||
models::Zone::get_all(c)
|
user_info
|
||||||
} else {
|
).await.map_err(|e| e.status)?;
|
||||||
user_info.get_zones(c)
|
|
||||||
}
|
|
||||||
}).await.map_err(|e| models::ErrorResponse::from(e).status)?;
|
|
||||||
|
|
||||||
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/zones.html",
|
"pages/zones.html",
|
||||||
|
@ -62,3 +61,68 @@ pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[get("/zones/new")]
|
||||||
|
pub async fn get_create_zone_page(
|
||||||
|
conn: DbConn,
|
||||||
|
user_info: models::UserInfo,
|
||||||
|
origin: &Origin<'_>
|
||||||
|
) -> Result<Template<'static, Value>, Status> {
|
||||||
|
|
||||||
|
user_info
|
||||||
|
.check_admin()
|
||||||
|
.map_err(|e| models::ErrorResponse::from(e).status)?;
|
||||||
|
|
||||||
|
let zones = controllers::get_zones(
|
||||||
|
&conn,
|
||||||
|
user_info
|
||||||
|
).await.map_err(|e| e.status)?;
|
||||||
|
|
||||||
|
Ok(Template::new(
|
||||||
|
"pages/zones/new.html",
|
||||||
|
json!({
|
||||||
|
"zone": None::<models::Zone>,
|
||||||
|
"zones": zones,
|
||||||
|
"error": None::<String>,
|
||||||
|
"nav_page": origin.clone().into_normalized().path().as_str(),
|
||||||
|
"nav_sections": vec!["zones", "_new-zone"],
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/zones/new", data = "<zone_request>")]
|
||||||
|
pub async fn post_create_zone_page(
|
||||||
|
conn: DbConn,
|
||||||
|
dns_api: Box<dyn ZoneConnector>,
|
||||||
|
user_info: models::UserInfo,
|
||||||
|
zone_request: Form<models::CreateZoneRequest>,
|
||||||
|
origin: &Origin<'_>
|
||||||
|
) -> Result<Template<'static, Value>, Status> {
|
||||||
|
user_info
|
||||||
|
.check_admin()
|
||||||
|
.map_err(|e| models::ErrorResponse::from(e).status)?;
|
||||||
|
|
||||||
|
let zone = controllers::create_zone(
|
||||||
|
&conn,
|
||||||
|
dns_api,
|
||||||
|
user_info.clone(),
|
||||||
|
zone_request.into_inner()
|
||||||
|
).await.map_err(|e| e.status)?;
|
||||||
|
|
||||||
|
let zones = controllers::get_zones(
|
||||||
|
&conn,
|
||||||
|
user_info
|
||||||
|
).await.map_err(|e| e.status)?;
|
||||||
|
|
||||||
|
Ok(Template::new(
|
||||||
|
"pages/zones/new.html",
|
||||||
|
json!({
|
||||||
|
"zone": Some(zone),
|
||||||
|
"zones": zones,
|
||||||
|
"error": None::<String>,
|
||||||
|
"nav_page": origin.clone().into_normalized().path().as_str(),
|
||||||
|
"nav_sections": vec!["zones", "_new-zone"],
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -2,33 +2,43 @@
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<nav aria-label="Principal">
|
<nav aria-label="Principal">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/profil">Mon profile</a></li>
|
<li><a href="/profil">Mon profile</a></li>
|
||||||
<li>
|
<li>
|
||||||
{{ macros::nav_link(
|
{{ macros::nav_link(
|
||||||
content="Mes zones",
|
content="Mes zones",
|
||||||
href="/zones",
|
href="/zones",
|
||||||
current_page=nav_page,
|
current_page=nav_page,
|
||||||
section="zones",
|
section="zones",
|
||||||
current_sections=nav_sections,
|
current_sections=nav_sections,
|
||||||
) }}
|
) }}
|
||||||
<ul>
|
<ul>
|
||||||
{% for zone in zones %}
|
{% for zone in zones %}
|
||||||
<li>
|
<li>
|
||||||
{{ macros::nav_link(
|
{{ macros::nav_link(
|
||||||
content=zone.name,
|
content=zone.name,
|
||||||
href="/zone/" ~ zone.name,
|
href="/zone/" ~ zone.name,
|
||||||
current_page=nav_page,
|
current_page=nav_page,
|
||||||
section=zone.name,
|
section=zone.name,
|
||||||
current_sections=nav_sections,
|
current_sections=nav_sections,
|
||||||
) }}
|
) }}
|
||||||
{% endfor %}
|
</li>
|
||||||
</ul>
|
{% endfor %}
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
{{ macros::nav_link(
|
||||||
</nav>
|
content="Ajouter une zone",
|
||||||
|
href="/zones/new",
|
||||||
|
current_page=nav_page,
|
||||||
|
section="_new-zone",
|
||||||
|
current_sections=nav_sections,
|
||||||
|
) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
<main>
|
<main>
|
||||||
{% block main %}{% endblock main %}
|
{% block main %}{% endblock main %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -7,12 +7,13 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
{% if error %}
|
|
||||||
<p>
|
|
||||||
{{ error }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
|
{% if error %}
|
||||||
|
<p class="feedback error" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<label for="email">Adresse e-mail</label>
|
<label for="email">Adresse e-mail</label>
|
||||||
<input type="email" id="email" name="email">
|
<input type="email" id="email" name="email">
|
||||||
<label for="password">Mot de passe</label>
|
<label for="password">Mot de passe</label>
|
||||||
|
|
17
templates/pages/zones/new.html
Normal file
17
templates/pages/zones/new.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "bases/app.html" %}
|
||||||
|
|
||||||
|
{% block title %}Créer une zone ⋅ {% endblock title %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<form method="POST" action="/zones/new">
|
||||||
|
{% if error %}
|
||||||
|
<p class="feedback error" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<label for="zone_name">Nom de la zone</label>
|
||||||
|
<input type="text" id="zone_name" name="name">
|
||||||
|
<input type="submit" value="Créer la zone">
|
||||||
|
</form>
|
||||||
|
{% endblock main %}
|
Loading…
Reference in a new issue