From 0bf5a3567365ab0e11c250d66a475fabfcda6883 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Fri, 16 Sep 2022 17:41:39 +0530 Subject: [PATCH] feat: login and join HTML pages --- src/pages/auth/login.rs | 119 ++++++++++++ src/pages/auth/mod.rs | 162 ++++++++++++++++ src/pages/auth/register.rs | 107 ++++++++++ src/pages/auth/test.rs | 144 ++++++++++++++ src/pages/errors.rs | 105 ++++++++++ src/pages/mod.rs | 194 +++++++++++++++++++ src/pages/routes.rs | 87 +++++++++ templates/components/base.html | 15 ++ templates/components/error.html | 6 + templates/components/footer.html | 37 ++++ templates/components/nav/auth.html | 28 +++ templates/components/nav/base.html | 18 ++ templates/components/nav/pub.html | 26 +++ templates/components/nav/sass/main.scss | 112 +++++++++++ templates/components/nav/sass/mobile.scss | 141 ++++++++++++++ templates/components/sass/_fullscreen.scss | 5 + templates/components/sass/_link.scss | 4 + templates/components/sass/footer/main.scss | 79 ++++++++ templates/components/sass/footer/mobile.scss | 52 +++++ templates/defaults.scss | 70 +++++++ templates/main.scss | 5 + templates/mobile.scss | 3 + templates/pages/auth/base.html | 60 ++++++ templates/pages/auth/login.html | 44 +++++ templates/pages/auth/register.html | 73 +++++++ templates/pages/auth/sass/form/main.scss | 29 +++ templates/pages/auth/sass/main.scss | 152 +++++++++++++++ templates/pages/auth/sass/mobile.scss | 36 ++++ 28 files changed, 1913 insertions(+) create mode 100644 src/pages/auth/login.rs create mode 100644 src/pages/auth/mod.rs create mode 100644 src/pages/auth/register.rs create mode 100644 src/pages/auth/test.rs create mode 100644 src/pages/errors.rs create mode 100644 src/pages/mod.rs create mode 100644 src/pages/routes.rs create mode 100644 templates/components/base.html create mode 100644 templates/components/error.html create mode 100644 templates/components/footer.html create mode 100644 templates/components/nav/auth.html create mode 100644 templates/components/nav/base.html create mode 100644 templates/components/nav/pub.html create mode 100644 templates/components/nav/sass/main.scss create mode 100644 templates/components/nav/sass/mobile.scss create mode 100644 templates/components/sass/_fullscreen.scss create mode 100644 templates/components/sass/_link.scss create mode 100644 templates/components/sass/footer/main.scss create mode 100644 templates/components/sass/footer/mobile.scss create mode 100644 templates/defaults.scss create mode 100644 templates/main.scss create mode 100644 templates/mobile.scss create mode 100644 templates/pages/auth/base.html create mode 100644 templates/pages/auth/login.html create mode 100644 templates/pages/auth/register.html create mode 100644 templates/pages/auth/sass/form/main.scss create mode 100644 templates/pages/auth/sass/main.scss create mode 100644 templates/pages/auth/sass/mobile.scss diff --git a/src/pages/auth/login.rs b/src/pages/auth/login.rs new file mode 100644 index 0000000..64bdb8f --- /dev/null +++ b/src/pages/auth/login.rs @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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, +} + +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, + query: web::Query, + ctx: AppCtx, +) -> PageResult { + 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(); + } +} diff --git a/src/pages/auth/mod.rs b/src/pages/auth/mod.rs new file mode 100644 index 0000000..1aed55c --- /dev/null +++ b/src/pages/auth/mod.rs @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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, +// data: AppData, +//) -> PageResult { +// 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); +// } +//} +// diff --git a/src/pages/auth/register.rs b/src/pages/auth/register.rs new file mode 100644 index 0000000..9ecef00 --- /dev/null +++ b/src/pages/auth/register.rs @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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, +} + +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, + ctx: AppCtx, +) -> PageResult { + 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(); + } +} diff --git a/src/pages/auth/test.rs b/src/pages/auth/test.rs new file mode 100644 index 0000000..e03621d --- /dev/null +++ b/src/pages/auth/test.rs @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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() + ); +} diff --git a/src/pages/errors.rs b/src/pages/errors.rs new file mode 100644 index 0000000..16c4862 --- /dev/null +++ b/src/pages/errors.rs @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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 { + #[error(not(source))] + template: T, + readable: ReadableError, + #[error(not(source))] + error: ServiceError, +} + +impl fmt::Debug for PageError { + #[cfg(not(tarpaulin_include))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PageError") + .field("readable", &self.readable) + .finish() + } +} + +impl PageError { + /// 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 ResponseError for PageError { + 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 = std::result::Result>; diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000..f1c14e1 --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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 { + 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); + } + } +} diff --git a/src/pages/routes.rs b/src/pages/routes.rs new file mode 100644 index 0000000..73b2f96 --- /dev/null +++ b/src/pages/routes.rs @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * 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 . + */ +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 { + 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() + } + } +} diff --git a/templates/components/base.html b/templates/components/base.html new file mode 100644 index 0000000..dcd84a5 --- /dev/null +++ b/templates/components/base.html @@ -0,0 +1,15 @@ + + + + + + + LibrePages + {% block title %} {% endblock %} + + +
{% block nav %} {% endblock %}
+ {% block main %} {% endblock %} + {% include "footer" %} + + diff --git a/templates/components/error.html b/templates/components/error.html new file mode 100644 index 0000000..bb36399 --- /dev/null +++ b/templates/components/error.html @@ -0,0 +1,6 @@ +{% if error %} +
+

ERROR: {{ error.title }}

+

{{ error.reason }}

+
+{% endif %} diff --git a/templates/components/footer.html b/templates/components/footer.html new file mode 100644 index 0000000..286c6d6 --- /dev/null +++ b/templates/components/footer.html @@ -0,0 +1,37 @@ + diff --git a/templates/components/nav/auth.html b/templates/components/nav/auth.html new file mode 100644 index 0000000..cdbdc8a --- /dev/null +++ b/templates/components/nav/auth.html @@ -0,0 +1,28 @@ + diff --git a/templates/components/nav/base.html b/templates/components/nav/base.html new file mode 100644 index 0000000..a0e066f --- /dev/null +++ b/templates/components/nav/base.html @@ -0,0 +1,18 @@ + diff --git a/templates/components/nav/pub.html b/templates/components/nav/pub.html new file mode 100644 index 0000000..6fada76 --- /dev/null +++ b/templates/components/nav/pub.html @@ -0,0 +1,26 @@ + diff --git a/templates/components/nav/sass/main.scss b/templates/components/nav/sass/main.scss new file mode 100644 index 0000000..baef4b3 --- /dev/null +++ b/templates/components/nav/sass/main.scss @@ -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; +} diff --git a/templates/components/nav/sass/mobile.scss b/templates/components/nav/sass/mobile.scss new file mode 100644 index 0000000..2f9040c --- /dev/null +++ b/templates/components/nav/sass/mobile.scss @@ -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; +} diff --git a/templates/components/sass/_fullscreen.scss b/templates/components/sass/_fullscreen.scss new file mode 100644 index 0000000..3f6c693 --- /dev/null +++ b/templates/components/sass/_fullscreen.scss @@ -0,0 +1,5 @@ +@mixin fullscreen { + height: 100vh; + min-height: 500px; + max-height: 800px; +} diff --git a/templates/components/sass/_link.scss b/templates/components/sass/_link.scss new file mode 100644 index 0000000..e957dba --- /dev/null +++ b/templates/components/sass/_link.scss @@ -0,0 +1,4 @@ +@mixin a_hover { + color: rgb(0, 86, 179); + text-decoration: underline; +} diff --git a/templates/components/sass/footer/main.scss b/templates/components/sass/footer/main.scss new file mode 100644 index 0000000..41de9d3 --- /dev/null +++ b/templates/components/sass/footer/main.scss @@ -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; +} diff --git a/templates/components/sass/footer/mobile.scss b/templates/components/sass/footer/mobile.scss new file mode 100644 index 0000000..f6e0c1f --- /dev/null +++ b/templates/components/sass/footer/mobile.scss @@ -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; +} diff --git a/templates/defaults.scss b/templates/defaults.scss new file mode 100644 index 0000000..4e38a60 --- /dev/null +++ b/templates/defaults.scss @@ -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; + } +} diff --git a/templates/main.scss b/templates/main.scss new file mode 100644 index 0000000..ea4c173 --- /dev/null +++ b/templates/main.scss @@ -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"; diff --git a/templates/mobile.scss b/templates/mobile.scss new file mode 100644 index 0000000..ed8ebf1 --- /dev/null +++ b/templates/mobile.scss @@ -0,0 +1,3 @@ +@import "components/sass/footer/mobile.scss"; +@import "pages/auth/sass/mobile.scss"; +@import "components/nav/sass/mobile.scss"; diff --git a/templates/pages/auth/base.html b/templates/pages/auth/base.html new file mode 100644 index 0000000..9b5d93c --- /dev/null +++ b/templates/pages/auth/base.html @@ -0,0 +1,60 @@ + + + + + + + LibrePages + + +
{% include "pub_nav" %}
+ +
+
+
+

+ Easiest way to deploy websites +

+

+ JAMstack platform with focus on privacy and speed +

+
    +
  • + Seamless Git Integration making migration + easy +
  • +
  • + Pull Request Previews to verify changes + before deployment +
  • +
  • + Server-less form submissions to collect data + from visitors +
  • +
  • + Global CDN for high-speed access from across + the world +
  • +
  • + + 100% + Free Software : deploy your own instance +
  • +
  • + 25% of the income dedicated to sustain Free Software + dependencies +
  • +
+
+
+
+ {% block login %} {% endblock %} +
+
+ {% include "footer" %} + + diff --git a/templates/pages/auth/login.html b/templates/pages/auth/login.html new file mode 100644 index 0000000..c069279 --- /dev/null +++ b/templates/pages/auth/login.html @@ -0,0 +1,44 @@ +{% extends 'authbase' %} +{% block login %} +

Sign In

+
+ {% include "error_comp" %} + + + +
+ Forgot password? + +
+
+ +

+ New to LibrePages? + Create an account +

+{% endblock %} diff --git a/templates/pages/auth/register.html b/templates/pages/auth/register.html new file mode 100644 index 0000000..836e7d6 --- /dev/null +++ b/templates/pages/auth/register.html @@ -0,0 +1,73 @@ +{% extends 'authbase' %} +{% block title_name %}Sign Up {% endblock %} +{% block login %} +

Sign Up

+
+ {% include "error_comp" %} + + + + + + + +
+ Forgot password? + +
+
+ +

+ Already have an account? + Login +

+{% endblock %} diff --git a/templates/pages/auth/sass/form/main.scss b/templates/pages/auth/sass/form/main.scss new file mode 100644 index 0000000..a16f7e5 --- /dev/null +++ b/templates/pages/auth/sass/form/main.scss @@ -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; +} diff --git a/templates/pages/auth/sass/main.scss b/templates/pages/auth/sass/main.scss new file mode 100644 index 0000000..00fe4ed --- /dev/null +++ b/templates/pages/auth/sass/main.scss @@ -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; +} diff --git a/templates/pages/auth/sass/mobile.scss b/templates/pages/auth/sass/mobile.scss new file mode 100644 index 0000000..4d8e696 --- /dev/null +++ b/templates/pages/auth/sass/mobile.scss @@ -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; +}