mirror of
https://github.com/redstrate/Kawari.git
synced 2025-05-06 04:37:46 +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",
|
||||
"memchr",
|
||||
"mime",
|
||||
"multer",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
|
@ -321,6 +322,15 @@ version = "1.15.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
|
@ -937,6 +947,23 @@ dependencies = [
|
|||
"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]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
|
@ -1335,6 +1362,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
|
|
|
@ -52,7 +52,7 @@ serde_json = { version = "1.0", features = ["std"], default-features = false }
|
|||
|
||||
[dependencies]
|
||||
# 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 }
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::{Multipart, 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::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
|
||||
use kawari::config::get_config;
|
||||
use kawari::lobby::send_custom_world_packet;
|
||||
use kawari::login::{LoginDatabase, LoginError};
|
||||
use minijinja::{Environment, context};
|
||||
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) {
|
||||
let config = get_config();
|
||||
// TODO: remove session from database
|
||||
|
@ -231,6 +269,7 @@ async fn main() {
|
|||
.route("/oauth/oa/registligt", get(register))
|
||||
.route("/oauth/oa/registlist", post(do_register))
|
||||
.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/mbrPasswd", get(change_password))
|
||||
.route("/account/app/svc/mbrCancel", get(cancel_account))
|
||||
|
|
|
@ -1005,6 +1005,9 @@ async fn client_loop(
|
|||
.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!")
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ impl ReadWriteIpcSegment for CustomIpcSegment {
|
|||
CustomIpcType::RequestCharacterListRepsonse => 1 + (1184 * 8),
|
||||
CustomIpcType::DeleteCharacter => 4,
|
||||
CustomIpcType::CharacterDeleted => 1,
|
||||
CustomIpcType::ImportCharacter => 132,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +54,8 @@ pub enum CustomIpcType {
|
|||
DeleteCharacter = 0x9,
|
||||
/// Response to DeleteCharacter
|
||||
CharacterDeleted = 0x10,
|
||||
/// Request to import a character backup
|
||||
ImportCharacter = 0x11,
|
||||
}
|
||||
|
||||
#[binrw]
|
||||
|
@ -107,6 +110,15 @@ pub enum CustomIpcData {
|
|||
DeleteCharacter { content_id: u64 },
|
||||
#[br(pre_assert(*magic == CustomIpcType::CharacterDeleted))]
|
||||
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 {
|
||||
|
|
|
@ -209,4 +209,15 @@ impl LoginDatabase {
|
|||
.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),
|
||||
};
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
fn import_character(&self, path: &str) {
|
||||
tracing::info!("Importing character backup {path}...");
|
||||
pub fn import_character(&self, service_account_id: u32, path: &str) {
|
||||
tracing::info!("Importing character backup from {path}...");
|
||||
|
||||
let file = std::fs::File::open(path).unwrap();
|
||||
|
||||
|
@ -105,7 +92,8 @@ impl WorldDatabase {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -127,7 +115,7 @@ impl WorldDatabase {
|
|||
|
||||
// TODO: import inventory
|
||||
self.create_player_data(
|
||||
0x1,
|
||||
service_account_id,
|
||||
&character.name,
|
||||
&chara_make.to_json(),
|
||||
character.city_state.value as u8,
|
||||
|
|
|
@ -2,3 +2,10 @@
|
|||
<a href="/account/app/svc/logout">Logout</a>
|
||||
<a href="/account/app/svc/mbrPasswd">Change Password</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