mirror of
https://github.com/redstrate/Kawari.git
synced 2025-05-05 20:27:45 +00:00
Add "character backup import" feature to the account management page
We have had an import feature for a while, allowing you to easily recreate your retail character from Auracite backups. But the feature was implemented *before* we had proper service accounts, and it always assigned it to ID 1. Now it's moved to the user-visible account management page.
This commit is contained in:
parent
ed8ccb86ee
commit
afed151488
9 changed files with 113 additions and 20 deletions
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -57,6 +57,7 @@ dependencies = [
|
||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
|
"multer",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
|
@ -321,6 +322,15 @@ version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_home"
|
name = "env_home"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -937,6 +947,23 @@ dependencies = [
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multer"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"spin",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1335,6 +1362,12 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
|
@ -52,7 +52,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", "multipart"], default-features = false }
|
||||||
axum-extra = { version = "0.10", features = ["cookie"], 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
|
||||||
|
|
2
USAGE.md
2
USAGE.md
|
@ -74,7 +74,7 @@ Some other launchers (like XIVLauncher) will allow you to specify these extra ar
|
||||||
|
|
||||||
## Importing characters from retail
|
## Importing characters from retail
|
||||||
|
|
||||||
It's possible to import existing characters from the retail server using [Auracite](https://auracite.xiv.zone). Place the backup ZIP under `backups/` and the World server will import it into the database the next time it's started.
|
It's possible to import existing characters from the retail server using [Auracite](https://auracite.xiv.zone). Upload the backup ZIP on the account management page on the login server.
|
||||||
|
|
||||||
This feature is still a work-in-progress, and not all data is imported yet.
|
This feature is still a work-in-progress, and not all data is imported yet.
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Multipart, 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::CookieJar;
|
||||||
use axum_extra::extract::cookie::{Cookie, Expiration};
|
use axum_extra::extract::cookie::{Cookie, Expiration};
|
||||||
|
use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
|
||||||
use kawari::config::get_config;
|
use kawari::config::get_config;
|
||||||
|
use kawari::lobby::send_custom_world_packet;
|
||||||
use kawari::login::{LoginDatabase, LoginError};
|
use kawari::login::{LoginDatabase, LoginError};
|
||||||
use minijinja::{Environment, context};
|
use minijinja::{Environment, context};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -189,6 +191,42 @@ async fn account(State(state): State<LoginServerState>, jar: CookieJar) -> Html<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn upload_character_backup(
|
||||||
|
State(state): State<LoginServerState>,
|
||||||
|
jar: CookieJar,
|
||||||
|
mut multipart: Multipart,
|
||||||
|
) -> Redirect {
|
||||||
|
if let Some(session_id) = jar.get("cis_sessid") {
|
||||||
|
let user_id = state.database.get_user_id(session_id.value());
|
||||||
|
let service_account_id = state.database.get_service_account(user_id);
|
||||||
|
|
||||||
|
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||||
|
let name = field.name().unwrap().to_string();
|
||||||
|
let data = field.bytes().await.unwrap();
|
||||||
|
|
||||||
|
std::fs::write("temp.zip", data).unwrap();
|
||||||
|
|
||||||
|
if name == "charbak" {
|
||||||
|
let ipc_segment = CustomIpcSegment {
|
||||||
|
unk1: 0,
|
||||||
|
unk2: 0,
|
||||||
|
op_code: CustomIpcType::ImportCharacter,
|
||||||
|
server_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
data: CustomIpcData::ImportCharacter {
|
||||||
|
service_account_id,
|
||||||
|
path: "temp.zip".to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
send_custom_world_packet(ipc_segment).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Redirect::to("/account/app/svc/manage")
|
||||||
|
}
|
||||||
|
|
||||||
async fn logout(jar: CookieJar) -> (CookieJar, Redirect) {
|
async fn logout(jar: CookieJar) -> (CookieJar, Redirect) {
|
||||||
let config = get_config();
|
let config = get_config();
|
||||||
// TODO: remove session from database
|
// TODO: remove session from database
|
||||||
|
@ -231,6 +269,7 @@ async fn main() {
|
||||||
.route("/oauth/oa/registligt", get(register))
|
.route("/oauth/oa/registligt", get(register))
|
||||||
.route("/oauth/oa/registlist", post(do_register))
|
.route("/oauth/oa/registlist", post(do_register))
|
||||||
.route("/account/app/svc/manage", get(account))
|
.route("/account/app/svc/manage", get(account))
|
||||||
|
.route("/account/app/svc/manage", post(upload_character_backup))
|
||||||
.route("/account/app/svc/logout", get(logout))
|
.route("/account/app/svc/logout", get(logout))
|
||||||
.route("/account/app/svc/mbrPasswd", get(change_password))
|
.route("/account/app/svc/mbrPasswd", get(change_password))
|
||||||
.route("/account/app/svc/mbrCancel", get(cancel_account))
|
.route("/account/app/svc/mbrCancel", get(cancel_account))
|
||||||
|
|
|
@ -1005,6 +1005,9 @@ async fn client_loop(
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CustomIpcData::ImportCharacter { service_account_id, path } => {
|
||||||
|
database.import_character(*service_account_id, path);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
panic!("The server is recieving a response or unknown custom IPC!")
|
panic!("The server is recieving a response or unknown custom IPC!")
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ impl ReadWriteIpcSegment for CustomIpcSegment {
|
||||||
CustomIpcType::RequestCharacterListRepsonse => 1 + (1184 * 8),
|
CustomIpcType::RequestCharacterListRepsonse => 1 + (1184 * 8),
|
||||||
CustomIpcType::DeleteCharacter => 4,
|
CustomIpcType::DeleteCharacter => 4,
|
||||||
CustomIpcType::CharacterDeleted => 1,
|
CustomIpcType::CharacterDeleted => 1,
|
||||||
|
CustomIpcType::ImportCharacter => 132,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +54,8 @@ pub enum CustomIpcType {
|
||||||
DeleteCharacter = 0x9,
|
DeleteCharacter = 0x9,
|
||||||
/// Response to DeleteCharacter
|
/// Response to DeleteCharacter
|
||||||
CharacterDeleted = 0x10,
|
CharacterDeleted = 0x10,
|
||||||
|
/// Request to import a character backup
|
||||||
|
ImportCharacter = 0x11,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[binrw]
|
#[binrw]
|
||||||
|
@ -107,6 +110,15 @@ pub enum CustomIpcData {
|
||||||
DeleteCharacter { content_id: u64 },
|
DeleteCharacter { content_id: u64 },
|
||||||
#[br(pre_assert(*magic == CustomIpcType::CharacterDeleted))]
|
#[br(pre_assert(*magic == CustomIpcType::CharacterDeleted))]
|
||||||
CharacterDeleted { deleted: u8 },
|
CharacterDeleted { deleted: u8 },
|
||||||
|
#[br(pre_assert(*magic == CustomIpcType::ImportCharacter))]
|
||||||
|
ImportCharacter {
|
||||||
|
service_account_id: u32,
|
||||||
|
#[bw(pad_size_to = 128)]
|
||||||
|
#[br(count = 128)]
|
||||||
|
#[br(map = read_string)]
|
||||||
|
#[bw(map = write_string)]
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CustomIpcData {
|
impl Default for CustomIpcData {
|
||||||
|
|
|
@ -209,4 +209,15 @@ impl LoginDatabase {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
stmt.query_row((user_id,), |row| row.get(0)).unwrap()
|
stmt.query_row((user_id,), |row| row.get(0)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TODO: only works for one
|
||||||
|
pub fn get_service_account(&self, user_id: u32) -> u32 {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT id FROM service_accounts WHERE user_id = ?1")
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_row((user_id,), |row| row.get(0)).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,24 +51,11 @@ impl WorldDatabase {
|
||||||
connection: Mutex::new(connection),
|
connection: Mutex::new(connection),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Import any backups
|
|
||||||
// NOTE: This won't make sense when service accounts are a real thing, so the functionality will probably be moved
|
|
||||||
{
|
|
||||||
if let Ok(paths) = std::fs::read_dir("./backups") {
|
|
||||||
for path in paths {
|
|
||||||
let path = path.unwrap().path();
|
|
||||||
if path.extension().unwrap() == "zip" {
|
|
||||||
this.import_character(path.to_str().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn import_character(&self, path: &str) {
|
pub fn import_character(&self, service_account_id: u32, path: &str) {
|
||||||
tracing::info!("Importing character backup {path}...");
|
tracing::info!("Importing character backup from {path}...");
|
||||||
|
|
||||||
let file = std::fs::File::open(path).unwrap();
|
let file = std::fs::File::open(path).unwrap();
|
||||||
|
|
||||||
|
@ -105,7 +92,8 @@ impl WorldDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.check_is_name_free(&character.name) {
|
if !self.check_is_name_free(&character.name) {
|
||||||
tracing::warn!("* Skipping since this character already exists.");
|
let name = character.name;
|
||||||
|
tracing::warn!("* Skipping since {name} already exists.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +115,7 @@ impl WorldDatabase {
|
||||||
|
|
||||||
// TODO: import inventory
|
// TODO: import inventory
|
||||||
self.create_player_data(
|
self.create_player_data(
|
||||||
0x1,
|
service_account_id,
|
||||||
&character.name,
|
&character.name,
|
||||||
&chara_make.to_json(),
|
&chara_make.to_json(),
|
||||||
character.city_state.value as u8,
|
character.city_state.value as u8,
|
||||||
|
|
|
@ -2,3 +2,10 @@
|
||||||
<a href="/account/app/svc/logout">Logout</a>
|
<a href="/account/app/svc/logout">Logout</a>
|
||||||
<a href="/account/app/svc/mbrPasswd">Change Password</a>
|
<a href="/account/app/svc/mbrPasswd">Change Password</a>
|
||||||
<a href="/account/app/svc/mbrCancel">Cancel Account</a>
|
<a href="/account/app/svc/mbrCancel">Cancel Account</a>
|
||||||
|
|
||||||
|
<!-- TODO: Move to it's own page. -->
|
||||||
|
<form method='post' enctype="multipart/form-data">
|
||||||
|
<label for="charbak">Upload character backup:</label>
|
||||||
|
<input type="file" id="charbak" name="charbak" accept="application/zip" />
|
||||||
|
<button type='submit'>Upload</button>
|
||||||
|
</form>
|
||||||
|
|
Loading…
Add table
Reference in a new issue