Move server/ to / and start i18ning

Currently i18n'd
- login page
- register page
- cookie page
- delete account confirmation page
- 404 page
- 500 page
- part of account settings
This commit is contained in:
asonix 2021-01-28 20:25:31 -06:00
parent d63327170d
commit e0858b7b3d
101 changed files with 644 additions and 286 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
/target
Cargo.lock
/server/sled
/sled
/docker/federation-test/volumes
/docker/federation-test/hyaenidae-server

View File

@ -1,9 +1,60 @@
[package]
name = "hyaenidae"
version = "0.1.0"
authors = ["asonix <asonix@asonix.dog>"]
edition = "2018"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
activitystreams = "0.7.0-alpha.8"
actix-session = "0.4.0"
actix-rt = "1.1.1"
actix-web = { version = "3.3.2", features = ["rustls", "compress"] }
actix-webfinger = "0.3.0"
anyhow = "1.0.35"
background-jobs = "0.8.0"
base64 = "0.13.0"
chrono = { version = "0.4.19", features = ["serde"] }
event-listener = "2.5.1"
fluent = "0.14.3"
fluent-syntax = "0.10.2"
futures = "0.3.11"
html-minifier = "3.0.8"
http-signature-normalization-actix = { version = "0.4.0", default-features = false, features = ["sha-2"] }
hyaenidae-accounts = { version = "0.1.0", path = "./accounts" }
hyaenidae-profiles = { version = "0.1.0", path = "./profiles" }
hyaenidae-toolkit = { version = "0.1.0", path = "./toolkit" }
i18n-embed-fl = "0.3.1"
i18n-embed = { version = "0.10.2", features = ["fluent-system"] }
log = "0.4"
mime = "0.3.16"
once_cell = "1.5.2"
rand = "0.7"
rsa = "0.3.0"
rsa-magic-public-key = "0.2.1"
rsa-pem = "0.2.0"
rust-embed = "5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.9"
sled = { version = "0.34.6", features = ["compression"] }
structopt = "0.3"
thiserror = "1.0"
tracing-subscriber = { version = "0.2.15", features = ["env-filter"] }
unic-langid = "0.9.0"
url = { version = "2.2.0", features = ["serde"] }
uuid = { version = "0.8.1", features = ["serde", "v4"] }
[build-dependencies]
ructe = { version = "0.13.0", features = ["mime03", "sass"] }
[workspace]
members = [
"accounts",
"content",
"profiles",
"server",
"toolkit",
"toolkit-examples"
]

6
i18n.toml Normal file
View File

@ -0,0 +1,6 @@
fallback_language = "en-US"
subcrates = ["hyaenidae-i18n"]
[fluent]
assets_dir = "translations"

View File

@ -1,45 +0,0 @@
[package]
name = "hyaenidae-server"
version = "0.1.0"
authors = ["asonix <asonix@asonix.dog>"]
edition = "2018"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
activitystreams = "0.7.0-alpha.8"
actix-session = "0.4.0"
actix-rt = "1.1.1"
actix-web = { version = "3.3.2", features = ["rustls", "compress"] }
actix-webfinger = "0.3.0"
anyhow = "1.0.35"
background-jobs = "0.8.0"
base64 = "0.13.0"
chrono = { version = "0.4.19", features = ["serde"] }
event-listener = "2.5.1"
futures = "0.3.11"
html-minifier = "3.0.8"
http-signature-normalization-actix = { version = "0.4.0", default-features = false, features = ["sha-2"] }
hyaenidae-accounts = { version = "0.1.0", path = "../accounts" }
hyaenidae-profiles = { version = "0.1.0", path = "../profiles" }
hyaenidae-toolkit = { version = "0.1.0", path = "../toolkit" }
log = "0.4"
mime = "0.3.16"
once_cell = "1.5.2"
rand = "0.7"
rsa = "0.3.0"
rsa-magic-public-key = "0.2.1"
rsa-pem = "0.2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.9"
sled = { version = "0.34.6", features = ["compression"] }
structopt = "0.3"
thiserror = "1.0"
tracing-subscriber = { version = "0.2.15", features = ["env-filter"] }
url = { version = "2.2.0", features = ["serde"] }
uuid = { version = "0.8.1", features = ["serde", "v4"] }
[build-dependencies]
ructe = { version = "0.13.0", features = ["mime03", "sass"] }

View File

@ -1,19 +0,0 @@
@use hyaenidae_accounts::CookiesState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title}, Button, Card};
@(card_config: &Card, state: &CookiesState)
@:card(card_config, {
@:card_title({ Accept Cookies })
@:card_body({
<p>In order to continue, you must accept the use of cookies</p>
})
@:card_body({
<form method="POST" action="@state.cookies_path()">
@:button_group(&[
Button::primary("Accept Cookies"),
Button::primary_outline("Cancel").href(&state.home_path()),
])
</form>
})
})

View File

@ -1,20 +0,0 @@
@use crate::templates::accounts::inputs::password_input;
@use hyaenidae_accounts::DeleteUserState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title}, Button, Card};
@(card_config: &Card, state: &DeleteUserState)
@:card(card_config, {
<form method="POST" action="@state.delete_user_path()">
@:card_title({ Delete Account })
@:card_body({
@:password_input("password", "Password", state.password(), state.password_error(), state.dark)
})
@:card_body({
@:button_group(&[
Button::primary("Delete Account"),
Button::primary_outline("Cancel").href(&state.accounts_path()),
])
})
</form>
})

View File

@ -1,22 +0,0 @@
@use crate::templates::accounts::inputs::{text_input, password_input};
@use hyaenidae_accounts::LoginState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title, link}, Button, Card, Link};
@(card_config: &Card, state: &LoginState)
@:card(card_config, {
<form method="POST" action="@state.login_path()">
@:card_title({ Login })
@:card_body({
@:text_input("username", "Username", state.username(), state.username_error(), state.dark)
@:password_input("password", "Password", state.password(), None, state.dark)
@:link(&Link::current_tab(&state.register_path()), { I do not have an account })
})
@:card_body({
@:button_group(&[
Button::primary("Login"),
Button::primary_outline("Cancel").href(&state.home_path()),
])
})
</form>
})

View File

@ -1,23 +0,0 @@
@use crate::templates::accounts::inputs::{text_input, password_input};
@use hyaenidae_accounts::RegisterState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title, link}, Button, Card, Link};
@(card_config: &Card, state: &RegisterState)
@:card(card_config, {
<form method="POST" action="@state.register_path()">
@:card_title({ Register })
@:card_body({
@:text_input("username", "Username", state.username(), state.username_error(), state.dark)
@:password_input("password", "Password", state.password(), None, state.dark)
@:password_input("password_confirmation", "Password Confirmation", state.confirmation(), state.confirmation_error(), state.dark)
@:link(&Link::current_tab(&state.login_path()), { I already have an account })
})
@:card_body({
@:button_group(&[
Button::primary("Register"),
Button::primary_outline("Cancel").href(&state.home_path()),
])
})
</form>
})

View File

@ -1,14 +0,0 @@
@use crate::templates::layouts::main;
@use hyaenidae_toolkit::{templates::{card, card_body, card_title, link}, Card, Link};
@(error: String, dark: bool)
@:main("Error", "There was an error processing your request", dark, {}, {
@:card(&Card::full_width().dark(dark), {
@:card_title({ There was an error processing your request })
@:card_body({ @error })
@:card_body({
@:link(&Link::current_tab("/"), { Return Home })
})
})
})

View File

@ -1,13 +0,0 @@
@use crate::templates::layouts::main;
@use hyaenidae_toolkit::{templates::{card, card_body, card_title, link}, Card, Link};
@(dark: bool)
@:main("404", "Not Found", dark, {}, {
@:card(&Card::full_width().dark(dark), {
@:card_title({ We couldn't find that })
@:card_body({
@:link(&Link::current_tab("/"), { Return Home })
})
})
})

View File

@ -1,11 +0,0 @@
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::cookies;
@use hyaenidae_accounts::CookiesState;
@use hyaenidae_toolkit::Card;
@(cookie_state: &CookiesState, nav_state: &NavState)
@:home("Accept Cookies", "Review the cookie policy", nav_state, {}, {
@:cookies(&Card::full_width().dark(nav_state.dark()), cookie_state)
})

View File

@ -1,13 +0,0 @@
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::delete_user;
@use hyaenidae_accounts::DeleteUserState;
@use hyaenidae_toolkit::Card;
@(state: &DeleteUserState, nav_state: &NavState)
@:home("Delete Account", "Are you sure you want to delete your account?", nav_state, {
@:button_js()
}, {
@:delete_user(&Card::full_width().dark(nav_state.dark()), state)
})

View File

@ -1,13 +0,0 @@
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::login;
@use hyaenidae_accounts::LoginState;
@use hyaenidae_toolkit::Card;
@(login_state: &LoginState, nav_state: &NavState)
@:home("Login", "Log into Hyaenidae", nav_state, {
@:button_js()
}, {
@:login(&Card::full_width().dark(nav_state.dark()), login_state)
})

View File

@ -1,11 +0,0 @@
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::register;
@use hyaenidae_accounts::RegisterState;
@use hyaenidae_toolkit::Card;
@(register_state: &RegisterState, nav_state: &NavState)
@:home("Register", "Register for Hyaenidae", nav_state, { @:button_js() }, {
@:register(&Card::full_width().dark(nav_state.dark()), register_state)
})

View File

@ -1,4 +1,4 @@
use crate::{nav::NavState, rendered, Error};
use crate::{nav::NavState, rendered, ActixLoader, Error};
use actix_web::{http::header::LOCATION, web, HttpResponse, Scope};
use hyaenidae_accounts::{
CookiesArgs, CookiesPageArgs, DeleteUserArgs, DeleteUserPageArgs, LoginArgs, LoginPageArgs,
@ -96,7 +96,11 @@ pub(crate) fn scope() -> Scope {
)
}
async fn cookies_page(args: CookiesPageArgs, nav_state: NavState) -> Result<HttpResponse, Error> {
async fn cookies_page(
loader: ActixLoader,
args: CookiesPageArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let mut cookie_state = match hyaenidae_accounts::cookies_page(args) {
Ok(state) => state,
Err(res) => return Ok(res),
@ -105,7 +109,7 @@ async fn cookies_page(args: CookiesPageArgs, nav_state: NavState) -> Result<Http
cookie_state.dark(nav_state.dark());
rendered(HttpResponse::Ok(), |cursor| {
crate::templates::session::cookies(cursor, &cookie_state, &nav_state)
crate::templates::session::cookies(cursor, &loader, &cookie_state, &nav_state)
})
}
@ -113,7 +117,11 @@ async fn cookies(args: CookiesArgs) -> Result<HttpResponse, Error> {
Ok(hyaenidae_accounts::cookies(args).await?)
}
async fn login_page(args: LoginPageArgs, nav_state: NavState) -> Result<HttpResponse, Error> {
async fn login_page(
loader: ActixLoader,
args: LoginPageArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let mut login_state = match hyaenidae_accounts::login_page(args) {
Ok(state) => state,
Err(res) => return Ok(res),
@ -122,11 +130,15 @@ async fn login_page(args: LoginPageArgs, nav_state: NavState) -> Result<HttpResp
login_state.dark(nav_state.dark());
rendered(HttpResponse::Ok(), |cursor| {
crate::templates::session::login(cursor, &login_state, &nav_state)
crate::templates::session::login(cursor, &loader, &login_state, &nav_state)
})
}
async fn login(args: LoginArgs, nav_state: NavState) -> Result<HttpResponse, Error> {
async fn login(
loader: ActixLoader,
args: LoginArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let mut login_state = match hyaenidae_accounts::login(args).await? {
Ok(state) => state,
Err(res) => return Ok(res),
@ -135,11 +147,15 @@ async fn login(args: LoginArgs, nav_state: NavState) -> Result<HttpResponse, Err
login_state.dark(nav_state.dark());
rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::session::login(cursor, &login_state, &nav_state)
crate::templates::session::login(cursor, &loader, &login_state, &nav_state)
})
}
async fn register_page(args: RegisterPageArgs, nav_state: NavState) -> Result<HttpResponse, Error> {
async fn register_page(
loader: ActixLoader,
args: RegisterPageArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let mut register_state = match hyaenidae_accounts::register_page(args) {
Ok(state) => state,
Err(res) => return Ok(res),
@ -148,11 +164,15 @@ async fn register_page(args: RegisterPageArgs, nav_state: NavState) -> Result<Ht
register_state.dark(nav_state.dark());
rendered(HttpResponse::Ok(), |cursor| {
crate::templates::session::register(cursor, &register_state, &nav_state)
crate::templates::session::register(cursor, &loader, &register_state, &nav_state)
})
}
async fn register(args: RegisterArgs, nav_state: NavState) -> Result<HttpResponse, Error> {
async fn register(
loader: ActixLoader,
args: RegisterArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let mut register_state = match hyaenidae_accounts::register(args).await? {
Ok(state) => state,
Err(res) => return Ok(res),
@ -161,7 +181,7 @@ async fn register(args: RegisterArgs, nav_state: NavState) -> Result<HttpRespons
register_state.dark(nav_state.dark());
rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::session::register(cursor, &register_state, &nav_state)
crate::templates::session::register(cursor, &loader, &register_state, &nav_state)
})
}
@ -170,6 +190,7 @@ async fn logout(args: LogoutArgs) -> HttpResponse {
}
async fn account_page(
loader: ActixLoader,
uname_args: UpdateUsernamePageArgs,
pass_args: UpdatePasswordPageArgs,
user: User,
@ -182,7 +203,14 @@ async fn account_page(
pass_state.dark(nav_state.dark());
rendered(HttpResponse::Ok(), |cursor| {
crate::templates::session::account(cursor, &user, &uname_state, &pass_state, &nav_state)
crate::templates::session::account(
cursor,
&loader,
&user,
&uname_state,
&pass_state,
&nav_state,
)
})
}
@ -193,6 +221,7 @@ async fn to_account() -> HttpResponse {
}
async fn update_username(
loader: ActixLoader,
uname_args: UpdateUsernameArgs,
pass_args: UpdatePasswordPageArgs,
user: User,
@ -208,11 +237,19 @@ async fn update_username(
pass_state.dark(nav_state.dark());
rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::session::account(cursor, &user, &uname_state, &pass_state, &nav_state)
crate::templates::session::account(
cursor,
&loader,
&user,
&uname_state,
&pass_state,
&nav_state,
)
})
}
async fn update_password(
loader: ActixLoader,
uname_args: UpdateUsernamePageArgs,
pass_args: UpdatePasswordArgs,
user: User,
@ -228,11 +265,19 @@ async fn update_password(
pass_state.dark(nav_state.dark());
rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::session::account(cursor, &user, &uname_state, &pass_state, &nav_state)
crate::templates::session::account(
cursor,
&loader,
&user,
&uname_state,
&pass_state,
&nav_state,
)
})
}
async fn delete_account_page(
loader: ActixLoader,
args: DeleteUserPageArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
@ -244,11 +289,15 @@ async fn delete_account_page(
delete_state.dark(nav_state.dark());
rendered(HttpResponse::Ok(), |cursor| {
crate::templates::session::delete_account(cursor, &delete_state, &nav_state)
crate::templates::session::delete_account(cursor, &loader, &delete_state, &nav_state)
})
}
async fn delete_account(args: DeleteUserArgs, nav_state: NavState) -> Result<HttpResponse, Error> {
async fn delete_account(
loader: ActixLoader,
args: DeleteUserArgs,
nav_state: NavState,
) -> Result<HttpResponse, Error> {
let mut delete_state = match hyaenidae_accounts::delete_user(args).await? {
Ok(state) => state,
Err(res) => return Ok(res),
@ -257,6 +306,6 @@ async fn delete_account(args: DeleteUserArgs, nav_state: NavState) -> Result<Htt
delete_state.dark(nav_state.dark());
rendered(HttpResponse::BadRequest(), |cursor| {
crate::templates::session::delete_account(cursor, &delete_state, &nav_state)
crate::templates::session::delete_account(cursor, &loader, &delete_state, &nav_state)
})
}

View File

@ -1,4 +1,3 @@
use crate::rendered;
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
pub(crate) trait OptionExt<T> {
@ -40,18 +39,7 @@ pub(crate) enum Error {
impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
match self {
Error::Render(_)
| Error::Accounts(_)
| Error::Profiles(_)
| Error::Sled(_)
| Error::Transaction(_)
| Error::Json(_)
| Error::Url(_)
| Error::Webfinger(_)
| Error::Panic => StatusCode::INTERNAL_SERVER_ERROR,
Error::Required => StatusCode::SEE_OTHER,
}
StatusCode::SEE_OTHER
}
fn error_response(&self) -> HttpResponse {
@ -59,14 +47,7 @@ impl ResponseError for Error {
return crate::to_404();
}
match rendered(HttpResponse::build(self.status_code()), |cursor| {
crate::templates::error(cursor, self.to_string(), true)
}) {
Ok(res) => res,
Err(_) => HttpResponse::build(self.status_code())
.content_type(mime::TEXT_PLAIN.essence_str())
.body(self.to_string()),
}
crate::to_500()
}
}

View File

@ -3,7 +3,7 @@ use crate::{
middleware::UserProfile,
nav::NavState,
pagination::{Page, PageSource, Pagination},
views::{ProfileView, SubmissionView},
views::SubmissionView,
State,
};
use actix_web::{web, HttpResponse, Scope};
@ -73,9 +73,6 @@ impl ViewFeedState {
.filter_map(move |submission_id| {
let submission = self.cache.submissions.get(submission_id)?;
let author = self.cache.profiles.get(&submission.profile_id())?;
let author_icon = author
.icon()
.and_then(move |icon_id| self.cache.files.get(&icon_id));
let files = submission
.files()
.iter()
@ -84,7 +81,6 @@ impl ViewFeedState {
Some(SubmissionView {
author,
author_icon,
submission,
files,
})
@ -121,20 +117,6 @@ impl ViewFeedState {
format!("/feed?max={}", id)
}
pub(crate) fn profile<'a>(&'a self) -> ProfileView<'a> {
ProfileView {
profile: &self.profile,
icon: self
.profile
.icon()
.and_then(|file_id| self.cache.files.get(&file_id)),
banner: self
.profile
.banner()
.and_then(|file_id| self.cache.files.get(&file_id)),
}
}
async fn build(
profile_id: Uuid,
source: Option<PageSource>,

149
src/i18n.rs Normal file
View File

@ -0,0 +1,149 @@
use actix_web::{dev::Payload, FromRequest, HttpRequest};
use fluent::{FluentArgs, FluentValue};
use fluent_syntax::ast::Message;
use i18n_embed::{
fluent::{fluent_language_loader, FluentLanguageLoader},
I18nAssets, I18nEmbedError, LanguageLoader,
};
use rust_embed::RustEmbed;
use std::future::{ready, Ready};
use std::{borrow::Cow, collections::HashMap};
use unic_langid::LanguageIdentifier;
#[derive(RustEmbed)]
#[folder = "./translations"]
struct Translations;
pub struct ActixLoader(FluentLanguageLoader);
impl ActixLoader {
pub fn current_languages(&self) -> Vec<LanguageIdentifier> {
self.0.current_languages()
}
pub fn get(&self, message_id: &str) -> String {
self.0.get(message_id)
}
pub fn get_args_concrete<'source>(
&self,
message_id: &str,
args: HashMap<&'source str, FluentValue<'source>>,
) -> String {
self.0.get_args_concrete(message_id, args)
}
pub fn get_args_fluent<'args>(
&self,
message_id: &str,
args: Option<&'args FluentArgs<'args>>,
) -> String {
self.0.get_args_fluent(message_id, args)
}
pub fn get_args<'a, S, V>(&self, id: &str, args: HashMap<S, V>) -> String
where
S: Into<Cow<'a, str>> + Clone,
V: Into<FluentValue<'a>> + Clone,
{
self.0.get_args(id, args)
}
pub fn has(&self, message_id: &str) -> bool {
self.0.has(message_id)
}
pub fn with_message_iter<OUT, C>(&self, language: &LanguageIdentifier, closure: C) -> OUT
where
C: Fn(&mut dyn Iterator<Item = &Message<&str>>) -> OUT,
{
self.0.with_message_iter(language, closure)
}
pub fn set_use_isolating(&self, value: bool) {
self.0.set_use_isolating(value)
}
}
impl FromRequest for ActixLoader {
type Config = ();
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let languages = req
.headers()
.get("Accept-Language")
.and_then(|header_value| {
let header_str = header_value.to_str().ok()?;
Some(parse_accept_language(header_str))
})
.unwrap_or(vec![]);
let loader = fluent_language_loader!();
let _ = i18n_embed::select(&loader, &Translations, &languages);
ready(Ok(ActixLoader(loader)))
}
}
fn parse_accept_language(s: &str) -> Vec<LanguageIdentifier> {
if s.trim() == "*" {
return vec![];
}
let mut langs = s
.split(',')
.filter_map(|language| {
let mut lang_iter = language.split(';');
let lang_with_sub = lang_iter.next()?;
let quality = lang_iter.next();
let quality_value: f32 = if let Some(quality) = quality {
quality.parse::<f32>().ok()?.clamp(0.0f32, 1.0f32)
} else {
1.0f32
};
let lang_identifier: LanguageIdentifier = lang_with_sub.parse().ok()?;
Some((quality_value, lang_identifier))
})
.collect::<Vec<_>>();
langs.sort_by(|lhs, rhs| {
rhs.0
.partial_cmp(&lhs.0)
.unwrap_or(std::cmp::Ordering::Less)
});
langs.into_iter().map(|(_, lang)| lang).collect()
}
impl LanguageLoader for ActixLoader {
fn fallback_language(&self) -> &LanguageIdentifier {
self.0.fallback_language()
}
fn domain(&self) -> &str {
self.0.domain()
}
fn language_file_name(&self) -> String {
self.0.language_file_name()
}
fn current_language(&self) -> LanguageIdentifier {
self.0.current_language()
}
fn load_languages(
&self,
i18n_assets: &dyn I18nAssets,
language_ids: &[&LanguageIdentifier],
) -> Result<(), I18nEmbedError> {
self.0.load_languages(i18n_assets, language_ids)
}
}

View File

@ -19,6 +19,7 @@ mod comments;
mod error;
mod extensions;
mod feed;
mod i18n;
mod images;
mod jobs;
mod middleware;
@ -32,6 +33,7 @@ mod views;
mod webfinger;
use error::{Error, OptionExt};
use i18n::ActixLoader;
use middleware::{CurrentProfile, UserProfile};
use nav::NavState;
use webfinger::HyaenidaeResolver;
@ -140,7 +142,7 @@ async fn main() -> anyhow::Result<()> {
.service(comments::scope())
.service(admin::scope())
.service(notifications::scope())
.default_service(web::route().to(|| async move { to_404() }))
.default_service(web::route().to(to_404))
})
.bind(config.bind_address)?
.run()
@ -334,6 +336,10 @@ fn to_404() -> HttpResponse {
redirect("/404")
}
fn to_500() -> HttpResponse {
redirect("/500")
}
fn to_home() -> HttpResponse {
redirect("/")
}
@ -342,19 +348,15 @@ fn redirect(path: &str) -> HttpResponse {
HttpResponse::SeeOther().header("Location", path).finish()
}
async fn not_found(nav_state: NavState) -> Result<HttpResponse, Error> {
async fn not_found(loader: ActixLoader, nav_state: NavState) -> Result<HttpResponse, Error> {
rendered(HttpResponse::NotFound(), |cursor| {
templates::not_found(cursor, nav_state.dark())
templates::not_found(cursor, loader, nav_state.dark())
})
}
async fn serve_error(nav_state: NavState) -> Result<HttpResponse, Error> {
async fn serve_error(loader: ActixLoader, nav_state: NavState) -> Result<HttpResponse, Error> {
rendered(HttpResponse::InternalServerError(), |cursor| {
templates::error(
cursor,
"Hyaenidae encountered a problem".to_owned(),
nav_state.dark(),
)
templates::error(cursor, loader, nav_state.dark())
})
}

View File

@ -27,20 +27,11 @@ pub struct OwnedSubmissionView {
pub struct SubmissionView<'a> {
pub(crate) author: &'a Profile,
pub(crate) author_icon: Option<&'a File>,
pub(crate) submission: &'a Submission,
pub(crate) files: Vec<&'a File>,
}
impl<'a> SubmissionView<'a> {
pub(crate) fn profile(&self) -> ProfileView<'a> {
ProfileView {
profile: self.author,
icon: self.author_icon,
banner: None,
}
}
pub(crate) fn thumbnail(&self) -> Option<Thumbnail> {
let file = self.files.get(0)?;
let key = file.pictrs_key()?;

View File

@ -0,0 +1,21 @@
@use crate::ActixLoader;
@use hyaenidae_accounts::CookiesState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title}, Button, Card};
@use i18n_embed_fl::fl;
@(card_config: &Card, loader: &ActixLoader, state: &CookiesState)
@:card(card_config, {
@:card_title({ @fl!(loader, "cookies-heading") })
@:card_body({
<p>@fl!(loader, "cookies-description")</p>
})
@:card_body({
<form method="POST" action="@state.cookies_path()">
@:button_group(&[
Button::primary(&fl!(loader, "cookies-accept")),
Button::primary_outline(&fl!(loader, "cookies-cancel")).href(&state.home_path()),
])
</form>
})
})

View File

@ -0,0 +1,22 @@
@use crate::ActixLoader;
@use crate::templates::accounts::inputs::password_input;
@use hyaenidae_accounts::DeleteUserState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title}, Button, Card};
@use i18n_embed_fl::fl;
@(card_config: &Card, loader: &ActixLoader, state: &DeleteUserState)
@:card(card_config, {
<form method="POST" action="@state.delete_user_path()">
@:card_title({ @fl!(loader, "delete-account-heading") })
@:card_body({
@:password_input("password", &fl!(loader, "password-input"), state.password(), state.password_error(), state.dark)
})
@:card_body({
@:button_group(&[
Button::primary(&fl!(loader, "delete-account-button")),
Button::primary_outline(&fl!(loader, "delete-account-cancel")).href(&state.accounts_path()),
])
})
</form>
})

View File

@ -0,0 +1,24 @@
@use crate::ActixLoader;
@use crate::templates::accounts::inputs::{text_input, password_input};
@use hyaenidae_accounts::LoginState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title, link}, Button, Card, Link};
@use i18n_embed_fl::fl;
@(card_config: &Card, loader: &ActixLoader, state: &LoginState)
@:card(card_config, {
<form method="POST" action="@state.login_path()">
@:card_title({ @fl!(loader, "login-heading") })
@:card_body({
@:text_input("username", &fl!(loader, "login-username"), state.username(), state.username_error(), state.dark)
@:password_input("password", &fl!(loader, "login-password"), state.password(), None, state.dark)
@:link(&Link::current_tab(&state.register_path()), { I do not have an account })
})
@:card_body({
@:button_group(&[
Button::primary(&fl!(loader, "login-submit")),
Button::primary_outline(&fl!(loader, "login-cancel")).href(&state.home_path()),
])
})
</form>
})

View File

@ -0,0 +1,25 @@
@use crate::ActixLoader;
@use crate::templates::accounts::inputs::{text_input, password_input};
@use hyaenidae_accounts::RegisterState;
@use hyaenidae_toolkit::{templates::{button_group, card, card_body, card_title, link}, Button, Card, Link};
@use i18n_embed_fl::fl;
@(card_config: &Card, loader: &ActixLoader, state: &RegisterState)
@:card(card_config, {
<form method="POST" action="@state.register_path()">
@:card_title({ @fl!(loader, "register-heading") })
@:card_body({
@:text_input("username", &fl!(loader, "register-username"), state.username(), state.username_error(), state.dark)
@:password_input("password", &fl!(loader, "register-password"), state.password(), None, state.dark)
@:password_input("password_confirmation", &fl!(loader, "register-confirmation"), state.confirmation(), state.confirmation_error(), state.dark)
@:link(&Link::current_tab(&state.login_path()), { I already have an account })
})
@:card_body({
@:button_group(&[
Button::primary(&fl!(loader, "register-submit")),
Button::primary_outline(&fl!(loader, "register-cancel")).href(&state.home_path()),
])
})
</form>
})

16
templates/error.rs.html Normal file
View File

@ -0,0 +1,16 @@
@use crate::ActixLoader;
@use crate::templates::layouts::main;
@use hyaenidae_toolkit::{templates::{card, card_body, card_title, link}, Card, Link};
@use i18n_embed_fl::fl;
@(loader: ActixLoader, dark: bool)
@:main(&fl!(loader, "error-title"), &fl!(loader, "error-subtitle"), dark, {}, {
@:card(&Card::full_width().dark(dark), {
@:card_title({ @fl!(loader, "error-heading") })
@:card_body({ @fl!(loader, "error-description") })
@:card_body({
@:link(&Link::current_tab("/"), { @fl!(loader, "return-home-button") })
})
})
})

View File

@ -0,0 +1,15 @@
@use crate::ActixLoader;
@use crate::templates::layouts::main;
@use hyaenidae_toolkit::{templates::{card, card_body, card_title, link}, Card, Link};
@use i18n_embed_fl::fl;
@(loader: ActixLoader, dark: bool)
@:main(&fl!(loader, "not-found-title"), &fl!(loader, "not-found-subtitle"), dark, {}, {
@:card(&Card::full_width().dark(dark), {
@:card_title({ @fl!(loader, "not-found-heading") })
@:card_body({
@:link(&Link::current_tab("/"), { @fl!(loader, "return-home-button") })
})
})
})

View File

@ -1,23 +1,25 @@
@use crate::ActixLoader;
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::{update_password, update_username};
@use hyaenidae_accounts::{UpdatePasswordState, UpdateUsernameState, User};
@use hyaenidae_toolkit::{templates::{button, card, card_body, card_title}, Button, Card};
@use i18n_embed_fl::fl;
@(user: &User, uname_state: &UpdateUsernameState, pass_state: &UpdatePasswordState, nav_state: &NavState)
@(loader: &ActixLoader, user: &User, uname_state: &UpdateUsernameState, pass_state: &UpdatePasswordState, nav_state: &NavState)
@:home("Account Settings", "Update account information", nav_state, {
@:home(&fl!(loader, "account-settings-heading"), &fl!(loader, "account-settings-subheading"), nav_state, {
@:button_js()
}, {
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Current Username: @user.username() })
@:card_title({ @fl!(loader, "username-heading", currentUsername = user.username()) })
})
@:update_username(&Card::full_width().dark(nav_state.dark()), uname_state)
@:update_password(&Card::full_width().dark(nav_state.dark()), pass_state)
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({ Danger })
@:card_title({ @fl!(loader, "danger-heading") })
@:card_body({
@:button(&Button::primary("Delete Account").href("/session/account/delete"))
@:button(&Button::primary(&fl!(loader, "delete-account-button")).href("/session/account/delete"))
})
})
})

View File

@ -0,0 +1,13 @@
@use crate::ActixLoader;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::cookies;
@use hyaenidae_accounts::CookiesState;
@use hyaenidae_toolkit::Card;
@use i18n_embed_fl::fl;
@(loader: &ActixLoader, cookie_state: &CookiesState, nav_state: &NavState)
@:home(&fl!(loader, "cookies-heading"), &fl!(loader, "cookies-subheading"), nav_state, {}, {
@:cookies(&Card::full_width().dark(nav_state.dark()), loader, cookie_state)
})

View File

@ -0,0 +1,15 @@
@use crate::ActixLoader;
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::delete_user;
@use hyaenidae_accounts::DeleteUserState;
@use hyaenidae_toolkit::Card;
@use i18n_embed_fl::fl;
@(loader: &ActixLoader, state: &DeleteUserState, nav_state: &NavState)
@:home(&fl!(loader, "delete-account-heading"), &fl!(loader, "delete-account-confirmation"), nav_state, {
@:button_js()
}, {
@:delete_user(&Card::full_width().dark(nav_state.dark()), loader, state)
})

View File

@ -0,0 +1,15 @@
@use crate::ActixLoader;
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::login;
@use hyaenidae_accounts::LoginState;
@use hyaenidae_toolkit::Card;
@use i18n_embed_fl::fl;
@(loader: &ActixLoader, login_state: &LoginState, nav_state: &NavState)
@:home(&fl!(loader, "login-heading"), &fl!(loader, "login-subheading"), nav_state, {
@:button_js()
}, {
@:login(&Card::full_width().dark(nav_state.dark()), loader, login_state)
})

View File

@ -0,0 +1,13 @@
@use crate::ActixLoader;
@use crate::templates::button_js;
@use crate::{templates::layouts::home, nav::NavState};
@use crate::templates::accounts::register;
@use hyaenidae_accounts::RegisterState;
@use hyaenidae_toolkit::Card;
@use i18n_embed_fl::fl;
@(loader: &ActixLoader, register_state: &RegisterState, nav_state: &NavState)
@:home(&fl!(loader, "register-heading"), &fl!(loader, "register-subheading"), nav_state, { @:button_js() }, {
@:register(&Card::full_width().dark(nav_state.dark()), loader, register_state)
})

Some files were not shown because too many files have changed in this diff Show More