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