diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/liveops.rs | 4 | ||||
| -rw-r--r-- | src/api/ops.rs | 20 | ||||
| -rw-r--r-- | src/api/users.rs | 24 | ||||
| -rw-r--r-- | src/main.rs | 19 | ||||
| -rw-r--r-- | src/resources/languages.rs | 67 | ||||
| -rw-r--r-- | src/resources/mod.rs | 4 | ||||
| -rw-r--r-- | src/resources/scripts.rs | 37 | ||||
| -rw-r--r-- | src/resources/style.rs | 53 | ||||
| -rw-r--r-- | src/resources/templates.rs | 59 |
9 files changed, 271 insertions, 16 deletions
diff --git a/src/api/liveops.rs b/src/api/liveops.rs index 2dda41d..d4bf129 100644 --- a/src/api/liveops.rs +++ b/src/api/liveops.rs @@ -1,11 +1,11 @@ use actix_web::{get, web, HttpResponse, Scope}; /// Simple ping -#[get("ping")] +#[get("/ping")] async fn ping() -> HttpResponse { HttpResponse::Ok().finish() } pub fn service() -> Scope { - web::scope("liveops").service(ping) + web::scope("/liveops").service(ping) } diff --git a/src/api/ops.rs b/src/api/ops.rs index 018743c..d947e64 100644 --- a/src/api/ops.rs +++ b/src/api/ops.rs @@ -1,9 +1,14 @@ -use actix_web::{http::StatusCode, post, web, HttpResponse, ResponseError, Scope}; +use std::str::FromStr; + +use actix_web::{get, http::StatusCode, post, web, HttpResponse, ResponseError, Scope}; use raise::yeet; use serde::Deserialize; use sqlx::MySqlPool; +use tera::Tera; use thiserror::Error; +use unic_langid::subtags::Language; +use crate::resources::{languages, templates}; use crate::services::db; /// A request to login @@ -60,6 +65,17 @@ async fn login( Ok(response) } +#[get("/login")] +async fn login_page( + tera: web::Data<Tera>, + translations: web::Data<languages::Translations>, +) -> HttpResponse { + // TODO find a better way of doing this + let language = Language::from_str("en").unwrap(); + let page = templates::login_page(&tera, language, translations.get_ref().clone()).unwrap(); + HttpResponse::Ok().content_type("text/html").body(page) +} + pub fn service() -> Scope { - web::scope("").service(login) + web::scope("").service(login).service(login_page) } diff --git a/src/api/users.rs b/src/api/users.rs index 863d99e..353f8ff 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -26,19 +26,21 @@ impl From<User> for UserResponse { } } -#[get("/")] -async fn search_users( - web::Query(username): web::Query<Option<Box<str>>>, - web::Query(limit): web::Query<Option<u32>>, - web::Query(offset): web::Query<Option<u32>>, - conn: web::Data<MySqlPool>, -) -> HttpResponse { +#[derive(Debug, Clone, Deserialize)] +struct SearchUsers { + username: Option<Box<str>>, + limit: Option<u32>, + offset: Option<u32>, +} + +#[get("")] +async fn search_users(params: web::Query<SearchUsers>, conn: web::Data<MySqlPool>) -> HttpResponse { let conn = conn.get_ref(); - let username = username.unwrap_or_default(); - let offset = offset.unwrap_or_default(); + let username = params.username.clone().unwrap_or_default(); + let offset = params.offset.unwrap_or_default(); - let results: Box<[UserResponse]> = if let Some(limit) = limit { + let results: Box<[UserResponse]> = if let Some(limit) = params.limit { db::search_users_limit(conn, &username, offset, limit) .await .unwrap() @@ -129,7 +131,7 @@ impl ResponseError for UsernameTakenError { } } -#[post("/")] +#[post("")] async fn create_user( body: web::Json<UserRequest>, conn: web::Data<MySqlPool>, diff --git a/src/main.rs b/src/main.rs index 7d2b643..7b25dd1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use actix_web::http::header::{self, HeaderValue}; -use actix_web::middleware::{DefaultHeaders, ErrorHandlerResponse, ErrorHandlers, Logger}; +use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers, Logger, NormalizePath}; use actix_web::web::Data; use actix_web::{dev, App, HttpServer}; @@ -7,8 +7,10 @@ use exun::*; mod api; mod models; +mod resources; mod services; +use resources::*; use services::*; fn error_content_language<B>( @@ -31,12 +33,27 @@ async fn main() -> Result<(), RawUnexpected> { let db_url = secrets::database_url()?; let sql_pool = db::initialize(&db_url).await?; + let tera = templates::initialize()?; + + let translations = languages::initialize()?; + // start the server HttpServer::new(move || { App::new() + // middleware .wrap(ErrorHandlers::new().default_handler(error_content_language)) + .wrap(NormalizePath::trim()) .wrap(Logger::new("\"%r\" %s %Dms")) + // app shared state .app_data(Data::new(sql_pool.clone())) + .app_data(Data::new(tera.clone())) + .app_data(Data::new(translations.clone())) + // frontend services + // has to be first so they don't get overwritten by the "" scope + .service(style::get_css) + .service(scripts::get_js) + .service(languages::languages()) + // api services .service(api::liveops()) .service(api::users()) .service(api::ops()) diff --git a/src/resources/languages.rs b/src/resources/languages.rs new file mode 100644 index 0000000..8ef7553 --- /dev/null +++ b/src/resources/languages.rs @@ -0,0 +1,67 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use actix_web::{get, web, HttpResponse, Scope}; +use exun::RawUnexpected; +use ini::{Ini, Properties}; +use raise::yeet; +use unic_langid::subtags::Language; + +#[derive(Debug, Clone, PartialEq)] +pub struct Translations { + languages: HashMap<Language, Properties>, +} + +pub fn initialize() -> Result<Translations, RawUnexpected> { + let mut translations = Translations { + languages: HashMap::new(), + }; + translations.refresh()?; + Ok(translations) +} + +impl Translations { + pub fn languages(&self) -> Box<[Language]> { + self.languages.keys().cloned().collect() + } + + pub fn get_message(&self, language: Language, key: &str) -> Option<String> { + Some(self.languages.get(&language)?.get(key)?.to_owned()) + } + + pub fn refresh(&mut self) -> Result<(), RawUnexpected> { + let mut languages = HashMap::with_capacity(1); + for entry in PathBuf::from("static/languages").read_dir()? { + let entry = entry?; + if entry.file_type()?.is_dir() { + continue; + } + + let path = entry.path(); + let path = path.to_string_lossy(); + let Some(language) = path.as_bytes().get(0..2) else { yeet!(RawUnexpected::msg(format!("{} not long enough to be a language name", path))) }; + let language = Language::from_bytes(language)?; + let messages = Ini::load_from_file(entry.path())?.general_section().clone(); + + languages.insert(language, messages); + } + + self.languages = languages; + Ok(()) + } +} + +#[get("")] +pub async fn all_languages(translations: web::Data<Translations>) -> HttpResponse { + HttpResponse::Ok().json( + translations + .languages() + .into_iter() + .map(|l| l.as_str()) + .collect::<Box<[&str]>>(), + ) +} + +pub fn languages() -> Scope { + web::scope("/languages").service(all_languages) +} diff --git a/src/resources/mod.rs b/src/resources/mod.rs new file mode 100644 index 0000000..9251d2c --- /dev/null +++ b/src/resources/mod.rs @@ -0,0 +1,4 @@ +pub mod languages; +pub mod scripts; +pub mod style; +pub mod templates; diff --git a/src/resources/scripts.rs b/src/resources/scripts.rs new file mode 100644 index 0000000..3e2d869 --- /dev/null +++ b/src/resources/scripts.rs @@ -0,0 +1,37 @@ +use std::path::{Path, PathBuf}; + +use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError}; +use exun::{Expect, ResultErrorExt}; +use raise::yeet; +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Clone, Error, Serialize)] +pub enum LoadScriptError { + #[error("The requested script does not exist")] + FileNotFound(Box<Path>), +} + +impl ResponseError for LoadScriptError { + fn status_code(&self) -> StatusCode { + match self { + Self::FileNotFound(..) => StatusCode::NOT_FOUND, + } + } +} + +fn load(script: &str) -> Result<String, Expect<LoadScriptError>> { + let path = PathBuf::from(format!("static/scripts/{}.js", script)); + if !path.exists() { + yeet!(LoadScriptError::FileNotFound(path.into()).into()); + } + let js = std::fs::read_to_string(format!("static/scripts/{}.js", script)).unexpect()?; + Ok(js) +} + +#[get("/{script}.js")] +pub async fn get_js(script: web::Path<Box<str>>) -> Result<HttpResponse, LoadScriptError> { + let js = load(&script).map_err(|e| e.unwrap())?; + let response = HttpResponse::Ok().content_type("text/javascript").body(js); + Ok(response) +} diff --git a/src/resources/style.rs b/src/resources/style.rs new file mode 100644 index 0000000..2777a82 --- /dev/null +++ b/src/resources/style.rs @@ -0,0 +1,53 @@ +use std::path::{Path, PathBuf}; + +use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError}; +use exun::{Expect, ResultErrorExt}; +use grass::OutputStyle; +use raise::yeet; +use serde::Serialize; +use thiserror::Error; + +fn output_style() -> OutputStyle { + if cfg!(debug_assertions) { + OutputStyle::Expanded + } else { + OutputStyle::Compressed + } +} + +fn options() -> grass::Options<'static> { + grass::Options::default() + .load_path("static/style") + .style(output_style()) +} + +#[derive(Debug, Clone, Error, Serialize)] +pub enum LoadStyleError { + #[error("The requested stylesheet was not found")] + FileNotFound(Box<Path>), +} + +impl ResponseError for LoadStyleError { + fn status_code(&self) -> StatusCode { + match self { + Self::FileNotFound(..) => StatusCode::NOT_FOUND, + } + } +} + +pub fn load(stylesheet: &str) -> Result<String, Expect<LoadStyleError>> { + let options = options(); + let path = PathBuf::from(format!("static/style/{}.scss", stylesheet)); + if !path.exists() { + yeet!(LoadStyleError::FileNotFound(path.into()).into()); + } + let css = grass::from_path(format!("static/style/{}.scss", stylesheet), &options).unexpect()?; + Ok(css) +} + +#[get("/{stylesheet}.css")] +pub async fn get_css(stylesheet: web::Path<Box<str>>) -> Result<HttpResponse, LoadStyleError> { + let css = load(&stylesheet).map_err(|e| e.unwrap())?; + let response = HttpResponse::Ok().content_type("text/css").body(css); + Ok(response) +} diff --git a/src/resources/templates.rs b/src/resources/templates.rs new file mode 100644 index 0000000..43d6b67 --- /dev/null +++ b/src/resources/templates.rs @@ -0,0 +1,59 @@ +use std::collections::HashMap; + +use exun::{RawUnexpected, ResultErrorExt}; +use raise::yeet; +use tera::{Function, Tera, Value}; +use unic_langid::subtags::Language; + +use super::languages; + +fn make_lang(language: Language) -> impl Function { + Box::new(move |_: &HashMap<String, Value>| -> tera::Result<Value> { + Ok(Value::String(language.to_string())) + }) +} + +fn make_msg(language: Language, translations: languages::Translations) -> impl Function { + Box::new( + move |args: &HashMap<String, Value>| -> tera::Result<Value> { + let Some(key) = args.get("key") else { yeet!("No parameter 'key' provided".into()) }; + let Some(key) = key.as_str() else { yeet!(format!("{} is not a string", key).into()) }; + let Some(value) = translations.get_message(language, key) else { yeet!(format!("{} does not exist", key).into()) }; + Ok(Value::String(value)) + }, + ) +} + +fn make_base_url() -> impl Function { + Box::new(|_: &HashMap<String, Value>| Ok(Value::String("foo".to_string()))) +} + +fn extend_tera( + tera: &Tera, + language: Language, + translations: languages::Translations, +) -> Result<Tera, RawUnexpected> { + let mut new_tera = initialize()?; + new_tera.extend(tera)?; + new_tera.register_function("lang", make_lang(language)); + new_tera.register_function("msg", make_msg(language, translations)); + new_tera.register_function("baseUrl", make_base_url()); + Ok(new_tera) +} + +pub fn initialize() -> tera::Result<Tera> { + let tera = Tera::new("static/templates/*")?; + Ok(tera) +} + +pub fn login_page( + tera: &Tera, + language: Language, + mut translations: languages::Translations, +) -> Result<String, RawUnexpected> { + translations.refresh()?; + let mut tera = extend_tera(tera, language, translations)?; + tera.full_reload()?; + let context = tera::Context::new(); + tera.render("login.html", &context).unexpect() +} |
