summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/liveops.rs4
-rw-r--r--src/api/ops.rs20
-rw-r--r--src/api/users.rs24
-rw-r--r--src/main.rs19
-rw-r--r--src/resources/languages.rs67
-rw-r--r--src/resources/mod.rs4
-rw-r--r--src/resources/scripts.rs37
-rw-r--r--src/resources/style.rs53
-rw-r--r--src/resources/templates.rs59
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()
+}