1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-21 23:17:45 +00:00

Add barebones account management page

This also moves the login pages to the login server, which makes the
code slightly a bit more contained. The account management page doesn't
allow you to do anything yet, but for future usage.
This commit is contained in:
Joshua Goins 2025-04-05 22:40:44 -04:00
parent 90e5e191e9
commit 09c347178c
9 changed files with 229 additions and 27 deletions

93
Cargo.lock generated
View file

@ -96,6 +96,29 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.74" version = "0.3.74"
@ -220,6 +243,17 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -272,6 +306,15 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.4.1" version = "1.4.1"
@ -807,6 +850,7 @@ name = "kawari"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra",
"binrw", "binrw",
"bitflags 1.3.2", "bitflags 1.3.2",
"md5", "md5",
@ -1024,6 +1068,12 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1151,6 +1201,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -1633,6 +1689,37 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@ -1813,6 +1900,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"

View file

@ -50,6 +50,7 @@ serde_json = { version = "1.0", features = ["std"], default-features = false }
[dependencies] [dependencies]
# Used for the web servers # Used for the web servers
axum = { version = "0.8", features = ["json", "tokio", "http1", "form", "query"], default-features = false } axum = { version = "0.8", features = ["json", "tokio", "http1", "form", "query"], default-features = false }
axum-extra = { version = "0.10", features = ["cookie"], default-features = false }
# Serialization used in almost every server # Serialization used in almost every server
serde = { version = "1.0", features = ["derive"], default-features = false } serde = { version = "1.0", features = ["derive"], default-features = false }

View file

@ -4,10 +4,28 @@ use axum::extract::{Query, State};
use axum::response::{Html, Redirect}; use axum::response::{Html, Redirect};
use axum::routing::post; use axum::routing::post;
use axum::{Form, Router, routing::get}; 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::config::get_config;
use kawari::login::{LoginDatabase, LoginError}; use kawari::login::{LoginDatabase, LoginError};
use minijinja::{Environment, context};
use serde::Deserialize; 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
}
#[derive(Clone)] #[derive(Clone)]
struct LoginServerState { struct LoginServerState {
database: Arc<LoginDatabase>, database: Arc<LoginDatabase>,
@ -109,6 +127,72 @@ async fn check_session(
serde_json::to_string(&accounts).unwrap_or(String::new()) serde_json::to_string(&accounts).unwrap_or(String::new())
} }
async fn login() -> Html<String> {
let environment = setup_default_environment();
let template = environment.get_template("login.html").unwrap();
Html(template.render(context! {}).unwrap())
}
async fn register() -> Html<String> {
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<String>,
password: Option<String>,
}
async fn do_login(
State(state): State<LoginServerState>,
jar: CookieJar,
Form(input): Form<LoginInput>,
) -> (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<LoginServerState>, jar: CookieJar) -> Html<String> {
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(State(state): State<LoginServerState>, 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)),
)
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@ -118,11 +202,19 @@ async fn main() {
}; };
let app = Router::new() let app = Router::new()
// retail API
.route("/oauth/ffxivarr/login/top", get(top)) .route("/oauth/ffxivarr/login/top", get(top))
.route("/oauth/ffxivarr/login/login.send", post(login_send)) .route("/oauth/ffxivarr/login/login.send", post(login_send))
.route("/register", post(do_register)) // private server<->server API
// TODO: make these actually private // TODO: make these actually private
.route("/_private/service_accounts", get(check_session)) .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))
.with_state(state); .with_state(state);
let config = get_config(); let config = get_config();

View file

@ -9,18 +9,13 @@ fn setup_default_environment() -> Environment<'static> {
let mut env = Environment::new(); let mut env = Environment::new();
env.add_template("web.html", include_str!("../../templates/web.html")) env.add_template("web.html", include_str!("../../templates/web.html"))
.unwrap(); .unwrap();
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( env.add_template(
"worldstatus.html", "worldstatus.html",
include_str!("../../templates/worldstatus.html"), include_str!("../../templates/worldstatus.html"),
) )
.unwrap(); .unwrap();
env.add_template("account.html", include_str!("../../templates/account.html"))
.unwrap();
env env
} }
@ -31,21 +26,15 @@ struct GateStatus {
} }
async fn root() -> Html<String> { async fn root() -> Html<String> {
let config = get_config();
let environment = setup_default_environment(); let environment = setup_default_environment();
let template = environment.get_template("web.html").unwrap(); let template = environment.get_template("web.html").unwrap();
Html(template.render(context! {}).unwrap()) Html(
} template
.render(context! { login_server => config.login.server_name })
async fn login() -> Html<String> { .unwrap(),
let environment = setup_default_environment(); )
let template = environment.get_template("login.html").unwrap();
Html(template.render(context! {}).unwrap())
}
async fn register() -> Html<String> {
let environment = setup_default_environment();
let template = environment.get_template("register.html").unwrap();
Html(template.render(context! {}).unwrap())
} }
async fn world_status() -> Html<String> { async fn world_status() -> Html<String> {
@ -66,8 +55,6 @@ async fn main() {
let app = Router::new() let app = Router::new()
.route("/", get(root)) .route("/", get(root))
.route("/login", get(login))
.route("/register", get(register))
.route("/worldstatus", get(world_status)); .route("/worldstatus", get(world_status));
let config = get_config(); let config = get_config();

View file

@ -92,6 +92,8 @@ impl LobbyConfig {
pub struct LoginConfig { pub struct LoginConfig {
pub port: u16, pub port: u16,
pub listen_address: String, pub listen_address: String,
/// Public-facing domain of the server.
pub server_name: String,
} }
impl Default for LoginConfig { impl Default for LoginConfig {
@ -99,6 +101,7 @@ impl Default for LoginConfig {
Self { Self {
port: 6700, port: 6700,
listen_address: "127.0.0.1".to_string(), listen_address: "127.0.0.1".to_string(),
server_name: "ffxiv-login.square.localhost".to_string(),
} }
} }
} }
@ -157,6 +160,8 @@ impl PatchConfig {
pub struct WebConfig { pub struct WebConfig {
pub port: u16, pub port: u16,
pub listen_address: String, pub listen_address: String,
/// Public-facing domain of the server.
pub server_name: String,
} }
impl Default for WebConfig { impl Default for WebConfig {
@ -164,6 +169,7 @@ impl Default for WebConfig {
Self { Self {
port: 5801, port: 5801,
listen_address: "127.0.0.1".to_string(), listen_address: "127.0.0.1".to_string(),
server_name: "ffxiv.localhost".to_string(),
} }
} }
} }

View file

@ -10,6 +10,7 @@ pub struct LoginDatabase {
connection: Mutex<Connection>, connection: Mutex<Connection>,
} }
#[derive(Debug)]
pub enum LoginError { pub enum LoginError {
WrongUsername, WrongUsername,
WrongPassword, WrongPassword,
@ -190,4 +191,24 @@ impl LoginDatabase {
selected_row.is_ok() selected_row.is_ok()
} }
pub fn get_user_id(&self, sid: &str) -> u32 {
let connection = self.connection.lock().unwrap();
let mut stmt = connection
.prepare("SELECT user_id FROM sessions WHERE sid = ?1")
.ok()
.unwrap();
stmt.query_row((sid,), |row| Ok(row.get(0)?)).unwrap()
}
pub fn get_username(&self, user_id: u32) -> String {
let connection = self.connection.lock().unwrap();
let mut stmt = connection
.prepare("SELECT username FROM users WHERE id = ?1")
.ok()
.unwrap();
stmt.query_row((user_id,), |row| Ok(row.get(0)?)).unwrap()
}
} }

2
templates/account.html Normal file
View file

@ -0,0 +1,2 @@
<p>Managing account {{ username }}</p>
<a href="/account/app/svc/logout">Logout</a>

View file

@ -6,7 +6,7 @@
</head> </head>
<body> <body>
<form action='http://ffxiv-login.square.localhost/register' method='post'> <form action='/oauth/oa/registlist' method='post'>
<label for="username">Username:</label><br> <label for="username">Username:</label><br>
<input type='text' id='username' name='username'/><br> <input type='text' id='username' name='username'/><br>
<label for="password">Password:</label><br> <label for="password">Password:</label><br>

View file

@ -8,8 +8,8 @@
<p>Welcome to Kawari!</p> <p>Welcome to Kawari!</p>
<a href="/login">Login</a> <a href="http://{{ login_server }}/oauth/oa/oauthlogin">Login</a>
<a href="/register">Signup</a> <a href="http://{{ login_server }}/oauth/oa/registligt">Signup</a>
<a href="/worldstatus">World Status</a> <a href="/worldstatus">World Status</a>
</body> </body>