use std::sync::Arc; use axum::extract::{Query, State}; use axum::response::{Html, Redirect}; use axum::routing::post; use axum::{Form, Router, routing::get}; use axum_extra::extract::CookieJar; use axum_extra::extract::cookie::{Cookie, Expiration}; use kawari::config::get_config; use kawari::login::{LoginDatabase, LoginError}; use minijinja::{Environment, context}; use serde::Deserialize; fn setup_default_environment() -> Environment<'static> { let mut env = Environment::new(); env.add_template("login.html", include_str!("../../templates/login.html")) .unwrap(); env.add_template( "register.html", include_str!("../../templates/register.html"), ) .unwrap(); env.add_template("account.html", include_str!("../../templates/account.html")) .unwrap(); env.add_template( "changepassword.html", include_str!("../../templates/changepassword.html"), ) .unwrap(); env } #[derive(Clone)] struct LoginServerState { database: Arc, } #[derive(Deserialize)] #[allow(dead_code)] struct Params { lng: String, rgn: String, isft: String, cssmode: String, isnew: String, launchver: String, } async fn top(Query(_): Query) -> Html<&'static str> { Html( "\r\n\r\n\r\n\r\n
\r\n\t\r\n\t\t\r\n\t\t\n\r\n\t\t\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\t\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\r\n\t\t\r\n\t\t
\r\n\t\t\t\r\n\t\t\t\r\n\t\t
\r\n\t\t\r\n\r\n\t\t
\r\n\t\t\t\r\n\t\t
\r\n\r\n\t
\r\n\r\n\r\n\r\n\r\n", ) } #[derive(Deserialize, Debug)] #[allow(dead_code, non_snake_case)] struct Input { _STORED_: String, sqexid: String, password: String, otppw: String, } async fn login_send( State(state): State, Form(input): Form, ) -> Html { let user = state.database.login_user(&input.sqexid, &input.password); match user { Ok(session_id) => Html(format!( "window.external.user(\"login=auth,ok,sid,{session_id},terms,1,region,2,etmadd,0,playable,1,ps3pkg,0,maxex,5,product,1\");" )), Err(err) => { // TODO: see what the official error messages are match err { LoginError::WrongUsername => { Html("window.external.user(\"login=auth,ng,err,Wrong Username\");".to_string()) } LoginError::WrongPassword => { Html("window.external.user(\"login=auth,ng,err,Wrong Password\");".to_string()) } LoginError::InternalError => Html( "window.external.user(\"login=auth,ng,err,Internal Server Error\");" .to_string(), ), } } } } #[derive(Deserialize, Debug)] #[allow(dead_code)] struct RegisterInput { username: Option, password: Option, } async fn do_register( State(state): State, Form(input): Form, ) -> Redirect { tracing::info!( "Registering with {:#?} and {:#?}!", input.username, input.password ); let Some(username) = input.username else { panic!("Expected username!"); }; let Some(password) = input.password else { panic!("Expected password!"); }; state.database.add_user(&username, &password); Redirect::to("/") } #[derive(Deserialize)] #[allow(dead_code)] struct CheckSessionParams { sid: String, } async fn check_session( State(state): State, Query(params): Query, ) -> String { let accounts = state.database.check_session(¶ms.sid); serde_json::to_string(&accounts).unwrap_or(String::new()) } async fn login() -> Html { let environment = setup_default_environment(); let template = environment.get_template("login.html").unwrap(); Html(template.render(context! {}).unwrap()) } async fn register() -> Html { let environment = setup_default_environment(); let template = environment.get_template("register.html").unwrap(); Html(template.render(context! {}).unwrap()) } #[derive(Deserialize, Debug)] #[allow(dead_code)] struct LoginInput { username: Option, password: Option, } async fn do_login( State(state): State, jar: CookieJar, Form(input): Form, ) -> (CookieJar, Redirect) { tracing::info!("{:#?} logging in!", input.username,); let Some(username) = input.username else { panic!("Expected username!"); }; let Some(password) = input.password else { panic!("Expected password!"); }; let sid = state.database.login_user(&username, &password).unwrap(); let cookie = Cookie::build(("cis_sessid", sid)) .path("/") .secure(false) .expires(Expiration::Session) .http_only(true); (jar.add(cookie), Redirect::to("/account/app/svc/manage")) } async fn account(State(state): State, jar: CookieJar) -> Html { if let Some(session_id) = jar.get("cis_sessid") { let user_id = state.database.get_user_id(session_id.value()); let username = state.database.get_username(user_id); let environment = setup_default_environment(); let template = environment.get_template("account.html").unwrap(); Html(template.render(context! { username => username }).unwrap()) } else { Html("You need to be logged in!".to_string()) } } async fn logout(jar: CookieJar) -> (CookieJar, Redirect) { let config = get_config(); // TODO: remove session from database ( jar.remove("cis_sessid"), Redirect::to(&format!("http://{}/", config.web.server_name)), ) } async fn change_password() -> Html { // TODO: actually change password let environment = setup_default_environment(); let template = environment.get_template("changepassword.html").unwrap(); Html(template.render(context! {}).unwrap()) } async fn cancel_account(jar: CookieJar) -> (CookieJar, Redirect) { // TODO: actually delete account (jar.remove("cis_sessid"), Redirect::to("/")) } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let state = LoginServerState { database: Arc::new(LoginDatabase::new()), }; let app = Router::new() // retail API .route("/oauth/ffxivarr/login/top", get(top)) .route("/oauth/ffxivarr/login/login.send", post(login_send)) // private server<->server API // TODO: make these actually private .route("/_private/service_accounts", get(check_session)) // public website .route("/oauth/oa/oauthlogin", get(login)) .route("/oauth/oa/oauthlogin", post(do_login)) .route("/oauth/oa/registligt", get(register)) .route("/oauth/oa/registlist", post(do_register)) .route("/account/app/svc/manage", get(account)) .route("/account/app/svc/logout", get(logout)) .route("/account/app/svc/mbrPasswd", get(change_password)) .route("/account/app/svc/mbrCancel", get(cancel_account)) .with_state(state); let config = get_config(); let addr = config.login.get_socketaddr(); tracing::info!("Server started on {addr}"); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); }