feat: login and join HTML pages
This commit is contained in:
parent
a1caff7538
commit
0bf5a35673
28 changed files with 1913 additions and 0 deletions
119
src/pages/auth/login.rs
Normal file
119
src/pages/auth/login.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::http::header::ContentType;
|
||||||
|
use tera::Context;
|
||||||
|
|
||||||
|
use crate::api::v1::RedirectQuery;
|
||||||
|
use crate::ctx::api::v1::auth::Login as LoginPayload;
|
||||||
|
use crate::pages::errors::*;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::AppCtx;
|
||||||
|
|
||||||
|
pub use super::*;
|
||||||
|
|
||||||
|
pub struct Login {
|
||||||
|
ctx: RefCell<Context>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const LOGIN: TemplateFile = TemplateFile::new("login", "pages/auth/login.html");
|
||||||
|
|
||||||
|
impl CtxError for Login {
|
||||||
|
fn with_error(&self, e: &ReadableError) -> String {
|
||||||
|
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||||
|
self.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Login {
|
||||||
|
pub fn new(settings: &Settings, payload: Option<&LoginPayload>) -> Self {
|
||||||
|
let ctx = RefCell::new(context(settings));
|
||||||
|
if let Some(payload) = payload {
|
||||||
|
ctx.borrow_mut().insert(PAYLOAD_KEY, payload);
|
||||||
|
}
|
||||||
|
Self { ctx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
TEMPLATES.render(LOGIN.name, &self.ctx.borrow()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page(s: &Settings) -> String {
|
||||||
|
let p = Self::new(s, None);
|
||||||
|
p.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::get(path = "PAGES.auth.login")]
|
||||||
|
pub async fn get_login(ctx: AppCtx) -> impl Responder {
|
||||||
|
let login = Login::page(&ctx.settings);
|
||||||
|
let html = ContentType::html();
|
||||||
|
HttpResponse::Ok().content_type(html).body(login)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get_login);
|
||||||
|
cfg.service(login_submit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(path = "PAGES.auth.login")]
|
||||||
|
pub async fn login_submit(
|
||||||
|
id: Identity,
|
||||||
|
payload: web::Form<LoginPayload>,
|
||||||
|
query: web::Query<RedirectQuery>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> PageResult<impl Responder, Login> {
|
||||||
|
let username = ctx
|
||||||
|
.login(&payload)
|
||||||
|
.await
|
||||||
|
.map_err(|e| PageError::new(Login::new(&ctx.settings, Some(&payload)), e))?;
|
||||||
|
id.remember(username);
|
||||||
|
let query = query.into_inner();
|
||||||
|
if let Some(redirect_to) = query.redirect_to {
|
||||||
|
Ok(HttpResponse::Found()
|
||||||
|
.insert_header((http::header::LOCATION, redirect_to))
|
||||||
|
.finish())
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::Found()
|
||||||
|
.insert_header((http::header::LOCATION, PAGES.home))
|
||||||
|
.finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Login;
|
||||||
|
use super::LoginPayload;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::pages::errors::*;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_page_renders() {
|
||||||
|
let settings = Settings::new().unwrap();
|
||||||
|
Login::page(&settings);
|
||||||
|
let payload = LoginPayload {
|
||||||
|
login: "foo".into(),
|
||||||
|
password: "foo".into(),
|
||||||
|
};
|
||||||
|
let page = Login::new(&settings, Some(&payload));
|
||||||
|
page.with_error(&ReadableError::new(&ServiceError::WrongPassword));
|
||||||
|
page.render();
|
||||||
|
}
|
||||||
|
}
|
162
src/pages/auth/mod.rs
Normal file
162
src/pages/auth/mod.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::*;
|
||||||
|
|
||||||
|
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
|
||||||
|
|
||||||
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
pub const AUTH_BASE: TemplateFile = TemplateFile::new("authbase", "pages/auth/base.html");
|
||||||
|
|
||||||
|
pub fn register_templates(t: &mut tera::Tera) {
|
||||||
|
for template in [AUTH_BASE, login::LOGIN, register::REGISTER].iter() {
|
||||||
|
template.register(t).expect(template.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(signout);
|
||||||
|
register::services(cfg);
|
||||||
|
login::services(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::get(
|
||||||
|
path = "PAGES.auth.logout",
|
||||||
|
wrap = "super::get_auth_middleware()"
|
||||||
|
)]
|
||||||
|
async fn signout(id: Identity) -> impl Responder {
|
||||||
|
use actix_auth_middleware::GetLoginRoute;
|
||||||
|
|
||||||
|
if id.identity().is_some() {
|
||||||
|
id.forget();
|
||||||
|
}
|
||||||
|
println!("received signout");
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header((http::header::LOCATION, PAGES.get_login_route(None)))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[post(path = "PAGES.auth.login")]
|
||||||
|
//pub async fn login_submit(
|
||||||
|
// id: Identity,
|
||||||
|
// payload: web::Form<runners::Login>,
|
||||||
|
// data: AppData,
|
||||||
|
//) -> PageResult<impl Responder> {
|
||||||
|
// let payload = payload.into_inner();
|
||||||
|
// match runners::login_runner(&payload, &data).await {
|
||||||
|
// Ok(username) => {
|
||||||
|
// id.remember(username);
|
||||||
|
// Ok(HttpResponse::Found()
|
||||||
|
// .insert_header((header::LOCATION, PAGES.home))
|
||||||
|
// .finish())
|
||||||
|
// }
|
||||||
|
// Err(e) => {
|
||||||
|
// let status = e.status_code();
|
||||||
|
// let heading = status.canonical_reason().unwrap_or("Error");
|
||||||
|
//
|
||||||
|
// Ok(HttpResponseBuilder::new(status)
|
||||||
|
// .content_type("text/html; charset=utf-8")
|
||||||
|
// .body(
|
||||||
|
// IndexPage::new(heading, &format!("{}", e))
|
||||||
|
// .render_once()
|
||||||
|
// .unwrap(),
|
||||||
|
// ))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//#[cfg(test)]
|
||||||
|
//mod tests {
|
||||||
|
// use actix_web::test;
|
||||||
|
//
|
||||||
|
// use super::*;
|
||||||
|
//
|
||||||
|
// use crate::api::v1::auth::runners::{Login, Register};
|
||||||
|
// use crate::data::Data;
|
||||||
|
// use crate::tests::*;
|
||||||
|
// use crate::*;
|
||||||
|
// use actix_web::http::StatusCode;
|
||||||
|
//
|
||||||
|
// #[actix_rt::test]
|
||||||
|
// async fn auth_form_works() {
|
||||||
|
// let data = Data::new().await;
|
||||||
|
// const NAME: &str = "testuserform";
|
||||||
|
// const PASSWORD: &str = "longpassword";
|
||||||
|
//
|
||||||
|
// let app = get_app!(data).await;
|
||||||
|
//
|
||||||
|
// delete_user(NAME, &data).await;
|
||||||
|
//
|
||||||
|
// // 1. Register with email == None
|
||||||
|
// let msg = Register {
|
||||||
|
// username: NAME.into(),
|
||||||
|
// password: PASSWORD.into(),
|
||||||
|
// confirm_password: PASSWORD.into(),
|
||||||
|
// email: None,
|
||||||
|
// };
|
||||||
|
// let resp = test::call_service(
|
||||||
|
// &app,
|
||||||
|
// post_request!(&msg, V1_API_ROUTES.auth.register).to_request(),
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
// assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
//
|
||||||
|
// // correct form login
|
||||||
|
// let msg = Login {
|
||||||
|
// login: NAME.into(),
|
||||||
|
// password: PASSWORD.into(),
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// let resp = test::call_service(
|
||||||
|
// &app,
|
||||||
|
// post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
// assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
// let headers = resp.headers();
|
||||||
|
// assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.home,);
|
||||||
|
//
|
||||||
|
// // incorrect form login
|
||||||
|
// let msg = Login {
|
||||||
|
// login: NAME.into(),
|
||||||
|
// password: NAME.into(),
|
||||||
|
// };
|
||||||
|
// let resp = test::call_service(
|
||||||
|
// &app,
|
||||||
|
// post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
// assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
//
|
||||||
|
// // non-existent form login
|
||||||
|
// let msg = Login {
|
||||||
|
// login: PASSWORD.into(),
|
||||||
|
// password: PASSWORD.into(),
|
||||||
|
// };
|
||||||
|
// let resp = test::call_service(
|
||||||
|
// &app,
|
||||||
|
// post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
107
src/pages/auth/register.rs
Normal file
107
src/pages/auth/register.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use actix_web::http::header::ContentType;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use tera::Context;
|
||||||
|
|
||||||
|
use crate::ctx::api::v1::auth::Register as RegisterPayload;
|
||||||
|
use crate::pages::errors::*;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::AppCtx;
|
||||||
|
|
||||||
|
pub use super::*;
|
||||||
|
|
||||||
|
pub const REGISTER: TemplateFile = TemplateFile::new("register", "pages/auth/register.html");
|
||||||
|
|
||||||
|
pub struct Register {
|
||||||
|
ctx: RefCell<Context>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CtxError for Register {
|
||||||
|
fn with_error(&self, e: &ReadableError) -> String {
|
||||||
|
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||||
|
self.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Register {
|
||||||
|
fn new(settings: &Settings, payload: Option<&RegisterPayload>) -> Self {
|
||||||
|
let ctx = RefCell::new(context(settings));
|
||||||
|
if let Some(payload) = payload {
|
||||||
|
ctx.borrow_mut().insert(PAYLOAD_KEY, payload);
|
||||||
|
}
|
||||||
|
Self { ctx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self) -> String {
|
||||||
|
TEMPLATES.render(REGISTER.name, &self.ctx.borrow()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn page(s: &Settings) -> String {
|
||||||
|
let p = Self::new(s, None);
|
||||||
|
p.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::get(path = "PAGES.auth.register")]
|
||||||
|
pub async fn get_register(ctx: AppCtx) -> impl Responder {
|
||||||
|
let login = Register::page(&ctx.settings);
|
||||||
|
let html = ContentType::html();
|
||||||
|
HttpResponse::Ok().content_type(html).body(login)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get_register);
|
||||||
|
cfg.service(register_submit);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(path = "PAGES.auth.register")]
|
||||||
|
pub async fn register_submit(
|
||||||
|
payload: web::Form<RegisterPayload>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> PageResult<impl Responder, Register> {
|
||||||
|
ctx.register(&payload)
|
||||||
|
.await
|
||||||
|
.map_err(|e| PageError::new(Register::new(&ctx.settings, Some(&payload)), e))?;
|
||||||
|
Ok(HttpResponse::Found()
|
||||||
|
.insert_header((http::header::LOCATION, PAGES.auth.login))
|
||||||
|
.finish())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Register;
|
||||||
|
use super::RegisterPayload;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::pages::errors::*;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_page_renders() {
|
||||||
|
let settings = Settings::new().unwrap();
|
||||||
|
Register::page(&settings);
|
||||||
|
let payload = RegisterPayload {
|
||||||
|
username: "foo".into(),
|
||||||
|
password: "foo".into(),
|
||||||
|
confirm_password: "foo".into(),
|
||||||
|
email: "foo".into(),
|
||||||
|
};
|
||||||
|
let page = Register::new(&settings, Some(&payload));
|
||||||
|
page.with_error(&ReadableError::new(&ServiceError::WrongPassword));
|
||||||
|
page.render();
|
||||||
|
}
|
||||||
|
}
|
144
src/pages/auth/test.rs
Normal file
144
src/pages/auth/test.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use actix_auth_middleware::GetLoginRoute;
|
||||||
|
|
||||||
|
use actix_web::http::header;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::ctx::api::v1::auth::{Login, Register};
|
||||||
|
use crate::ctx::ArcCtx;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::tests::*;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn postgrest_pages_auth_works() {
|
||||||
|
let (_, ctx) = get_ctx().await;
|
||||||
|
auth_works(ctx.clone()).await;
|
||||||
|
serverside_password_validation_works(ctx.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_works(ctx: ArcCtx) {
|
||||||
|
const NAME: &str = "testuserform";
|
||||||
|
const EMAIL: &str = "testuserform@foo.com";
|
||||||
|
const PASSWORD: &str = "longpassword";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
// 1. Register with email
|
||||||
|
let msg = Register {
|
||||||
|
username: NAME.into(),
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
confirm_password: PASSWORD.into(),
|
||||||
|
email: EMAIL.into(),
|
||||||
|
};
|
||||||
|
let resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&msg, PAGES.auth.register, FORM).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
let headers = resp.headers();
|
||||||
|
assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login);
|
||||||
|
|
||||||
|
// sign in
|
||||||
|
let msg = Login {
|
||||||
|
login: NAME.into(),
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
};
|
||||||
|
let resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
let headers = resp.headers();
|
||||||
|
assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.home);
|
||||||
|
let cookies = get_cookie!(resp);
|
||||||
|
|
||||||
|
// redirect after signin
|
||||||
|
let redirect = "/foo/bar/nonexistantuser";
|
||||||
|
let url = PAGES.get_login_route(Some(redirect));
|
||||||
|
let resp = test::call_service(&app, post_request!(&msg, &url, FORM).to_request()).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
let headers = resp.headers();
|
||||||
|
assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect);
|
||||||
|
|
||||||
|
// wrong password signin
|
||||||
|
let msg = Login {
|
||||||
|
login: NAME.into(),
|
||||||
|
password: NAME.into(),
|
||||||
|
};
|
||||||
|
let resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), ServiceError::WrongPassword.status_code());
|
||||||
|
|
||||||
|
// signout
|
||||||
|
|
||||||
|
println!("{}", PAGES.auth.logout);
|
||||||
|
let signout_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
test::TestRequest::get()
|
||||||
|
.uri(PAGES.auth.logout)
|
||||||
|
.cookie(cookies)
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(signout_resp.status(), StatusCode::FOUND);
|
||||||
|
let headers = signout_resp.headers();
|
||||||
|
assert_eq!(
|
||||||
|
headers.get(header::LOCATION).unwrap(),
|
||||||
|
&PAGES.get_login_route(None)
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serverside_password_validation_works(ctx: ArcCtx) {
|
||||||
|
const NAME: &str = "pagetestuser542";
|
||||||
|
const EMAIL: &str = "pagetestuser542@foo.com";
|
||||||
|
const PASSWORD: &str = "longpassword2";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
// checking to see if server-side password validation (password == password_config)
|
||||||
|
// works
|
||||||
|
let register_msg = Register {
|
||||||
|
username: NAME.into(),
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
confirm_password: NAME.into(),
|
||||||
|
email: EMAIL.into(),
|
||||||
|
};
|
||||||
|
let resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(®ister_msg, PAGES.auth.register, FORM).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
resp.status(),
|
||||||
|
ServiceError::PasswordsDontMatch.status_code()
|
||||||
|
);
|
||||||
|
}
|
105
src/pages/errors.rs
Normal file
105
src/pages/errors.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
error::ResponseError,
|
||||||
|
http::{header::ContentType, StatusCode},
|
||||||
|
HttpResponse, HttpResponseBuilder,
|
||||||
|
};
|
||||||
|
use derive_more::Display;
|
||||||
|
use derive_more::Error;
|
||||||
|
use serde::*;
|
||||||
|
|
||||||
|
use super::TemplateFile;
|
||||||
|
use crate::errors::ServiceError;
|
||||||
|
|
||||||
|
pub const ERROR_KEY: &str = "error";
|
||||||
|
|
||||||
|
pub const ERROR_TEMPLATE: TemplateFile = TemplateFile::new("error_comp", "components/error.html");
|
||||||
|
pub fn register_templates(t: &mut tera::Tera) {
|
||||||
|
ERROR_TEMPLATE.register(t).expect(ERROR_TEMPLATE.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render template with error context
|
||||||
|
pub trait CtxError {
|
||||||
|
fn with_error(&self, e: &ReadableError) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Display, Clone)]
|
||||||
|
#[display(fmt = "title: {} reason: {}", title, reason)]
|
||||||
|
pub struct ReadableError {
|
||||||
|
pub reason: String,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadableError {
|
||||||
|
pub fn new(e: &ServiceError) -> Self {
|
||||||
|
let reason = format!("{}", e);
|
||||||
|
let title = format!("{}", e.status_code());
|
||||||
|
|
||||||
|
Self { reason, title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Display)]
|
||||||
|
#[display(fmt = "{}", readable)]
|
||||||
|
pub struct PageError<T> {
|
||||||
|
#[error(not(source))]
|
||||||
|
template: T,
|
||||||
|
readable: ReadableError,
|
||||||
|
#[error(not(source))]
|
||||||
|
error: ServiceError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for PageError<T> {
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("PageError")
|
||||||
|
.field("readable", &self.readable)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: CtxError> PageError<T> {
|
||||||
|
/// create new instance of [PageError] from a template and an error
|
||||||
|
pub fn new(template: T, error: ServiceError) -> Self {
|
||||||
|
let readable = ReadableError::new(&error);
|
||||||
|
Self {
|
||||||
|
error,
|
||||||
|
template,
|
||||||
|
readable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
impl<T: CtxError> ResponseError for PageError<T> {
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponseBuilder::new(self.status_code())
|
||||||
|
.content_type(ContentType::html())
|
||||||
|
.body(self.template.with_error(&self.readable))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
self.error.status_code()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic result data structure
|
||||||
|
#[cfg(not(tarpaulin_include))]
|
||||||
|
pub type PageResult<V, T> = std::result::Result<V, PageError<T>>;
|
194
src/pages/mod.rs
Normal file
194
src/pages/mod.rs
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use actix_web::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
use serde::*;
|
||||||
|
use tera::*;
|
||||||
|
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use crate::static_assets::ASSETS;
|
||||||
|
use crate::{GIT_COMMIT_HASH, VERSION};
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::get_auth_middleware;
|
||||||
|
pub use routes::PAGES;
|
||||||
|
|
||||||
|
pub struct TemplateFile {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub path: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateFile {
|
||||||
|
pub const fn new(name: &'static str, path: &'static str) -> Self {
|
||||||
|
Self { name, path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
|
||||||
|
t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
|
||||||
|
use std::path::Path;
|
||||||
|
t.add_template_file(Path::new("templates/").join(self.path), Some(self.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PAYLOAD_KEY: &str = "payload";
|
||||||
|
|
||||||
|
pub const BASE: TemplateFile = TemplateFile::new("base", "components/base.html");
|
||||||
|
pub const FOOTER: TemplateFile = TemplateFile::new("footer", "components/footer.html");
|
||||||
|
pub const PUB_NAV: TemplateFile = TemplateFile::new("pub_nav", "components/nav/pub.html");
|
||||||
|
pub const AUTH_NAV: TemplateFile = TemplateFile::new("auth_nav", "components/nav/auth.html");
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref TEMPLATES: Tera = {
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
for t in [BASE, FOOTER, PUB_NAV, AUTH_NAV].iter() {
|
||||||
|
t.register(&mut tera).unwrap();
|
||||||
|
}
|
||||||
|
errors::register_templates(&mut tera);
|
||||||
|
tera.autoescape_on(vec![".html", ".sql"]);
|
||||||
|
auth::register_templates(&mut tera);
|
||||||
|
tera
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "templates/"]
|
||||||
|
pub struct Templates;
|
||||||
|
|
||||||
|
impl Templates {
|
||||||
|
pub fn get_template(t: &TemplateFile) -> Option<String> {
|
||||||
|
match Self::get(t.path) {
|
||||||
|
Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(s: &Settings) -> Context {
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
let footer = Footer::new(s);
|
||||||
|
ctx.insert("footer", &footer);
|
||||||
|
ctx.insert("page", &PAGES);
|
||||||
|
ctx.insert("assets", &*ASSETS);
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auth_ctx(username: Option<&str>, s: &Settings) -> Context {
|
||||||
|
// use routes::GistProfilePathComponent;
|
||||||
|
// let mut profile_link = None;
|
||||||
|
// if let Some(name) = username {
|
||||||
|
// profile_link = Some(
|
||||||
|
// PAGES
|
||||||
|
// .gist
|
||||||
|
// .get_profile_route(GistProfilePathComponent { username: name }),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
let footer = Footer::new(s);
|
||||||
|
ctx.insert("footer", &footer);
|
||||||
|
ctx.insert("page", &PAGES);
|
||||||
|
ctx.insert("assets", &*ASSETS);
|
||||||
|
// ctx.insert("loggedin_user", &profile_link);
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Footer<'a> {
|
||||||
|
version: &'a str,
|
||||||
|
support_email: &'a str,
|
||||||
|
source_code: &'a str,
|
||||||
|
git_hash: &'a str,
|
||||||
|
settings: &'a Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Footer<'a> {
|
||||||
|
pub fn new(settings: &'a Settings) -> Self {
|
||||||
|
Self {
|
||||||
|
version: VERSION,
|
||||||
|
source_code: &settings.source_code,
|
||||||
|
support_email: &settings.support_email,
|
||||||
|
git_hash: &GIT_COMMIT_HASH[..8],
|
||||||
|
settings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
auth::services(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn templates_work_basic() {
|
||||||
|
use super::*;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
let mut tera2 = Tera::default();
|
||||||
|
for t in [
|
||||||
|
BASE,
|
||||||
|
FOOTER,
|
||||||
|
PUB_NAV,
|
||||||
|
AUTH_NAV,
|
||||||
|
auth::AUTH_BASE,
|
||||||
|
auth::login::LOGIN,
|
||||||
|
auth::register::REGISTER,
|
||||||
|
errors::ERROR_TEMPLATE,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
{
|
||||||
|
t.register_from_file(&mut tera2).unwrap();
|
||||||
|
t.register(&mut tera).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod http_page_tests {
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
|
||||||
|
use crate::ctx::ArcCtx;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
use super::PAGES;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn postgrest_templates_work() {
|
||||||
|
let (_, ctx) = crate::tests::get_ctx().await;
|
||||||
|
templates_work(ctx).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn templates_work(ctx: ArcCtx) {
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
for file in [PAGES.auth.login, PAGES.auth.register].iter() {
|
||||||
|
let resp = get_request!(&app, file);
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
src/pages/routes.rs
Normal file
87
src/pages/routes.rs
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use actix_auth_middleware::{Authentication, GetLoginRoute};
|
||||||
|
use serde::*;
|
||||||
|
|
||||||
|
/// constant [Pages](Pages) instance
|
||||||
|
pub const PAGES: Pages = Pages::new();
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
/// Top-level routes data structure for V1 AP1
|
||||||
|
pub struct Pages {
|
||||||
|
/// Authentication routes
|
||||||
|
pub auth: Auth,
|
||||||
|
/// home page
|
||||||
|
pub home: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pages {
|
||||||
|
/// create new instance of Routes
|
||||||
|
const fn new() -> Pages {
|
||||||
|
let auth = Auth::new();
|
||||||
|
let home = auth.login;
|
||||||
|
Pages { auth, home }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
/// Authentication routes
|
||||||
|
pub struct Auth {
|
||||||
|
/// logout route
|
||||||
|
pub logout: &'static str,
|
||||||
|
/// login route
|
||||||
|
pub login: &'static str,
|
||||||
|
/// registration route
|
||||||
|
pub register: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Auth {
|
||||||
|
/// create new instance of Authentication route
|
||||||
|
pub const fn new() -> Auth {
|
||||||
|
let login = "/login";
|
||||||
|
let logout = "/logout";
|
||||||
|
let register = "/join";
|
||||||
|
Auth {
|
||||||
|
logout,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct GistProfilePathComponent<'a> {
|
||||||
|
pub username: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_auth_middleware() -> Authentication<Pages> {
|
||||||
|
Authentication::with_identity(PAGES)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetLoginRoute for Pages {
|
||||||
|
fn get_login_route(&self, src: Option<&str>) -> String {
|
||||||
|
if let Some(redirect_to) = src {
|
||||||
|
format!(
|
||||||
|
"{}?redirect_to={}",
|
||||||
|
self.auth.login,
|
||||||
|
urlencoding::encode(redirect_to)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.auth.login.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
templates/components/base.html
Normal file
15
templates/components/base.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="{{ assets.css }}" />
|
||||||
|
<title>LibrePages</title>
|
||||||
|
<title>{% block title %} {% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>{% block nav %} {% endblock %}</header>
|
||||||
|
{% block main %} {% endblock %}
|
||||||
|
{% include "footer" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
6
templates/components/error.html
Normal file
6
templates/components/error.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% if error %}
|
||||||
|
<div class="error_container">
|
||||||
|
<h3 class="error-title">ERROR: {{ error.title }}</h3>
|
||||||
|
<p class="error-message">{{ error.reason }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
37
templates/components/footer.html
Normal file
37
templates/components/footer.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<footer>
|
||||||
|
<div class="footer__container">
|
||||||
|
<div class="footer__column">
|
||||||
|
<span class="license__conatiner">
|
||||||
|
<a class="license__link" rel="noreferrer" href="/docs" target="_blank"
|
||||||
|
>Docs</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="footer__column">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="footer__link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
title="RSS"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="footer__column-divider">|</div>
|
||||||
|
<a href="mailto:{{ footer.support_email }}" class="footer__link"
|
||||||
|
>Support</a
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="footer__column-divider">|</div>
|
||||||
|
<a
|
||||||
|
class="footer__link"
|
||||||
|
href="{{ footer.source_code }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
title="Source Code"
|
||||||
|
>
|
||||||
|
v{{ footer.version }}-{{ footer.git_hash }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
28
templates/components/nav/auth.html
Normal file
28
templates/components/nav/auth.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<nav class="nav__container">
|
||||||
|
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||||
|
|
||||||
|
<div class="nav__header">
|
||||||
|
<a class="nav__logo-container" href="/">
|
||||||
|
<p class="nav__home-btn">LibrePages</p>
|
||||||
|
</a>
|
||||||
|
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||||
|
<span class="nav__hamburger-inner"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav__spacer"></div>
|
||||||
|
|
||||||
|
<div class="nav__link-group">
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a class="nav__link" rel="noreferrer" href="{{ page.gist.new }}">New Paste</a>
|
||||||
|
</div>
|
||||||
|
{% if loggedin_user %}
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a class="nav__link" rel="noreferrer" href="{{ loggedin_user }}">Profile</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a class="nav__link" rel="noreferrer" href="{{ page.auth.logout }}">Log out</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
18
templates/components/nav/base.html
Normal file
18
templates/components/nav/base.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<nav class="nav__container">
|
||||||
|
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||||
|
|
||||||
|
<div class="nav__header">
|
||||||
|
<a class="nav__logo-container" href="/">
|
||||||
|
<p class="nav__home-btn">LibrePages</p>
|
||||||
|
</a>
|
||||||
|
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||||
|
<span class="nav__hamburger-inner"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav__spacer"></div>
|
||||||
|
|
||||||
|
<div class="nav__link-group">
|
||||||
|
{% block nav_links %} {% endblock %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
26
templates/components/nav/pub.html
Normal file
26
templates/components/nav/pub.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<nav class="nav__container">
|
||||||
|
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||||
|
|
||||||
|
<div class="nav__header">
|
||||||
|
<a class="nav__logo-container" href="/">
|
||||||
|
<p class="nav__home-btn">LibrePages</p>
|
||||||
|
</a>
|
||||||
|
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||||
|
<span class="nav__hamburger-inner"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav__spacer"></div>
|
||||||
|
|
||||||
|
<div class="nav__link-group">
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a class="nav__link" rel="noreferrer" href="https://docs.librepages.org">Docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a class="nav__link" rel="noreferrer" href="{{ page.auth.login }}">Login</a>
|
||||||
|
</div>
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a class="nav__link" rel="noreferrer" href="{{ page.auth.register }}">Register</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
112
templates/components/nav/sass/main.scss
Normal file
112
templates/components/nav/sass/main.scss
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
@import "../../sass/_link";
|
||||||
|
|
||||||
|
header {
|
||||||
|
z-index: 5;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 5px;
|
||||||
|
border-bottom: 1px solid rgb(211, 211, 211);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__home-btn {
|
||||||
|
font-weight: bold;
|
||||||
|
// font-family: monospace, monospace;
|
||||||
|
margin: auto;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__spacer--small {
|
||||||
|
width: 100px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__spacer {
|
||||||
|
flex: 4;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__logo-container {
|
||||||
|
display: inline-flex;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__logo-container:hover {
|
||||||
|
@include a_hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__logo {
|
||||||
|
display: inline-flex;
|
||||||
|
margin: auto;
|
||||||
|
padding: 5px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin nav__link-group {
|
||||||
|
flex: 1.5;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
align-self: center;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-group {
|
||||||
|
@include nav__link-group;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-group--small {
|
||||||
|
@include nav__link-group;
|
||||||
|
flex: 0.5;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin nav__link-container {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-container {
|
||||||
|
@include nav__link-container;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-container--action {
|
||||||
|
@include nav__link-container;
|
||||||
|
background-color: green;
|
||||||
|
padding: 15px;
|
||||||
|
.nav__link {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: black !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link:hover {
|
||||||
|
@include a_hover;
|
||||||
|
}
|
141
templates/components/nav/sass/mobile.scss
Normal file
141
templates/components/nav/sass/mobile.scss
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
//@import '../_vars';
|
||||||
|
|
||||||
|
$hamburger-menu-animation: 0.4s ease-out;
|
||||||
|
$nav__hamburger-inner-height: 1.3px;
|
||||||
|
|
||||||
|
.nav__container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-group,
|
||||||
|
.nav__link-group--small {
|
||||||
|
position: sticky;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
// background-color: $light-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-container--action {
|
||||||
|
background-color: #fff;
|
||||||
|
.nav__link {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin nav__link-container {
|
||||||
|
border-bottom: 1px dashed rgba(55, 55, 55, 0.4);
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-container {
|
||||||
|
@include nav__link-container;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-container--action {
|
||||||
|
@include nav__link-container;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-container:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-menu {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__link-group {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__toggle:not(:checked) ~ .nav__link-group, .nav__link-group--small {
|
||||||
|
max-height: 0;
|
||||||
|
transition: max-height $hamburger-menu-animation;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__toggle:checked ~ .nav__link-group, .nav__toggle:checked ~ .nav__link-group--small {
|
||||||
|
max-height: 500px;
|
||||||
|
transition: max-height $hamburger-menu-animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__toggle:checked ~ .nav__header {
|
||||||
|
.nav__hamburger-inner::after {
|
||||||
|
width: 24px;
|
||||||
|
bottom: $nav__hamburger-inner-height;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: bottom 0.1s ease-out,
|
||||||
|
transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s,
|
||||||
|
width 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner::before {
|
||||||
|
top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: top 0.1s ease-out, opacity 0.1s ease-out 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner {
|
||||||
|
transform: rotate(225deg);
|
||||||
|
transition-delay: 0.12s;
|
||||||
|
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner::after {
|
||||||
|
bottom: -7px;
|
||||||
|
transition: bottom 0.1s ease-in 0.25s,
|
||||||
|
transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19),
|
||||||
|
width 0.1s ease-in 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner::after,
|
||||||
|
.nav__hamburger-inner::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner::before {
|
||||||
|
top: -7px;
|
||||||
|
transition: top 0.1s ease-in 0.25s, opacity 0.1s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner {
|
||||||
|
top: 50%;
|
||||||
|
margin: auto;
|
||||||
|
transition-duration: 0.22s;
|
||||||
|
transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-inner,
|
||||||
|
.nav__hamburger-inner::after,
|
||||||
|
.nav__hamburger-inner::before {
|
||||||
|
width: 24px;
|
||||||
|
height: $nav__hamburger-inner-height;
|
||||||
|
position: relative;
|
||||||
|
// background: $dark-black;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__hamburger-menu,
|
||||||
|
.nav__hamburger-inner {
|
||||||
|
display: block;
|
||||||
|
}
|
5
templates/components/sass/_fullscreen.scss
Normal file
5
templates/components/sass/_fullscreen.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@mixin fullscreen {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 500px;
|
||||||
|
max-height: 800px;
|
||||||
|
}
|
4
templates/components/sass/_link.scss
Normal file
4
templates/components/sass/_link.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
@mixin a_hover {
|
||||||
|
color: rgb(0, 86, 179);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
79
templates/components/sass/footer/main.scss
Normal file
79
templates/components/sass/footer/main.scss
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
@import "../_link";
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin footer__column-base {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
margin: auto 50px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column {
|
||||||
|
@include footer__column-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column--center {
|
||||||
|
@include footer__column-base;
|
||||||
|
margin: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column:last-child {
|
||||||
|
justify-content: flex-end;
|
||||||
|
a {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__link-container {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license__link {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license__link:hover {
|
||||||
|
@include a_hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column-divider,
|
||||||
|
.footer__column-divider--mobile-visible,
|
||||||
|
.footer__column-divider--mobile-only {
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column-divider--mobile-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__icon {
|
||||||
|
margin: auto 5px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
52
templates/components/sass/footer/mobile.scss
Normal file
52
templates/components/sass/footer/mobile.scss
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
$footer-font-size: 0.44rem;
|
||||||
|
|
||||||
|
footer {
|
||||||
|
font-size: $footer-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(3, 100%);
|
||||||
|
align-items: center;
|
||||||
|
margin: auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__link {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.license__conatiner,
|
||||||
|
.license__link {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin footer__column-base {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column:first-child {
|
||||||
|
grid-row-start: 3;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column:last-child {
|
||||||
|
grid-row-start: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column {
|
||||||
|
@include footer__column-base;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column--center {
|
||||||
|
@include footer__column-base;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__column-divider--mobile-only {
|
||||||
|
margin: 0 3px;
|
||||||
|
font-size: 9.9px;
|
||||||
|
}
|
70
templates/defaults.scss
Normal file
70
templates/defaults.scss
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
//font-family: "Inter UI", -apple-system, BlinkMacSystemFont, "Roboto",
|
||||||
|
// "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover, button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
color: rgb(0, 86, 179);
|
||||||
|
}
|
||||||
|
|
||||||
|
.base {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main__content-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
li,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 0.3em solid rgba(55, 55, 55, 0.4);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
//padding-left: 20px;
|
||||||
|
padding: 0 1em;
|
||||||
|
color: #707070;
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
li,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
5
templates/main.scss
Normal file
5
templates/main.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@import "defaults.scss";
|
||||||
|
@import "pages/auth/sass/main.scss";
|
||||||
|
@import "pages/auth/sass/form/main.scss";
|
||||||
|
@import "components/sass/footer/main.scss";
|
||||||
|
@import "components/nav/sass/main.scss";
|
3
templates/mobile.scss
Normal file
3
templates/mobile.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@import "components/sass/footer/mobile.scss";
|
||||||
|
@import "pages/auth/sass/mobile.scss";
|
||||||
|
@import "components/nav/sass/mobile.scss";
|
60
templates/pages/auth/base.html
Normal file
60
templates/pages/auth/base.html
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="{{ assets.css }}" />
|
||||||
|
<title>LibrePages</title>
|
||||||
|
</head>
|
||||||
|
<body class="auth__body">
|
||||||
|
<header>{% include "pub_nav" %}</header>
|
||||||
|
|
||||||
|
<main class="index-banner__container">
|
||||||
|
<section class="index-banner">
|
||||||
|
<div class="index-banner__content-container">
|
||||||
|
<h1 class="index-banner__title">
|
||||||
|
Easiest way to deploy websites
|
||||||
|
</h1>
|
||||||
|
<p class="index-banner__tagline">
|
||||||
|
JAMstack platform with focus on privacy and speed
|
||||||
|
</p>
|
||||||
|
<ul class="index-banner__features-list">
|
||||||
|
<li class="index-banner__features">
|
||||||
|
<b>Seamless Git Integration</b> making migration
|
||||||
|
easy
|
||||||
|
</li>
|
||||||
|
<li class="index-banner__features">
|
||||||
|
<b>Pull Request Previews</b> to verify changes
|
||||||
|
before deployment
|
||||||
|
</li>
|
||||||
|
<li class="index-banner__features">
|
||||||
|
<b>Server-less form submissions</b> to collect data
|
||||||
|
from visitors
|
||||||
|
</li>
|
||||||
|
<li class="index-banner__features">
|
||||||
|
<b>Global CDN</b> for high-speed access from across
|
||||||
|
the world
|
||||||
|
</li>
|
||||||
|
<li class="index-banner__features">
|
||||||
|
<b>
|
||||||
|
100%
|
||||||
|
<a
|
||||||
|
href="https://www.gnu.org/philosophy/free-sw.html"
|
||||||
|
>Free Software</a
|
||||||
|
> </b
|
||||||
|
>: deploy your own instance
|
||||||
|
</li>
|
||||||
|
<li class="index-banner__features">
|
||||||
|
25% of the income dedicated to sustain Free Software
|
||||||
|
dependencies
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="index-banner__logo-container">
|
||||||
|
{% block login %} {% endblock %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% include "footer" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
44
templates/pages/auth/login.html
Normal file
44
templates/pages/auth/login.html
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{% extends 'authbase' %}
|
||||||
|
{% block login %}
|
||||||
|
<h2>Sign In</h2>
|
||||||
|
<form action="{{ page.auth.login }}" method="POST" class="auth-form" accept-charset="utf-8">
|
||||||
|
{% include "error_comp" %}
|
||||||
|
<label class="auth-form__label" for="login">
|
||||||
|
Username or Email
|
||||||
|
<input
|
||||||
|
class="auth-form__input"
|
||||||
|
name="login"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
id="login"
|
||||||
|
type="text"
|
||||||
|
{% if payload.username %}
|
||||||
|
value={{ payload.username }}
|
||||||
|
{% endif %}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="auth-form__label" for="password">
|
||||||
|
Password
|
||||||
|
<input
|
||||||
|
class="auth-form__input"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
{% if payload.password %}
|
||||||
|
value={{ payload.password }}
|
||||||
|
{% endif %}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="auth-form__action-container">
|
||||||
|
<a href="">Forgot password?</a>
|
||||||
|
<button class="auth-form__submit" type="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="auth-form__alt-action">
|
||||||
|
New to LibrePages?
|
||||||
|
<a href="{{ page.auth.register }}">Create an account </a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
73
templates/pages/auth/register.html
Normal file
73
templates/pages/auth/register.html
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{% extends 'authbase' %}
|
||||||
|
{% block title_name %}Sign Up {% endblock %}
|
||||||
|
{% block login %}
|
||||||
|
<h2>Sign Up</h2>
|
||||||
|
<form action="{{ page.auth.register }}" method="POST" class="auth-form" accept-charset="utf-8">
|
||||||
|
{% include "error_comp" %}
|
||||||
|
<label class="auth-form__label" for="username">
|
||||||
|
Username
|
||||||
|
<input
|
||||||
|
class="auth-form__input"
|
||||||
|
autofocus
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
{% if payload.username %}
|
||||||
|
value={{ payload.username }}
|
||||||
|
{% endif %}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="auth-form__label" for="email">
|
||||||
|
Email
|
||||||
|
<input
|
||||||
|
class="auth-form__input"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
{% if payload.email %}
|
||||||
|
value={{ payload.email }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="auth-form__label" for="password">
|
||||||
|
password
|
||||||
|
<input
|
||||||
|
class="auth-form__input"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
{% if payload.password %}
|
||||||
|
value={{ payload.password }}
|
||||||
|
{% endif %}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="auth-form__label" for="confirm_password">
|
||||||
|
Re-enter Password
|
||||||
|
<input
|
||||||
|
class="auth-form__input"
|
||||||
|
name="confirm_password"
|
||||||
|
required
|
||||||
|
id="confirm_password"
|
||||||
|
type="password"
|
||||||
|
{% if payload.confirm_password %}
|
||||||
|
value={{ payload.confirm_password }}
|
||||||
|
{% endif %}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="auth-form__action-container">
|
||||||
|
<a href="/forgot-password">Forgot password?</a>
|
||||||
|
<button class="auth-form__submit" type="submit">Sign Up</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="auth-form__alt-action">
|
||||||
|
Already have an account?
|
||||||
|
<a href="{{ page.auth.login }}"> Login </a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
29
templates/pages/auth/sass/form/main.scss
Normal file
29
templates/pages/auth/sass/form/main.scss
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form__input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form__submit {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
background-color: green;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form__submit:hover {
|
||||||
|
background-color: green;
|
||||||
|
}
|
152
templates/pages/auth/sass/main.scss
Normal file
152
templates/pages/auth/sass/main.scss
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
@import "../../../components/sass/fullscreen";
|
||||||
|
|
||||||
|
.auth__body {
|
||||||
|
display: flex;
|
||||||
|
@include fullscreen;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
$heading-letter-spacing: 20px;
|
||||||
|
.index-banner__container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
//background-color: #d1875a;
|
||||||
|
// background-color: #3c3c3c;
|
||||||
|
// background-color: #58181f;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner {
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__content-container {
|
||||||
|
// height: 300px;
|
||||||
|
li {
|
||||||
|
// color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.index-banner__logo-container {
|
||||||
|
margin: auto;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__title {
|
||||||
|
margin: auto;
|
||||||
|
font-style: none;
|
||||||
|
//color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__tagline {
|
||||||
|
margin: auto;
|
||||||
|
// color: #fff;
|
||||||
|
// font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__title-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__logo {
|
||||||
|
width: 120px;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__main-action-btn {
|
||||||
|
display: block;
|
||||||
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
margin: 20px 0;
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__main-action-link {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__main-action-btn:hover {
|
||||||
|
// background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__features-list {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__features {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home__features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home__features-title {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page-content-width: 80%;
|
||||||
|
.index__group-content {
|
||||||
|
.page__container {
|
||||||
|
width: $page-content-width;
|
||||||
|
@include fullscreen;
|
||||||
|
height: 90vh !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__container {
|
||||||
|
background: #1f5818;
|
||||||
|
width: 100%;
|
||||||
|
padding: 60px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__margin-container {
|
||||||
|
display: flex;
|
||||||
|
width: $page-content-width;
|
||||||
|
margin: auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__prompt {
|
||||||
|
color: white;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__button {
|
||||||
|
display: block;
|
||||||
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
margin: 20px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__button:hover {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call_link {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call_link:hover {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
36
templates/pages/auth/sass/mobile.scss
Normal file
36
templates/pages/auth/sass/mobile.scss
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
.home__container {
|
||||||
|
max-height: 100vh;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home__name {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index__group-content {
|
||||||
|
.page__container {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-banner__logo-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__margin-container {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-call__prompt {
|
||||||
|
text-align: center;
|
||||||
|
}
|
Loading…
Reference in a new issue