From a5eef831c716ed532775347e764c2c3890ccc370 Mon Sep 17 00:00:00 2001 From: emilis Date: Tue, 13 Sep 2022 01:35:05 +0100 Subject: [PATCH] wip: move to axum --- Cargo.lock | 119 ++++++++++-- Cargo.toml | 2 +- migrate/1662887862_schema.up.sql | 1 + src/database/users.rs | 7 +- src/model.rs | 11 -- src/servek/html.rs | 319 ++++++++++++++++++------------- src/servek/mod.rs | 15 +- src/servek/servek.rs | 94 ++++----- src/svc/profiles.rs | 14 +- static/style/login.css | 1 - static/style/main.css | 30 +++ templates/html/index.html | 1 + templates/html/login-error.html | 40 ---- templates/html/login.html | 22 +-- templates/html/post-signin.html | 15 ++ templates/html/signup.html | 35 ++++ 16 files changed, 436 insertions(+), 290 deletions(-) delete mode 100644 static/style/login.css delete mode 100644 templates/html/login-error.html create mode 100644 templates/html/post-signin.html create mode 100644 templates/html/signup.html diff --git a/Cargo.lock b/Cargo.lock index 2ea31f6..73847a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,53 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.66" @@ -245,6 +292,7 @@ version = "0.0.1" dependencies = [ "anyhow", "argon2", + "axum", "base-62", "handlebars", "jsonwebtoken", @@ -257,7 +305,6 @@ dependencies = [ "tokio", "tokio-postgres", "warp", - "warp-embed", ] [[package]] @@ -460,6 +507,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "httparse" version = "1.8.0" @@ -579,6 +632,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + [[package]] name = "md-5" version = "0.10.4" @@ -1257,6 +1316,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "synstructure" version = "0.12.6" @@ -1444,6 +1509,47 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.2" @@ -1633,17 +1739,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "warp-embed" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b958139e25f097e0ebde85342a3a2dbb728983ca893ba96b7fb8f448337110af" -dependencies = [ - "mime_guess", - "rust-embed", - "warp", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 11d89ff..0db7b9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] anyhow = "1.0.64" argon2 = "0.4.1" +axum = "0.5.16" base-62 = "0.1.1" handlebars = "4.3.3" jsonwebtoken = "8.1.1" @@ -20,4 +21,3 @@ serde_json = "1.0.85" tokio = { version = "1", features = ["full"] } tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] } warp = { version = "0.3.2" } -warp-embed = "0.4.0" diff --git a/migrate/1662887862_schema.up.sql b/migrate/1662887862_schema.up.sql index 250a063..26cc7a8 100644 --- a/migrate/1662887862_schema.up.sql +++ b/migrate/1662887862_schema.up.sql @@ -5,6 +5,7 @@ CREATE TABLE users ( username TEXT NOT NULL, host TEXT, display_name TEXT, + email TEXT NOT NULL, password_hash TEXT NOT NULL ); diff --git a/src/database/users.rs b/src/database/users.rs index dfc09f7..f7588da 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -17,8 +17,8 @@ impl Users { pub async fn create_user(&self, u: User) -> Result { let row = self.0.lock().await.query_one( - "insert into users (id, username, host, display_name, password_hash) values ($1, $2, $3, $4, $5) returning id", - &[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash], + "insert into users (id, username, host, display_name, password_hash, email) values ($1, $2, $3, $4, $5, $6) returning id", + &[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email], ).await?; Ok(User { id: row.get("id"), @@ -26,6 +26,7 @@ impl Users { host: u.host, display_name: u.display_name, password_hash: u.password_hash, + email: u.email, }) } @@ -73,6 +74,7 @@ pub struct User { pub host: Option, pub display_name: Option, pub password_hash: String, + pub email: String, } impl From<&Row> for User { @@ -83,6 +85,7 @@ impl From<&Row> for User { host: row.get("host"), display_name: row.get("display_name"), password_hash: row.get("password_hash"), + email: row.get("email"), } } } diff --git a/src/model.rs b/src/model.rs index ccda0fc..8b13789 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,12 +1 @@ -use serde::Serialize; -#[derive(Clone, Serialize)] -pub struct Error<'a> { - pub error: &'a str, -} - -impl<'a> Error<'a> { - pub fn error(error: &'a str) -> Self { - Self { error } - } -} diff --git a/src/servek/html.rs b/src/servek/html.rs index e4126d4..7a934b8 100644 --- a/src/servek/html.rs +++ b/src/servek/html.rs @@ -1,18 +1,20 @@ use std::{collections::HashMap, str::FromStr}; -use warp::{ - http::HeaderValue, - hyper::Uri, - path::Tail, - reply::{Html, Response}, - Filter, Rejection, Reply, +use axum::{ + body::Full, + extract::Path, + handler::Handler, + http::{header, StatusCode}, + response::{self, Html, IntoResponse, Redirect, Response}, + routing, Extension, Form, Json, Router, }; +use warp::{http::HeaderValue, hyper::Uri, path::Tail, Filter, Rejection, Reply}; use crate::svc::auth::AuthError; use super::{ servek::{Server, ServerError}, - CreateProfileRequest, ErrorTemplate, + CreateProfileRequest, LoginRequest, Notification, }; use rust_embed::RustEmbed; @@ -21,147 +23,190 @@ use rust_embed::RustEmbed; struct StaticData; impl Server { - pub(super) async fn html( - &self, - ) -> impl Filter + Clone { - Self::index() - .or(self.profile()) - .or(self.create_profile()) - .or(Server::static_files()) - .or(self.login_page()) - .or(self.login()) - .or(Self::handler_404()) - } - - fn index() -> impl Filter + Clone { - warp::get().and(warp::path::end().map(move || { - warp::reply::html(include_str!("../../templates/html/index.html").to_owned()) - })) - } - - fn handler_404() -> impl Filter + Clone { - warp::get().and(warp::path::end().map(move || { - warp::reply::html(include_str!("../../templates/html/404.html").to_owned()) - })) - } - - fn login_with_error(&self, error: String) -> Result, Rejection> { - self.hb - .render("login-error", &serde_json::json!(ErrorTemplate { error })) - .map(|html| warp::reply::html(html)) - .map_err(|e| ServerError::from(e).rejection()) - } - - fn login_page( - &self, - ) -> impl Filter + Clone { - warp::get().and(warp::path::path("login").map(move || { - warp::reply::html(include_str!("../../templates/html/login.html").to_owned()) - })) - } - - fn login(&self) -> impl Filter + Clone { - warp::post().and( - warp::body::content_length_limit(8192).and( - warp::path::path("login") - .and(Self::with_server(self.clone())) - .and(warp::body::form()) - .and_then(|srv: Server, body: HashMap| async move { - let user = body.get("username").ok_or( - ServerError::BadRequest("no username provided".to_owned()).rejection(), - )?; - let pass = body.get("password").ok_or( - ServerError::BadRequest("no password provided".to_owned()).rejection(), - )?; - let token = srv - .auth - .login(user.clone(), pass.clone()) - .await - .map(|html| warp::reply::html(html)); - if let Err(e) = &token { - if let AuthError::InvalidCredentials = e { - return srv.login_with_error("invalid credentials".to_owned()); - } - } - token.map_err(|e| ServerError::from(e).rejection()) - }), - ), + async fn index() -> impl IntoResponse { + ( + StatusCode::OK, + response::Html(include_str!("../../templates/html/index.html")), ) } + pub(super) fn register_html(&self, router: &Router) -> Router { + router + .clone() + .route("/", routing::get(Self::index)) + .route("/login", routing::get(Self::login_page)) + .route( + "/signup", + routing::get(Self::signup_page).post(Self::create_user), + ) + .route("/static/*file", routing::get(Self::static_handler)) + .fallback(routing::get(Self::handler_404)) + } + + async fn handler_404() -> impl IntoResponse { + ( + StatusCode::OK, + response::Html(include_str!("../../templates/html/404.html")), + ) + } + + fn login_page_with( + &self, + tag_name: String, + message: String, + ) -> Result, ServerError> { + Ok(self + .hb + .render( + "login", + &serde_json::json!(Notification { message, tag_name }), + ) + .map(|html| response::Html(html))?) + } + + async fn login_page( + Extension(srv): Extension, + ) -> Result { + Ok(( + StatusCode::OK, + response::Html(srv.hb.render("login", &serde_json::json!(()))?), + )) + } + + // async fn login( + // Extension(srv): Extension, + // Form(login): Form, + // ) -> Result { + // if login.username == "" || login.password == "" { + // return srv.login_page_with( + // "error-partial".to_owned(), + // "credentials required".to_owned(), + // ); + // } + // let token = srv.auth.login(login.username, login.password).await?; + // } + + // async fn login( + // srv: Server, + // Form(body): Form>, + // ) -> Result, ServerError> { + // let user = body + // .get("username") + // .ok_or(ServerError::BadRequest("no username provided".to_owned()))?; + // let pass = body + // .get("password") + // .ok_or(ServerError::BadRequest("no password provided".to_owned()))?; + + // let token = srv + // .auth + // .login(user.clone(), pass.clone()) + // .await + // .map(|html| response::Html(html)); + // if let Err(e) = &token { + // if let AuthError::InvalidCredentials = e { + // return srv.login_with_error("invalid credentials".to_owned()); + // } + // } + // Ok(token?) + // } + fn with_server( srv: Server, ) -> impl Filter + Clone { warp::any().map(move || srv.clone()) } - fn profile(&self) -> impl Filter + Clone { - warp::get().and( - warp::path!("@" / String) - .and(Self::with_server(self.clone())) - .and_then(|username: String, srv: Server| async move { - srv.hb - .render( - "profile", - &serde_json::json!(srv - .profiler - .profile(username) - .await - .map_err(|e| ServerError::from(e))?), - ) - .map(|html| warp::reply::html(html)) - .map_err(|e| ServerError::from(e).rejection()) - }), - ) + // fn profile(&self) -> impl Filter + Clone { + // warp::get().and( + // warp::path!("@" / String) + // .and(Self::with_server(self.clone())) + // .and_then(|username: String, srv: Server| async move { + // srv.hb + // .render( + // "profile", + // &serde_json::json!(srv + // .profiler + // .profile(username) + // .await + // .map_err(|e| ServerError::from(e))?), + // ) + // .map(|html| warp::reply::html(html)) + // .map_err(|e| ServerError::from(e).rejection()) + // }), + // ) + // } + + async fn create_user( + Extension(srv): Extension, + Form(body): Form, + ) -> Result { + if body.username == "" { + return srv.signup_page_with("error-partial".to_owned(), "empty username".to_owned()); + } + if body.password == "" { + return srv.signup_page_with("error-partial".to_owned(), "empty password".to_owned()); + } + if body.email == "" { + return srv.signup_page_with("error-partial".to_owned(), "empty email".to_owned()); + } + srv.profiler + .create_user(body.username, body.password, body.email) + .await?; + srv.signup_page_with("success-partial".to_owned(), "signup successful".to_owned()) } - fn create_profile(&self) -> impl Filter + Clone { - warp::post().and( - warp::body::content_length_limit(8192).and( - warp::path!("@" / String) - .and(Self::with_server(self.clone())) - .and(warp::body::form()) - .and_then( - |username: String, srv: Server, body: CreateProfileRequest| async move { - if body.password_hash.len() == 0 { - return Err(ServerError::BadRequest( - "cannot have an empty password".to_owned(), - ) - .rejection()); - } - let user = srv - .profiler - .create_user(username, body.password_hash) - .await - .map_err(|e| ServerError::from(e)); - match user { - Ok(u) => Ok(warp::redirect( - Uri::from_str(format!("/@/{}", u.username).as_str()).unwrap(), - )), - Err(e) => Err(e.rejection()), - } - }, - ), - ), - ) - } - - fn static_files() -> impl Filter + Clone { - warp::get().and(warp::path("static").and(warp::path::tail()).and_then( - |path: Tail| async move { - let asset = match StaticData::get(path.as_str()) { - Some(a) => a, - None => return Err(ServerError::NotFound.rejection()), - }; - let mime = mime_guess::from_path(path.as_str()).first_or_octet_stream(); - - let mut res = Response::new(asset.data.into()); - res.headers_mut().insert( - "Content-Type", - HeaderValue::from_str(mime.as_ref()).unwrap(), - ); - Ok(res) - }, + async fn signup_page( + Extension(srv): Extension, + ) -> Result { + Ok(( + StatusCode::OK, + response::Html(srv.hb.render("signup", &serde_json::json!(()))?), )) } + + fn signup_page_with( + &self, + tag_name: String, + message: String, + ) -> Result { + Ok(( + StatusCode::OK, + response::Html(self.hb.render( + "signup", + &serde_json::json!(Notification { message, tag_name }), + )?), + )) + } + + async fn static_handler(Path(mut path): Path) -> impl IntoResponse { + path.remove(0); + println!("getting path: {}", path); + StaticFile(path) + } +} + +pub struct StaticFile(pub T); + +impl IntoResponse for StaticFile +where + T: Into, +{ + fn into_response(self) -> axum::response::Response { + let path = self.0.into(); + + match StaticData::get(path.as_str()) { + Some(content) => { + let body = axum::body::boxed(Full::from(content.data)); + let mime = mime_guess::from_path(path).first_or_octet_stream(); + Response::builder() + .header(header::CONTENT_TYPE, mime.as_ref()) + .body(body) + .unwrap() + } + None => Response::builder() + .status(StatusCode::NOT_FOUND) + .body(axum::body::boxed(Full::from("404"))) + .unwrap(), + } + } } diff --git a/src/servek/mod.rs b/src/servek/mod.rs index b183d8d..b2126ab 100644 --- a/src/servek/mod.rs +++ b/src/servek/mod.rs @@ -4,13 +4,22 @@ mod html; pub mod servek; #[derive(Debug, Clone, Serialize)] -struct ErrorTemplate { - pub error: String, +struct Notification { + pub message: String, + pub tag_name: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: String, } #[derive(Debug, Clone, Deserialize)] pub struct CreateProfileRequest { - pub password_hash: String, + pub username: String, + pub password: String, + pub email: String, } #[derive(Debug, Clone, Serialize)] diff --git a/src/servek/servek.rs b/src/servek/servek.rs index d4e4d44..0ce24b9 100644 --- a/src/servek/servek.rs +++ b/src/servek/servek.rs @@ -1,6 +1,11 @@ use core::panic; -use std::{convert::Infallible, fmt::Display}; +use std::{convert::Infallible, fmt::Display, net::SocketAddr}; +use axum::{ + http::uri::InvalidUri, + response::{self, IntoResponse, Response}, + routing, Extension, Router, +}; use handlebars::{Handlebars, RenderError}; use warp::{ hyper::StatusCode, @@ -28,62 +33,33 @@ impl Server { let mut hb = Handlebars::new(); hb.register_template_string("profile", include_str!("../../templates/html/profile.html")) .expect("profile template"); - hb.register_template_string( - "login-error", - include_str!("../../templates/html/login-error.html"), - ) - .expect("login-error template"); - + hb.register_template_string("login", include_str!("../../templates/html/login.html")) + .expect("login template"); + hb.register_template_string("signup", include_str!("../../templates/html/signup.html")) + .expect("login template"); + hb.register_template_string("error-partial", r#"

{{message}}

"#) + .expect("error-partial"); + hb.register_template_string("success-partial", r#"

{{message}}

"#) + .expect("success-partial"); Self { hb, profiler, auth } } pub async fn listen_and_serve(self, port: u16) -> ! { - println!("starting server on port {}", port); - warp::serve(self.html().await.recover(Self::handle_rejection)) - .run(([127, 0, 0, 1], port)) - .await; + let router = Router::new(); + let router = self + .register_html(&router) + .layer(Extension::(self.clone())); + + let addr = SocketAddr::from(([127, 0, 0, 1], port)); + println!("listening on {}", addr); + + axum::Server::bind(&addr) + .serve(router.into_make_service()) + .await + .unwrap(); + panic!("server stopped prematurely") } - - async fn handle_rejection(err: Rejection) -> Result { - let code; - let message; - - if err.is_not_found() { - code = StatusCode::NOT_FOUND; - message = "not found"; - } else if let Some(err) = err.find::() { - match err { - ServerError::Internal(err) => { - println!("internal server error: {}", err); - code = StatusCode::INTERNAL_SERVER_ERROR; - message = "internal server error"; - } - ServerError::NotFound => { - code = StatusCode::NOT_FOUND; - message = "not found"; - } - ServerError::BadRequest(err) => { - code = StatusCode::BAD_REQUEST; - message = err; - } - } - } else if let Some(err) = err.find::() { - println!("MethodNotAllowed: {:#?}", err); - code = StatusCode::NOT_FOUND; - message = "not found"; - } else { - // We should have expected this... Just log and say its a 500 - println!("FIXME: unhandled rejection: {:?}", err); - code = StatusCode::INTERNAL_SERVER_ERROR; - message = "internal server error" - } - - Ok(warp::reply::with_status( - warp::reply::json(&model::Error::error(message)), - code, - )) - } } #[derive(Clone, Debug)] @@ -93,12 +69,6 @@ pub(super) enum ServerError { BadRequest(String), } -impl ServerError { - pub(super) fn rejection(self) -> Rejection { - warp::reject::custom(self) - } -} - impl Reject for ServerError {} impl From for ServerError { @@ -133,3 +103,13 @@ impl Display for ServerError { write!(f, "{}", self) } } + +impl IntoResponse for ServerError { + fn into_response(self) -> axum::response::Response { + match self { + ServerError::Internal(err) => (StatusCode::INTERNAL_SERVER_ERROR, err).into_response(), + ServerError::NotFound => (StatusCode::NOT_FOUND, "").into_response(), + ServerError::BadRequest(err) => (StatusCode::BAD_REQUEST, err).into_response(), + } + } +} diff --git a/src/svc/profiles.rs b/src/svc/profiles.rs index 20f9de1..36b542e 100644 --- a/src/svc/profiles.rs +++ b/src/svc/profiles.rs @@ -28,7 +28,12 @@ impl Profiler { Ok(User::from(self.db.user(select).await?)) } - pub async fn create_user(&self, username: String, password: String) -> Result { + pub async fn create_user( + &self, + username: String, + password: String, + email: String, + ) -> Result { let result = self .db .create_user(users::User { @@ -37,6 +42,7 @@ impl Profiler { host: None, display_name: None, password_hash: sec::hash(password), + email, }) .await?; @@ -70,12 +76,6 @@ pub enum UserError { Other(String), } -impl From for UserError { - fn from(err: anyhow::Error) -> Self { - Self::Other(format!("UserError: {}", err)) - } -} - impl From for UserError { fn from(err: db::DBError) -> Self { match err { diff --git a/static/style/login.css b/static/style/login.css deleted file mode 100644 index 8b13789..0000000 --- a/static/style/login.css +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/style/main.css b/static/style/main.css index 9013015..556858f 100644 --- a/static/style/main.css +++ b/static/style/main.css @@ -14,9 +14,39 @@ input { border-color: rebeccapurple; } +form>div { + width: 80%; +} + .error { color: rgba(255, 0, 0, 0.7); font-weight: bold; border: 5px solid rgba(99, 0, 0, 0.2); background-color: rgba(95, 0, 0, 0.5); } + +.success { + color: rgba(0, 202, 0, 0.7); + font-weight: bold; + border: 5px solid rgba(0, 99, 0, 0.2); + background-color: rgba(0, 99, 0, 0.5); +} + +#central { + width: 30%; + border: 5px solid rebeccapurple; + height: 30vw; + padding: 10px; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: rgba(13, 6, 19, 0.4); + font-size: large; + font-weight: bold; +} + +a { + color: rebeccapurple; +} diff --git a/templates/html/index.html b/templates/html/index.html index 07473be..092f0dc 100644 --- a/templates/html/index.html +++ b/templates/html/index.html @@ -9,6 +9,7 @@

hi

login

+

sign up

diff --git a/templates/html/login-error.html b/templates/html/login-error.html deleted file mode 100644 index aba163c..0000000 --- a/templates/html/login-error.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - flabk - login error - - - - - -

login

-
-

Login form

-

error: {{error}}

-
- - - -
-
- - - - diff --git a/templates/html/login.html b/templates/html/login.html index 90318b5..c78b7f6 100644 --- a/templates/html/login.html +++ b/templates/html/login.html @@ -4,29 +4,13 @@ flabk - login -

login

-
-

Login form

+
+

login form

+ {{> (lookup this "tag_name")}}
diff --git a/templates/html/post-signin.html b/templates/html/post-signin.html new file mode 100644 index 0000000..2d3f0db --- /dev/null +++ b/templates/html/post-signin.html @@ -0,0 +1,15 @@ + + + + + logging in + + + + + + + diff --git a/templates/html/signup.html b/templates/html/signup.html new file mode 100644 index 0000000..4ec7cc3 --- /dev/null +++ b/templates/html/signup.html @@ -0,0 +1,35 @@ + + + + + flabk - signup + + + + +

signup

+
+

signup form

+ {{> (lookup this "tag_name")}} + +
+ + +
+
+
+ + +
+
+
+ + +
+ + +
+ + + +