1
0
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:
NG (Graham)
2026-05-02 11:03:49 -04:00
parent e09aeceed3
commit e3183eb238
51 changed files with 1424 additions and 77 deletions

View 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()
)
}
}
}

View 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
}

View 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()
)
}
}

View 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
View 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()
))
}
}