mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Create society service with basic functionality and auth #118
This commit is contained in:
0
rc_society/src/api/mod.rs
Normal file
0
rc_society/src/api/mod.rs
Normal file
58
rc_society/src/cli.rs
Normal file
58
rc_society/src/cli.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct CliArgs {
|
||||
/// TCP port on which to accept connections
|
||||
#[arg(short, long, default_value_t = 8002)]
|
||||
pub port: u16,
|
||||
|
||||
/// IP Address on which to accept connections
|
||||
#[arg(long, default_value_t = {"127.0.0.1".to_string()})]
|
||||
pub ip: String,
|
||||
|
||||
/// Assets root
|
||||
#[arg(long, default_value_t = {"../assets/robocraft".to_string()})]
|
||||
pub assets_robocraft: String,
|
||||
|
||||
/// User data root
|
||||
#[arg(long, default_value_t = {"../data/robocraft".to_string()})]
|
||||
pub data_robocraft: String,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
pub fn get() -> Self {
|
||||
Self::parse()
|
||||
}
|
||||
|
||||
pub fn loaded(&self) -> LoadedArgs {
|
||||
let assets_path = std::path::PathBuf::from(&self.assets_robocraft);
|
||||
let data_path = std::path::PathBuf::from(&self.data_robocraft);
|
||||
let token_path = data_path.join(oj_rc_core::persist::user::TOKEN_SECRET_FILENAME);
|
||||
let secret = std::fs::read(&token_path).expect("Bad token");
|
||||
let cookie_key = if secret.len() < 32 {
|
||||
log::warn!("{} should be >= 32 bytes (extending with zeroes)", token_path.display());
|
||||
let mut secret_ext = secret.clone();
|
||||
while secret_ext.len() < 32 {
|
||||
secret_ext.push(0);
|
||||
}
|
||||
actix_web::cookie::Key::derive_from(&secret_ext)
|
||||
} else {
|
||||
actix_web::cookie::Key::derive_from(&secret)
|
||||
};
|
||||
LoadedArgs {
|
||||
secret: std::sync::Arc::new(secret.clone()),
|
||||
cookie_key,
|
||||
assets: assets_path,
|
||||
data: data_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct LoadedArgs {
|
||||
pub secret: std::sync::Arc<Vec<u8>>,
|
||||
pub cookie_key: actix_web::cookie::Key,
|
||||
pub assets: std::path::PathBuf,
|
||||
pub data: std::path::PathBuf,
|
||||
}
|
||||
78
rc_society/src/main.rs
Normal file
78
rc_society/src/main.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
#![forbid(unsafe_code)]
|
||||
mod cli;
|
||||
mod api;
|
||||
mod web;
|
||||
|
||||
use actix_web::{App, HttpServer, Responder};
|
||||
|
||||
pub static START_TIME: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(-1);
|
||||
|
||||
#[actix_web::get("/version")]
|
||||
async fn version_info() -> impl Responder {
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]);
|
||||
let authors = env!("CARGO_PKG_AUTHORS");
|
||||
let license = env!("CARGO_PKG_LICENSE");
|
||||
let repo = env!("CARGO_PKG_REPOSITORY");
|
||||
format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
let cli_args = cli::CliArgs::get();
|
||||
|
||||
let config = oj_rc_core::ConfigImpl::load(&cli_args.assets_robocraft)?;
|
||||
|
||||
let server_settings = actix_web::web::Data::new(<oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::server_config(&config));
|
||||
|
||||
let users = oj_rc_core::UserImpl::load(&cli_args.data_robocraft, &config).await.expect("Bad user data");
|
||||
let auth_ref = actix_web::web::Data::new(Box::new(users));
|
||||
|
||||
let cli_args2 = actix_web::web::Data::new(cli_args.clone());
|
||||
let loadeds_args = actix_web::web::Data::new(cli_args.loaded());
|
||||
|
||||
let mut handlebars_conf = handlebars::Handlebars::new();
|
||||
let mut dir_conf = handlebars::DirectorySourceOptions::default();
|
||||
dir_conf.tpl_extension = ".html.hbs".to_owned();
|
||||
dir_conf.hidden = false;
|
||||
dir_conf.temporary = false;
|
||||
handlebars_conf
|
||||
.register_templates_directory(
|
||||
std::path::PathBuf::from(&cli_args.assets_robocraft).parent().expect("Bad robocraft asset path").join("templates/rc_society"),
|
||||
dir_conf,
|
||||
)
|
||||
.unwrap();
|
||||
let handlebars_ref = actix_web::web::Data::new(handlebars_conf);
|
||||
|
||||
START_TIME.store(chrono::Utc::now().timestamp(), std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap_fn(|req, srv| {
|
||||
use actix_web::dev::Service;
|
||||
log::trace!("Request {} {}", req.method(), req.path());
|
||||
srv.call(req)
|
||||
})
|
||||
.wrap(actix_identity::IdentityMiddleware::default())
|
||||
.wrap(actix_session::SessionMiddleware::new(
|
||||
actix_session::storage::CookieSessionStore::default(),
|
||||
loadeds_args.cookie_key.clone(),
|
||||
))
|
||||
.app_data(cli_args2.clone())
|
||||
.app_data(loadeds_args.clone())
|
||||
.app_data(handlebars_ref.clone())
|
||||
.app_data(server_settings.clone())
|
||||
.app_data(auth_ref.clone())
|
||||
.service(version_info)
|
||||
.service(web::login::form_submit)
|
||||
.service(web::login::form_load)
|
||||
.service(web::favicon::favicon_standard)
|
||||
.service(web::dashboard::get)
|
||||
.service(web::index::get)
|
||||
})
|
||||
.bind((cli_args.ip, cli_args.port))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
65
rc_society/src/web/dashboard.rs
Normal file
65
rc_society/src/web/dashboard.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use actix_web::{get, web::Data, Responder, HttpRequest};
|
||||
use actix_identity::Identity;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
const FORM_NAME: &str = "dashboard";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RenderData {
|
||||
// TODO
|
||||
display_name: String,
|
||||
public_id: String,
|
||||
debug: DebugData,
|
||||
perms: PermissionData,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DebugData {
|
||||
user_id: i32,
|
||||
creation_time_unix: i64,
|
||||
creation_time_iso: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PermissionData {
|
||||
r#mod: bool,
|
||||
admin: bool,
|
||||
dev: bool,
|
||||
royal: bool,
|
||||
banned: bool,
|
||||
}
|
||||
|
||||
#[get("/dashboard")]
|
||||
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
|
||||
match super::try_auth_user(user_opt, auth.as_ref(), &req).await? {
|
||||
super::LoginReturn::AuthFail(resp) => Ok(resp),
|
||||
super::LoginReturn::Success(user) => {
|
||||
// TODO
|
||||
let creation_time = user.creation();
|
||||
let creation_time_chrono = chrono::DateTime::<chrono::Utc>::from_timestamp_secs(creation_time).unwrap_or_default();
|
||||
Ok(super::render_ok(
|
||||
RenderData {
|
||||
display_name: user.display_name().to_owned(),
|
||||
public_id: user.public_id().to_owned(),
|
||||
debug: DebugData {
|
||||
user_id: user.account_id(),
|
||||
creation_time_unix: creation_time,
|
||||
creation_time_iso: creation_time_chrono.to_rfc3339(),
|
||||
},
|
||||
perms: PermissionData {
|
||||
r#mod: user.is_mod(),
|
||||
admin: user.is_admin(),
|
||||
dev: user.is_dev(),
|
||||
royal: user.is_royal(),
|
||||
banned: user.is_banned(),
|
||||
}
|
||||
},
|
||||
handlebars_ref.as_ref(),
|
||||
FORM_NAME,
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
11
rc_society/src/web/favicon.rs
Normal file
11
rc_society/src/web/favicon.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use actix_web::{get, web::Data, Responder};
|
||||
|
||||
async fn favicon_impl(cli_args: Data<crate::cli::LoadedArgs>) -> impl Responder {
|
||||
let path = std::path::PathBuf::from(&cli_args.assets).join("favicon.jpg");
|
||||
actix_files::NamedFile::open_async(path).await
|
||||
}
|
||||
|
||||
#[get("/favicon.ico")]
|
||||
pub async fn favicon_standard(cli_args: Data<crate::cli::LoadedArgs>) -> impl Responder {
|
||||
favicon_impl(cli_args).await
|
||||
}
|
||||
77
rc_society/src/web/index.rs
Normal file
77
rc_society/src/web/index.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use actix_web::{get, web::Data, Responder, HttpRequest};
|
||||
use actix_identity::Identity;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub const FORM_NAME: &str = "index";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RenderData {
|
||||
is_logged_in: bool,
|
||||
display_name: Option<String>,
|
||||
server: ServerDetails,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ServerDetails {
|
||||
domain: String,
|
||||
cdn: String,
|
||||
auth: String,
|
||||
min_version: i32,
|
||||
server_version: String,
|
||||
start_time_iso: String,
|
||||
start_time_unix: i64,
|
||||
}
|
||||
|
||||
fn server_details(conf: &oj_rc_core::persist::config::ServerConfig) -> ServerDetails {
|
||||
let start_time_unix = crate::START_TIME.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let start_time_chrono = chrono::DateTime::<chrono::Utc>::from_timestamp_secs(start_time_unix).unwrap_or_default();
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]);
|
||||
let server_version = format!("{}:{}", version, git_version);
|
||||
ServerDetails {
|
||||
domain: conf.domain.clone(),
|
||||
cdn: conf.cdn_url.clone(),
|
||||
auth: conf.auth_url.clone(),
|
||||
min_version: conf.minimum_version,
|
||||
server_version,
|
||||
start_time_unix,
|
||||
start_time_iso: start_time_chrono.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn get(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, server_config: Data<oj_rc_core::persist::config::ServerConfig>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
|
||||
let server_info = server_details(server_config.as_ref());
|
||||
if let Some(user) = user_opt {
|
||||
match super::try_auth_user(Some(user), auth.as_ref(), &req).await? {
|
||||
super::LoginReturn::AuthFail(resp) => Ok(resp),
|
||||
super::LoginReturn::Success(user) => {
|
||||
Ok(super::render_ok(
|
||||
RenderData {
|
||||
is_logged_in: true,
|
||||
display_name: Some(user.display_name().to_owned()),
|
||||
server: server_info,
|
||||
},
|
||||
handlebars_ref.as_ref(),
|
||||
FORM_NAME,
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body()
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(super::render_ok(
|
||||
RenderData {
|
||||
is_logged_in: false,
|
||||
display_name: None,
|
||||
server: server_info,
|
||||
},
|
||||
handlebars_ref.as_ref(),
|
||||
FORM_NAME,
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body()
|
||||
)
|
||||
}
|
||||
}
|
||||
66
rc_society/src/web/login.rs
Normal file
66
rc_society/src/web/login.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use actix_web::{get, post, web::{Data, Form, Redirect}, Responder, HttpRequest, HttpMessage};
|
||||
use actix_identity::Identity;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
const FORM_NAME: &str = "login";
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct LoginForm {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
pub async fn form_submit(form: Form<LoginForm>, auth: Data<Box<oj_rc_core::UserImpl>>, handlebars_ref: Data<handlebars::Handlebars<'_>>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
|
||||
use oj_rc_core::UserAuthenticator;
|
||||
let auth_result = auth.login(oj_rc_core::persist::user::UserAuthInfo::Username {
|
||||
username: form.username.to_owned(),
|
||||
password: form.password.to_owned(),
|
||||
}).await;
|
||||
match auth_result {
|
||||
Ok(user) => {
|
||||
let resp = Redirect::to("/dashboard")
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body();
|
||||
Identity::login(&req.extensions(), user.response.token)?;
|
||||
Ok(resp)
|
||||
},
|
||||
Err(e) => {
|
||||
Ok(super::render_err(form.0, e.message, handlebars_ref.as_ref(), FORM_NAME)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
pub async fn form_load(handlebars_ref: Data<handlebars::Handlebars<'_>>, auth: Data<Box<oj_rc_core::UserImpl>>, user_opt: Option<Identity>, req: HttpRequest) -> Result<impl Responder, actix_web::error::Error> {
|
||||
if let Some(user) = user_opt {
|
||||
let user_id = user.id()?;
|
||||
use oj_rc_core::UserAuthenticator;
|
||||
if auth.verify(user_id).await.is_ok() {
|
||||
Ok(Redirect::to("/dashboard")
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body())
|
||||
} else {
|
||||
Ok(super::render_err(
|
||||
LoginForm::default(),
|
||||
"Invalid login token".to_owned(),
|
||||
handlebars_ref.as_ref(),
|
||||
FORM_NAME,
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Ok(super::render_ok(
|
||||
LoginForm::default(),
|
||||
handlebars_ref.as_ref(),
|
||||
FORM_NAME,
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body()
|
||||
)
|
||||
}
|
||||
}
|
||||
71
rc_society/src/web/mod.rs
Normal file
71
rc_society/src/web/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
pub mod dashboard;
|
||||
pub mod login;
|
||||
pub mod favicon;
|
||||
pub mod index;
|
||||
|
||||
use serde::Serialize;
|
||||
use actix_web::{web::{Html, Redirect}, Responder};
|
||||
|
||||
fn version_string() -> String {
|
||||
let name = env!("CARGO_PKG_NAME");
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
//let license = env!("CARGO_PKG_LICENSE");
|
||||
//let repo = env!("CARGO_PKG_REPOSITORY");
|
||||
format!("OpenJam {} {}", name, version)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Context<T: Serialize> {
|
||||
form: T,
|
||||
error: Option<String>,
|
||||
version: String,
|
||||
source_url: String,
|
||||
}
|
||||
|
||||
fn render_ok<T: Serialize>(form: T, renderer: &handlebars::Handlebars<'_>, form_name: &str) -> Html {
|
||||
let rendered = renderer.render(form_name, &Context {
|
||||
form,
|
||||
error: None,
|
||||
version: version_string(),
|
||||
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
||||
}).unwrap();
|
||||
Html::new(rendered)
|
||||
}
|
||||
|
||||
fn render_err<T: Serialize>(form: T, error: String , renderer: &handlebars::Handlebars<'_>, form_name: &str) -> Html {
|
||||
let rendered = renderer.render(form_name, &Context {
|
||||
form,
|
||||
error: Some(error),
|
||||
version: version_string(),
|
||||
source_url: env!("CARGO_PKG_REPOSITORY").to_owned(),
|
||||
}).unwrap();
|
||||
Html::new(rendered)
|
||||
}
|
||||
|
||||
enum LoginReturn {
|
||||
Success(Box<dyn oj_rc_core::persist::user::WebUser>),
|
||||
AuthFail(actix_web::HttpResponse<actix_web::body::BoxBody>),
|
||||
}
|
||||
|
||||
async fn try_auth_user(user_opt: Option<actix_identity::Identity>, auth: &oj_rc_core::UserImpl, req: &actix_web::HttpRequest) -> Result<LoginReturn, actix_web::Error> {
|
||||
if let Some(user) = user_opt {
|
||||
let user_id = user.id()?;
|
||||
match <oj_rc_core::UserImpl as oj_rc_core::UserProvider<()>>::web_authenticate(auth, user_id.clone()).await {
|
||||
Ok(user) => Ok(LoginReturn::Success(user)),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to login with token {}: {} ({:?})", user_id, e.message, e.code);
|
||||
Ok(LoginReturn::AuthFail(
|
||||
Redirect::to("/login")
|
||||
.respond_to(req)
|
||||
.map_into_boxed_body()
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(LoginReturn::AuthFail(
|
||||
Redirect::to("/login")
|
||||
.respond_to(req)
|
||||
.map_into_boxed_body()
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user