wip: move to axum

This commit is contained in:
emilis 2022-09-13 01:35:05 +01:00
parent 1c5c9caf2a
commit a5eef831c7
16 changed files with 436 additions and 290 deletions

119
Cargo.lock generated
View File

@ -51,6 +51,53 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 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]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.66" version = "0.3.66"
@ -245,6 +292,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
"axum",
"base-62", "base-62",
"handlebars", "handlebars",
"jsonwebtoken", "jsonwebtoken",
@ -257,7 +305,6 @@ dependencies = [
"tokio", "tokio",
"tokio-postgres", "tokio-postgres",
"warp", "warp",
"warp-embed",
] ]
[[package]] [[package]]
@ -460,6 +507,12 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@ -579,6 +632,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "matchit"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.4" version = "0.10.4"
@ -1257,6 +1316,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "sync_wrapper"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.12.6" version = "0.12.6"
@ -1444,6 +1509,47 @@ dependencies = [
"tracing", "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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@ -1633,17 +1739,6 @@ dependencies = [
"tracing", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

View File

@ -8,6 +8,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.64" anyhow = "1.0.64"
argon2 = "0.4.1" argon2 = "0.4.1"
axum = "0.5.16"
base-62 = "0.1.1" base-62 = "0.1.1"
handlebars = "4.3.3" handlebars = "4.3.3"
jsonwebtoken = "8.1.1" jsonwebtoken = "8.1.1"
@ -20,4 +21,3 @@ serde_json = "1.0.85"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] } tokio-postgres = { version = "0.7.7", features = ["with-serde_json-1"] }
warp = { version = "0.3.2" } warp = { version = "0.3.2" }
warp-embed = "0.4.0"

View File

@ -5,6 +5,7 @@ CREATE TABLE users (
username TEXT NOT NULL, username TEXT NOT NULL,
host TEXT, host TEXT,
display_name TEXT, display_name TEXT,
email TEXT NOT NULL,
password_hash TEXT NOT NULL password_hash TEXT NOT NULL
); );

View File

@ -17,8 +17,8 @@ impl Users {
pub async fn create_user(&self, u: User) -> Result<User, db::DBError> { pub async fn create_user(&self, u: User) -> Result<User, db::DBError> {
let row = self.0.lock().await.query_one( 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", "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], &[&sec::new_id(), &u.username, &u.host, &u.display_name, &u.password_hash, &u.email],
).await?; ).await?;
Ok(User { Ok(User {
id: row.get("id"), id: row.get("id"),
@ -26,6 +26,7 @@ impl Users {
host: u.host, host: u.host,
display_name: u.display_name, display_name: u.display_name,
password_hash: u.password_hash, password_hash: u.password_hash,
email: u.email,
}) })
} }
@ -73,6 +74,7 @@ pub struct User {
pub host: Option<String>, pub host: Option<String>,
pub display_name: Option<String>, pub display_name: Option<String>,
pub password_hash: String, pub password_hash: String,
pub email: String,
} }
impl From<&Row> for User { impl From<&Row> for User {
@ -83,6 +85,7 @@ impl From<&Row> for User {
host: row.get("host"), host: row.get("host"),
display_name: row.get("display_name"), display_name: row.get("display_name"),
password_hash: row.get("password_hash"), password_hash: row.get("password_hash"),
email: row.get("email"),
} }
} }
} }

View File

@ -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 }
}
}

View File

@ -1,18 +1,20 @@
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
use warp::{ use axum::{
http::HeaderValue, body::Full,
hyper::Uri, extract::Path,
path::Tail, handler::Handler,
reply::{Html, Response}, http::{header, StatusCode},
Filter, Rejection, Reply, 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 crate::svc::auth::AuthError;
use super::{ use super::{
servek::{Server, ServerError}, servek::{Server, ServerError},
CreateProfileRequest, ErrorTemplate, CreateProfileRequest, LoginRequest, Notification,
}; };
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
@ -21,147 +23,190 @@ use rust_embed::RustEmbed;
struct StaticData; struct StaticData;
impl Server { impl Server {
pub(super) async fn html( async fn index() -> impl IntoResponse {
&self, (
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { StatusCode::OK,
Self::index() response::Html(include_str!("../../templates/html/index.html")),
.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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Html<String>, 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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<Extract = impl warp::Reply, Error = warp::Rejection> + 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<String, String>| 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())
}),
),
) )
} }
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<response::Html<String>, 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<Server>,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(srv.hb.render("login", &serde_json::json!(()))?),
))
}
// async fn login(
// Extension(srv): Extension<Server>,
// Form(login): Form<LoginRequest>,
// ) -> Result<impl IntoResponse, ServerError> {
// 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<HashMap<String, String>>,
// ) -> Result<response::Html<String>, 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( fn with_server(
srv: Server, srv: Server,
) -> impl Filter<Extract = (Server,), Error = std::convert::Infallible> + Clone { ) -> impl Filter<Extract = (Server,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || srv.clone()) warp::any().map(move || srv.clone())
} }
fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { // fn profile(&self) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::get().and( // warp::get().and(
warp::path!("@" / String) // warp::path!("@" / String)
.and(Self::with_server(self.clone())) // .and(Self::with_server(self.clone()))
.and_then(|username: String, srv: Server| async move { // .and_then(|username: String, srv: Server| async move {
srv.hb // srv.hb
.render( // .render(
"profile", // "profile",
&serde_json::json!(srv // &serde_json::json!(srv
.profiler // .profiler
.profile(username) // .profile(username)
.await // .await
.map_err(|e| ServerError::from(e))?), // .map_err(|e| ServerError::from(e))?),
) // )
.map(|html| warp::reply::html(html)) // .map(|html| warp::reply::html(html))
.map_err(|e| ServerError::from(e).rejection()) // .map_err(|e| ServerError::from(e).rejection())
}), // }),
) // )
// }
async fn create_user(
Extension(srv): Extension<Server>,
Form(body): Form<CreateProfileRequest>,
) -> Result<impl IntoResponse, ServerError> {
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<Extract = impl Reply, Error = Rejection> + Clone { async fn signup_page(
warp::post().and( Extension(srv): Extension<Server>,
warp::body::content_length_limit(8192).and( ) -> Result<impl IntoResponse, ServerError> {
warp::path!("@" / String) Ok((
.and(Self::with_server(self.clone())) StatusCode::OK,
.and(warp::body::form()) response::Html(srv.hb.render("signup", &serde_json::json!(()))?),
.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<Extract = impl Reply, Error = Rejection> + 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)
},
)) ))
} }
fn signup_page_with(
&self,
tag_name: String,
message: String,
) -> Result<impl IntoResponse, ServerError> {
Ok((
StatusCode::OK,
response::Html(self.hb.render(
"signup",
&serde_json::json!(Notification { message, tag_name }),
)?),
))
}
async fn static_handler(Path(mut path): Path<String>) -> impl IntoResponse {
path.remove(0);
println!("getting path: {}", path);
StaticFile(path)
}
}
pub struct StaticFile<T>(pub T);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
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(),
}
}
} }

View File

@ -4,13 +4,22 @@ mod html;
pub mod servek; pub mod servek;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
struct ErrorTemplate { struct Notification {
pub error: String, pub message: String,
pub tag_name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct CreateProfileRequest { pub struct CreateProfileRequest {
pub password_hash: String, pub username: String,
pub password: String,
pub email: String,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]

View File

@ -1,6 +1,11 @@
use core::panic; 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 handlebars::{Handlebars, RenderError};
use warp::{ use warp::{
hyper::StatusCode, hyper::StatusCode,
@ -28,62 +33,33 @@ impl Server {
let mut hb = Handlebars::new(); let mut hb = Handlebars::new();
hb.register_template_string("profile", include_str!("../../templates/html/profile.html")) hb.register_template_string("profile", include_str!("../../templates/html/profile.html"))
.expect("profile template"); .expect("profile template");
hb.register_template_string( hb.register_template_string("login", include_str!("../../templates/html/login.html"))
"login-error", .expect("login template");
include_str!("../../templates/html/login-error.html"), hb.register_template_string("signup", include_str!("../../templates/html/signup.html"))
) .expect("login template");
.expect("login-error template"); hb.register_template_string("error-partial", r#"<h2 class="error">{{message}}</h2>"#)
.expect("error-partial");
hb.register_template_string("success-partial", r#"<h2 class="success">{{message}}</h2>"#)
.expect("success-partial");
Self { hb, profiler, auth } Self { hb, profiler, auth }
} }
pub async fn listen_and_serve(self, port: u16) -> ! { pub async fn listen_and_serve(self, port: u16) -> ! {
println!("starting server on port {}", port); let router = Router::new();
warp::serve(self.html().await.recover(Self::handle_rejection)) let router = self
.run(([127, 0, 0, 1], port)) .register_html(&router)
.await; .layer(Extension::<Server>(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") panic!("server stopped prematurely")
} }
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let code;
let message;
if err.is_not_found() {
code = StatusCode::NOT_FOUND;
message = "not found";
} else if let Some(err) = err.find::<ServerError>() {
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::<MethodNotAllowed>() {
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)] #[derive(Clone, Debug)]
@ -93,12 +69,6 @@ pub(super) enum ServerError {
BadRequest(String), BadRequest(String),
} }
impl ServerError {
pub(super) fn rejection(self) -> Rejection {
warp::reject::custom(self)
}
}
impl Reject for ServerError {} impl Reject for ServerError {}
impl From<RenderError> for ServerError { impl From<RenderError> for ServerError {
@ -133,3 +103,13 @@ impl Display for ServerError {
write!(f, "{}", self) 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(),
}
}
}

View File

@ -28,7 +28,12 @@ impl Profiler {
Ok(User::from(self.db.user(select).await?)) Ok(User::from(self.db.user(select).await?))
} }
pub async fn create_user(&self, username: String, password: String) -> Result<User, UserError> { pub async fn create_user(
&self,
username: String,
password: String,
email: String,
) -> Result<User, UserError> {
let result = self let result = self
.db .db
.create_user(users::User { .create_user(users::User {
@ -37,6 +42,7 @@ impl Profiler {
host: None, host: None,
display_name: None, display_name: None,
password_hash: sec::hash(password), password_hash: sec::hash(password),
email,
}) })
.await?; .await?;
@ -70,12 +76,6 @@ pub enum UserError {
Other(String), Other(String),
} }
impl From<anyhow::Error> for UserError {
fn from(err: anyhow::Error) -> Self {
Self::Other(format!("UserError: {}", err))
}
}
impl From<db::DBError> for UserError { impl From<db::DBError> for UserError {
fn from(err: db::DBError) -> Self { fn from(err: db::DBError) -> Self {
match err { match err {

View File

@ -1 +0,0 @@

View File

@ -14,9 +14,39 @@ input {
border-color: rebeccapurple; border-color: rebeccapurple;
} }
form>div {
width: 80%;
}
.error { .error {
color: rgba(255, 0, 0, 0.7); color: rgba(255, 0, 0, 0.7);
font-weight: bold; font-weight: bold;
border: 5px solid rgba(99, 0, 0, 0.2); border: 5px solid rgba(99, 0, 0, 0.2);
background-color: rgba(95, 0, 0, 0.5); 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;
}

View File

@ -9,6 +9,7 @@
<body> <body>
<h1>hi</h1> <h1>hi</h1>
<h1><a href="/login">login</a></h1> <h1><a href="/login">login</a></h1>
<h1><a href="/signup">sign up</a></h1>
</body> </body>
</html> </html>

View File

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - login error</title>
<link rel="stylesheet" href="/static/style/main.css">
<style>
#login-main {
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;
}
</style>
</head>
<body>
<h1>login</h1>
<div id="login-main">
<p>Login form</p>
<h2 class="error">error: {{error}}</h2>
<form id="login" method="post" action="/login">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="Submit" hidden>
</form>
</div>
</body>
</html>

View File

@ -4,29 +4,13 @@
<head> <head>
<title>flabk - login</title> <title>flabk - login</title>
<link rel="stylesheet" href="/static/style/main.css"> <link rel="stylesheet" href="/static/style/main.css">
<style>
#login-main {
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;
}
</style>
</head> </head>
<body> <body>
<h1>login</h1> <h1>login</h1>
<div id="login-main"> <div id="central">
<p>Login form</p> <p>login form</p>
{{> (lookup this "tag_name")}}
<form id="login" method="post" action="/login"> <form id="login" method="post" action="/login">
<input type="text" name="username"> <input type="text" name="username">
<input type="password" name="password"> <input type="password" name="password">

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>logging in</title>
</head>
<body>
<script>
window.localStorage.setItem('token', '{{token}}');
document.location.href = "/";
</script>
</body>
</html>

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>flabk - signup</title>
<link rel="stylesheet" href="/static/style/main.css">
</head>
<body>
<h1>signup</h1>
<div id="central">
<p>signup form</p>
{{> (lookup this "tag_name")}}
<form id="signup" method="post" action="/signup">
<div>
<label for="username">username</label>
<input type="text" name="username">
</div>
<br />
<div>
<label for="email">email</label>
<input type="email" name="email">
</div>
<br />
<div>
<label for="password">password</label>
<input type="password" name="password">
</div>
<input type="submit" value="Submit" hidden>
</form>
</div>
</body>
</html>