Compare commits

11 Commits

Author SHA1 Message Date
Ethan O'Brien
86e1e6050a Bump version to 1.1.0 2026-02-19 14:32:11 -06:00
Ethan O'Brien
956634d8ab Add ability to host webui card image files 2026-02-19 14:04:31 -06:00
Ethan O'Brien
8a1d33bd2b Downgrade jni for now 2026-02-19 12:20:08 -06:00
Ethan O'Brien
aef2c4986d Upgrade usage of JNI 2026-02-19 11:09:55 -06:00
Ethan O'Brien
b35aa35b1a Upgrade dependencies 2026-02-19 10:33:15 -06:00
Ethan O'Brien
0ef0a83685 Remove nodejs from build 2026-02-19 10:18:30 -06:00
Ethan O'Brien
586f4612ed Update readme 2026-02-19 10:18:19 -06:00
Ethan O'Brien
f0183370be Add error message when webui submodules are not pulled 2026-02-19 09:59:16 -06:00
Ethan O'Brien
f4fe727a0e New webui 2026-02-19 09:51:51 -06:00
Ethan O'Brien
d61b27af8f Add coins/primogems and item limits 2026-02-10 14:51:51 -06:00
Ethan O'Brien
8e4300a7ea Fix cheat endpoint and jp client 2026-02-10 14:35:19 -06:00
38 changed files with 644 additions and 5727 deletions

View File

@@ -18,11 +18,6 @@ jobs:
shell: bash shell: bash
run: curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal run: curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install android NDK - name: Install android NDK
id: setup-ndk id: setup-ndk
uses: https://github.com/nttld/setup-ndk@v1 uses: https://github.com/nttld/setup-ndk@v1
@@ -33,12 +28,6 @@ jobs:
run: | run: |
mkdir out mkdir out
- name: Build webui
run: |
cd webui
npm install
npm run build
- name: Install cargo ndk - name: Install cargo ndk
shell: bash shell: bash
run: | run: |

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "assets"] [submodule "assets"]
path = assets path = assets
url = https://git.ethanthesleepy.one/ethanaobrien/sif2-runtime-files url = https://git.ethanthesleepy.one/ethanaobrien/sif2-runtime-files
[submodule "webui"]
path = webui
url = https://git.ethanthesleepy.one/ethanaobrien/ew-webui

690
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
[package] [package]
name = "ew" name = "ew"
version = "1.0.0" version = "1.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
actix-web = "4.12" actix-web = "4.13"
rusqlite = { version = "0.37.0", features = ["bundled"] } rusqlite = { version = "0.38.0", features = ["bundled"] }
clap = { version = "4.5.37", features = ["derive"]} clap = { version = "4.5.59", features = ["derive"]}
base64 = "0.22.1" base64 = "0.22.1"
json = "0.12.4" json = "0.12.4"
rand = "0.9.2" rand = "0.10.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
hex = "0.4.3" hex = "0.4.3"
hmac = "0.12.1" hmac = "0.12.1"
@@ -17,8 +17,8 @@ md5 = "0.8.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
sha1 = "0.10.6" sha1 = "0.10.6"
substring = "1.4.5" substring = "1.4.5"
uuid = { version = "1.18.1", features = ["v7"] } uuid = { version = "1.21.0", features = ["v7"] }
rsa = { version = "0.9.9", features = ["sha1"] } rsa = { version = "0.9.10", features = ["sha1"] }
mime = "0.3.17" mime = "0.3.17"
sha2 = "0.10.9" sha2 = "0.10.9"
include-flate-codegen = "0.3.1" include-flate-codegen = "0.3.1"
@@ -26,22 +26,23 @@ libflate = "2.2.1"
cbc = { version = "0.1.2", features = ["alloc"] } cbc = { version = "0.1.2", features = ["alloc"] }
aes = "0.8.4" aes = "0.8.4"
pem = "3.0.6" pem = "3.0.6"
ureq = "3.1.4" ureq = "3.2.0"
include_dir = {version = "0.7.4", optional = true } mime_guess = "2.0.5"
include_dir = "0.7.4"
[target.aarch64-linux-android.dependencies] [target.aarch64-linux-android.dependencies]
jni = { version = "0.21.1", features = ["invocation", "default"], optional = true } jni = { version = "0.21.0", features = ["invocation", "default"], optional = true }
[target.aarch64-apple-ios.dependencies] [target.aarch64-apple-ios.dependencies]
objc2 = { version = "0.6.3", optional = true } objc2 = { version = "0.6.3", optional = true }
objc2-foundation = { version = "0.3.2", features = ["NSFileManager"], optional = true } objc2-foundation = { version = "0.3.2", features = ["NSFileManager"], optional = true }
[build-dependencies] [build-dependencies]
cc = "1.0" cc = "1.2"
# To enable this library you MUST comment out lib block below and add --features library # To enable this library you MUST comment out lib block below and add --features library
[features] [features]
library = ["jni", "objc2", "objc2-foundation", "include_dir"] library = ["jni", "objc2", "objc2-foundation"]
#[lib] #[lib]
#crate-type = ["cdylib"] #crate-type = ["cdylib"]

View File

@@ -9,4 +9,8 @@ fn main() {
.compile("libc_code.a"); .compile("libc_code.a");
println!("cargo:rerun-if-changed=src/log.c"); println!("cargo:rerun-if-changed=src/log.c");
} }
if !std::fs::exists("webui/index.html").unwrap_or(false) {
panic!("Could not compile crate! Missing webui! Did you pull submodules?");
}
} }

View File

@@ -3,18 +3,12 @@ FROM docker.io/library/debian:latest AS builder
# First - build # First - build
RUN apt update && apt install -y curl libssl-dev perl git gcc make RUN apt update && apt install -y curl libssl-dev perl git gcc make
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash
RUN apt update && apt install -y nodejs
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
WORKDIR /ew/ WORKDIR /ew/
COPY ./ ./ COPY ./ ./
WORKDIR /ew/webui/
RUN npm i && npm run build
WORKDIR /ew/ WORKDIR /ew/
RUN . "$HOME/.cargo/env" && cargo build --release RUN . "$HOME/.cargo/env" && cargo build --release

View File

@@ -1,4 +1,3 @@
version: '3'
services: services:
sif2-ew: sif2-ew:
image: sif2-ew:latest image: sif2-ew:latest
@@ -14,6 +13,7 @@ services:
HIDDEN: false # Will disable the webui HIDDEN: false # Will disable the webui
DISABLE_IMPORTS: false # Will disable account imports DISABLE_IMPORTS: false # Will disable account imports
DISABLE_EXPORTS: false # Will disable account exports DISABLE_EXPORTS: false # Will disable account exports
#IMAGE_ASSET_PATH: /images/ # Images for cards in webui (will default to the public server)
# Everything below is for the "Help" page # Everything below is for the "Help" page
#ANDROID_GLOBAL: "link.to/patched/android/global.apk" #ANDROID_GLOBAL: "link.to/patched/android/global.apk"
@@ -25,4 +25,7 @@ services:
- 8080:8080 - 8080:8080
volumes: volumes:
- ./data:/data - ./data:/data
# You can download from https://git.ethanthesleepy.one/ethanaobrien/ew-webui/releases/download/images/card-thumbnails.zip
# No, you should not have a nested folder EXAMPLE: `images/FILENAME.webp`
#- ./images:/images
restart: unless-stopped restart: unless-stopped

View File

@@ -25,4 +25,6 @@ asset_android_en=$([ "$EN_ANDROID_ASSET_HASH" != "" ] && echo "--en-android-asse
asset_ios_en=$([ "$EN_IOS_ASSET_HASH" != "" ] && echo "--en-ios-asset-hash $EN_IOS_ASSET_HASH" || echo "") asset_ios_en=$([ "$EN_IOS_ASSET_HASH" != "" ] && echo "--en-ios-asset-hash $EN_IOS_ASSET_HASH" || echo "")
/root/ew/ew --path $directory --port $port --npps4 $npps4 $asset_android_jp $asset_ios_jp $asset_android_en $asset_ios_en $exports $imports $purge $hidden $https --global-android "$ANDROID_GLOBAL" --japan-android "$ANDROID_JAPAN" --global-ios "$IOS_GLOBAL" --japan-ios "$IOS_JAPAN" --assets-url "$ASSET_URL" --max-time $maxTime image_asset_path=$([ "$IMAGE_ASSET_PATH" != "" ] && echo "--image-asset-path $IMAGE_ASSET_PATH" || echo "")
/root/ew/ew --path $directory --port $port --npps4 $npps4 $asset_android_jp $asset_ios_jp $asset_android_en $asset_ios_en $exports $imports $purge $hidden $https $image_asset_path --global-android "$ANDROID_GLOBAL" --japan-android "$ANDROID_JAPAN" --global-ios "$IOS_GLOBAL" --japan-ios "$IOS_JAPAN" --assets-url "$ASSET_URL" --max-time $maxTime

View File

@@ -5,27 +5,15 @@ A (mostly functioning) server for Love Live! School idol festival 2 MIRACLE LIVE
## Building ## Building
### Linux ### Requirements
#### Requirements
- [perl](https://www.perl.org/get.html) (This is normally pre-installed)
- [rust](https://www.rust-lang.org/tools/install)
- [npm](https://www.npmjs.com/)
- The [libssl-dev](https://packages.debian.org/buster/libssl-dev) package. This will vary across distros.
`apt install -y npm libssl-dev perl`
### Windows
#### Requirements
- [Strawberry Perl](https://strawberryperl.com/)
- [rust](https://www.rust-lang.org/tools/install) - [rust](https://www.rust-lang.org/tools/install)
### Packaging/Running ### Packaging/Running
**Build npm:** **Build:**
`cd webui && npm install && npm run build`
**Build Rust:**
Debug: `cargo run` Debug: `cargo run`
Release: `cargo build --release` Release: `cargo build --release`
**Docker:**
`docker build --tag ew --file docker/Dockerfile .`
Will create docker image with tag "ew"

View File

@@ -1,5 +1,5 @@
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use rand::Rng; use rand::RngExt;
use aes::cipher::BlockEncryptMut; use aes::cipher::BlockEncryptMut;
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}; use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};

View File

@@ -48,9 +48,7 @@ pub async fn run_server(in_thread: bool) -> std::io::Result<()> {
srv.call(req) srv.call(req)
}) })
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25)) .app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
.service(static_handlers::css)
.service(static_handlers::maintenance) .service(static_handlers::maintenance)
.service(static_handlers::js)
.service(static_handlers::files_jp) .service(static_handlers::files_jp)
.service(static_handlers::files_gl) .service(static_handlers::files_gl)
.default_service(web::route().to(router::request)) .default_service(web::route().to(router::request))

View File

@@ -57,7 +57,10 @@ pub struct Args {
pub en_android_asset_hash: String, pub en_android_asset_hash: String,
#[arg(long, default_value = "", help = "Asset hash for JP Android client.")] #[arg(long, default_value = "", help = "Asset hash for JP Android client.")]
pub jp_android_asset_hash: String pub jp_android_asset_hash: String,
#[arg(long, default_value = "", help = "Path to image assets.")]
pub image_asset_path: String
} }
pub fn get_args() -> Args { pub fn get_args() -> Args {

View File

@@ -1,6 +1,6 @@
use json::{JsonValue, object, array}; use json::{JsonValue, object, array};
use actix_web::HttpRequest; use actix_web::HttpRequest;
use rand::Rng; use rand::RngExt;
use crate::encryption; use crate::encryption;
use crate::include_file; use crate::include_file;

View File

@@ -1,5 +1,5 @@
use json::{array, object, JsonValue}; use json::{array, object, JsonValue};
use rand::Rng; use rand::RngExt;
use actix_web::{HttpRequest, http::header::{HeaderMap, HeaderValue}}; use actix_web::{HttpRequest, http::header::{HeaderMap, HeaderValue}};
use crate::encryption; use crate::encryption;
@@ -43,33 +43,43 @@ pub fn get_region(headers: &HeaderMap) -> bool {
} }
pub fn check_for_region(user: &mut JsonValue, headers: &HeaderMap) { pub fn check_for_region(user: &mut JsonValue, headers: &HeaderMap) {
let items = if user["data"]["updated_value_list"]["item_list"].is_empty() {user["data"]["item_list"].clone()} else {user["data"]["updated_value_list"]["item_list"].clone()}; // returns true if jp
let is_jp = get_region(headers); if !get_region(headers) {
if !is_jp || items.is_empty() {
return; return;
} }
let mut id = 0;
for (i, data) in items.members().enumerate() { let targets = [
if data["master_item_id"] == 15570008 { 15570001, 15570002, 15570003, 15570004,
id = i + 1; 15570005, 15570006, 15570007, 15570008
break; ];
}
} let list = if !user["data"]["updated_value_list"]["item_list"].is_empty() {
if id > 0 { &mut user["data"]["updated_value_list"]["item_list"]
if user["data"]["updated_value_list"]["item_list"].is_empty() { } else {
user["data"]["item_list"].array_remove(id - 1); &mut user["data"]["item_list"]
} else { };
user["data"]["updated_value_list"]["item_list"].array_remove(id - 1);
} let mut to_remove: Vec<usize> = list
.members()
.enumerate()
.filter(|(_, item)| {
targets.contains(&item["master_item_id"].as_i64().unwrap_or(0))
})
.map(|(i, _)| i)
.collect();
to_remove.reverse();
for i in to_remove {
list.array_remove(i);
} }
} }
// true - limit reached // true - limit reached
// false - all good // false - all good
pub const GIFT_LIMIT: usize = 100000; pub const GIFT_LIMIT: usize = 100000;
pub const LIMIT_ITEMS: i64 = 200000000; pub const LIMIT_ITEMS: i64 = 9999999;
pub const LIMIT_COINS: i64 = 2000000000; pub const LIMIT_COINS: i64 = 999999999;
pub const LIMIT_PRIMOGEMS: i64 = 1000000; pub const LIMIT_PRIMOGEMS: i64 = 9999999;
fn give(array: &mut JsonValue, shop_id: &str, master_item_id: i64, limit: i64, count_id: &str, count: i64, default_push: JsonValue) -> bool { fn give(array: &mut JsonValue, shop_id: &str, master_item_id: i64, limit: i64, count_id: &str, count: i64, default_push: JsonValue) -> bool {
for data in array.members_mut() { for data in array.members_mut() {

View File

@@ -1,6 +1,6 @@
use json::{object, array, JsonValue}; use json::{object, array, JsonValue};
use actix_web::{HttpRequest}; use actix_web::{HttpRequest};
use rand::Rng; use rand::RngExt;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::router::{global, userdata, items, databases}; use crate::router::{global, userdata, items, databases};

View File

@@ -1,6 +1,6 @@
use json::{array, object, JsonValue}; use json::{array, object, JsonValue};
use actix_web::{HttpRequest}; use actix_web::{HttpRequest};
use rand::Rng; use rand::RngExt;
use crate::router::{global, userdata, items, databases}; use crate::router::{global, userdata, items, databases};
use crate::encryption; use crate::encryption;

View File

@@ -3,7 +3,7 @@ pub mod user;
use rusqlite::params; use rusqlite::params;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use json::{JsonValue, array, object}; use json::{JsonValue, array, object};
use rand::Rng; use rand::RngExt;
use crate::router::global; use crate::router::global;
use crate::router::items; use crate::router::items;
@@ -430,7 +430,8 @@ pub fn webui_login(uid: i64, password: &str) -> Result<String, String> {
let new_token = create_webui_token(); let new_token = create_webui_token();
DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(uid)); DATABASE.lock_and_exec("DELETE FROM webui WHERE user_id=?1", params!(uid));
DATABASE.lock_and_exec("INSERT INTO webui (user_id, token, last_login) VALUES (?1, ?2, ?3)", params!(uid, new_token, global::timestamp())); // This could overflow given enough time... though rusqlite doesnt currently support ToSql on a u64...... maybe someday
DATABASE.lock_and_exec("INSERT INTO webui (user_id, token, last_login) VALUES (?1, ?2, ?3)", params!(uid, new_token, global::timestamp() as i64));
Ok(new_token) Ok(new_token)
} }

View File

@@ -1,7 +1,7 @@
use rusqlite::params; use rusqlite::params;
use json::{JsonValue, object}; use json::{JsonValue, object};
use crate::router::userdata; use crate::router::userdata;
use rand::Rng; use rand::RngExt;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};

View File

@@ -6,12 +6,12 @@ use actix_web::{
}; };
use json::{JsonValue, object}; use json::{JsonValue, object};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use include_dir::{include_dir, Dir};
use std::fs;
use crate::include_file; use crate::include_file;
use crate::router::{userdata, items}; use crate::router::{userdata, items};
//pub const FRONTEND_DOMAIN: &str = "https://sif2-api.ethanthesleepy.one";
fn get_config() -> JsonValue { fn get_config() -> JsonValue {
let args = crate::get_args(); let args = crate::get_args();
object!{ object!{
@@ -35,7 +35,6 @@ fn error(msg: &str) -> HttpResponse {
message: msg message: msg
}; };
HttpResponse::Ok() HttpResponse::Ok()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -53,8 +52,6 @@ pub fn login(_req: HttpRequest, body: String) -> HttpResponse {
}; };
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(ContentType::json()) .insert_header(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
//.insert_header(("Access-Control-Allow-Credentials", "true"))
.insert_header(("Set-Cookie", format!("ew_token={}; SameSite=Strict; HttpOnly", token.unwrap()))) .insert_header(("Set-Cookie", format!("ew_token={}; SameSite=Strict; HttpOnly", token.unwrap())))
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -79,7 +76,6 @@ pub fn import(_req: HttpRequest, body: String) -> HttpResponse {
}; };
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(ContentType::json()) .insert_header(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -101,8 +97,6 @@ pub fn user(req: HttpRequest) -> HttpResponse {
data: data data: data
}; };
HttpResponse::Ok() HttpResponse::Ok()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
//.insert_header(("Access-Control-Allow-Credentials", "true"))
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -116,8 +110,6 @@ pub fn start_loginbonus(req: HttpRequest, body: String) -> HttpResponse {
let resp = userdata::webui_start_loginbonus(body["bonus_id"].as_i64().unwrap(), &token.unwrap()); let resp = userdata::webui_start_loginbonus(body["bonus_id"].as_i64().unwrap(), &token.unwrap());
HttpResponse::Ok() HttpResponse::Ok()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
//.insert_header(("Access-Control-Allow-Credentials", "true"))
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -131,8 +123,6 @@ pub fn set_time(req: HttpRequest, body: String) -> HttpResponse {
let resp = userdata::set_server_time(body["timestamp"].as_i64().unwrap(), &token.unwrap()); let resp = userdata::set_server_time(body["timestamp"].as_i64().unwrap(), &token.unwrap());
HttpResponse::Ok() HttpResponse::Ok()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
//.insert_header(("Access-Control-Allow-Credentials", "true"))
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -146,34 +136,63 @@ pub fn logout(req: HttpRequest) -> HttpResponse {
result: "OK" result: "OK"
}; };
HttpResponse::Found() HttpResponse::Found()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
//.insert_header(("Access-Control-Allow-Credentials", "true"))
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.insert_header(("Set-Cookie", "ew_token=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT")) .insert_header(("Set-Cookie", "ew_token=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT"))
.insert_header(("Location", "/")) .insert_header(("Location", "/login.html"))
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
static WEBUI_ASSETS: Dir<'_> = include_dir!("webui/");
pub fn main(req: HttpRequest) -> HttpResponse { pub fn main(req: HttpRequest) -> HttpResponse {
if req.path() == "/" { let path = if req.path().ends_with("/") { format!("{}index.html", req.path()) } else { req.path().to_string() };
let mut chars = path.chars();
chars.next();
let path = chars.as_str();
if path == "login.html" {
let token = get_login_token(&req); let token = get_login_token(&req);
if token.is_some() { if token.is_some() {
let data = userdata::webui_get_user(&token.unwrap()); let data = userdata::webui_get_user(&token.unwrap());
if data.is_some() { if data.is_some() {
return HttpResponse::Found() return HttpResponse::Found()
.insert_header(("Location", "/home/")) .insert_header(("Location", "/account.html"))
.body(""); .body("");
} }
} }
} }
if req.path() != "/" && req.path() != "/home/" && req.path() != "/import/" && req.path() != "/help/" {
return HttpResponse::Found() if let Some(file) = WEBUI_ASSETS.get_file(&path) {
.insert_header(("Location", "/")) let body = file.contents();
.body(""); let mime = mime_guess::from_path(path).first_or_octet_stream();
return HttpResponse::Ok()
.insert_header(ContentType(mime))
.insert_header(("content-length", body.len()))
.body(body);
} else if path.starts_with("webui/images/card-thumbnails") {
let args = crate::get_args();
let file_name = path.split("/").last().unwrap_or("");
let file_path = format!("{}/{}", args.image_asset_path, file_name).replace("//", "/");
if args.image_asset_path != "" && let Ok(body) = fs::read(file_path) {
let mime = mime_guess::from_path(path).first_or_octet_stream();
return HttpResponse::Ok()
.insert_header(ContentType(mime))
.insert_header(("content-length", body.len()))
.body(body);
} else {
if args.image_asset_path != "" {
println!("File '{file_name}' was requested, but no file was found on the disk!");
}
return HttpResponse::SeeOther()
.insert_header(("location", format!("https://sif2-api.ethanthesleepy.one{}", req.path())))
.body("");
}
} }
HttpResponse::Ok()
.insert_header(ContentType::html()) HttpResponse::Found()
.body(include_file!("webui/dist/index.html")) .insert_header(("Location", "/"))
.body("")
} }
pub fn export(req: HttpRequest) -> HttpResponse { pub fn export(req: HttpRequest) -> HttpResponse {
@@ -213,7 +232,6 @@ pub fn server_info(_req: HttpRequest) -> HttpResponse {
}; };
HttpResponse::Ok() HttpResponse::Ok()
.insert_header(ContentType::json()) .insert_header(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -244,7 +262,6 @@ pub fn get_card_info(req: HttpRequest) -> HttpResponse {
return HttpResponse::Ok() return HttpResponse::Ok()
.content_type(ContentType::json()) .content_type(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(resp)); .body(json::stringify(resp));
} }
@@ -268,20 +285,20 @@ pub fn get_card_info(req: HttpRequest) -> HttpResponse {
if page_items.is_empty() { if page_items.is_empty() {
return HttpResponse::NotFound() return HttpResponse::NotFound()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.finish(); .finish();
} }
let total_pages = (total_len as f64 / max as f64).ceil() as usize; let total_pages = (total_len as f64 / max as f64).ceil() as usize;
let args = crate::get_args();
let resp = object!{ let resp = object!{
total_pages: total_pages, total_pages: total_pages,
current: page_items current: page_items,
image_path: args.image_asset_path
}; };
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::json()) .content_type(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -306,7 +323,6 @@ pub fn get_music_info(req: HttpRequest) -> HttpResponse {
if page_items.is_empty() { if page_items.is_empty() {
return HttpResponse::NotFound() return HttpResponse::NotFound()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.finish(); .finish();
} }
@@ -320,7 +336,6 @@ pub fn get_music_info(req: HttpRequest) -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::json()) .content_type(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(resp)) .body(json::stringify(resp))
} }
@@ -332,14 +347,12 @@ lazy_static! {
pub fn list_login_bonus(_req: HttpRequest) -> HttpResponse { pub fn list_login_bonus(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::json()) .content_type(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(LOGIN_BONUS.clone())) .body(json::stringify(LOGIN_BONUS.clone()))
} }
pub fn list_items(_req: HttpRequest) -> HttpResponse { pub fn list_items(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()
.content_type(ContentType::json()) .content_type(ContentType::json())
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
.body(json::stringify(ITEM.clone())) .body(json::stringify(ITEM.clone()))
} }
@@ -349,31 +362,36 @@ pub fn cheat(req: HttpRequest, _body: String) -> HttpResponse {
return error("Not logged in"); return error("Not logged in");
} }
let key = userdata::webui_login_token(&token.unwrap()); let key = userdata::webui_login_token(&token.unwrap());
if key.is_some() { if key.is_none() {
return error("Not logged in"); return error("Not logged in");
} }
let key = key.unwrap(); let key = key.unwrap();
let mut user = userdata::get_acc_home(&key); let mut user = userdata::get_acc_home(&key);
for item in ITEM.entries() { for item in ITEM.entries() {
let id = item.0.parse::<i32>().unwrap_or(0); let id = item.0.parse::<i32>().unwrap_or(0);
let data = item.1; let data = item.1;
if id == 0 { if id == 0 {
continue; continue;
} }
items::gift_item_basic(id, items::LIMIT_ITEMS, data["reward_type"].as_i32().unwrap(), "You have cheated. Here are \"gifts\"", &mut user); let reward_type = data["reward_type"].as_i32().unwrap();
let limit = if reward_type == 4 {
items::LIMIT_COINS
} else if reward_type == 1 {
items::LIMIT_PRIMOGEMS
} else {
items::LIMIT_ITEMS
};
items::gift_item_basic(id, limit, reward_type, "You have cheated. Here are \"gifts\".", &mut user);
} }
userdata::save_acc_home(&key, user); userdata::save_acc_home(&key, user);
let resp = object!{ let resp = object!{
result: "OK" result: "OK"
}; };
HttpResponse::Ok() HttpResponse::Ok()
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
//.insert_header(("Access-Control-Allow-Credentials", "true"))
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.body(json::stringify(resp)) .body(json::stringify(resp))
} }

View File

@@ -1,4 +1,14 @@
{ {
"1": {
"reward_type": 4,
"en": "Coin",
"jp": "Coin"
},
"1000": {
"reward_type": 1,
"en": "Primogem (idk what its actually called)",
"jp": "Primogem (idk what its actually called)"
},
"15010001": { "15010001": {
"reward_type": 3, "reward_type": 3,
"en": "1x Scout Ticket", "en": "1x Scout Ticket",

View File

@@ -5,20 +5,7 @@ use actix_web::{
http::header::ContentType http::header::ContentType
}; };
use std::fs; use std::fs;
use crate::include_file;
#[get("/index.css")]
async fn css(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.insert_header(ContentType(mime::TEXT_CSS))
.body(include_file!("webui/dist/index.css"))
}
#[get("/index.js")]
async fn js(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok()
.insert_header(ContentType(mime::APPLICATION_JAVASCRIPT_UTF_8))
.body(include_file!("webui/dist/index.js"))
}
#[get("/maintenance/maintenance.json")] #[get("/maintenance/maintenance.json")]
async fn maintenance(_req: HttpRequest) -> HttpResponse { async fn maintenance(_req: HttpRequest) -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()

1
webui Submodule

Submodule webui added at e5b5d3d84d

View File

@@ -1,21 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

24
webui/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EW private server</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

4554
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
{
"name": "webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-bootstrap": "^2.10.2",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"vite": "^5.2.0"
}
}

View File

@@ -1,18 +0,0 @@
const serverUrl = "";
async function api(url, body) {
try {
let options = body ? {method: "POST", body: JSON.stringify(body)} : {}
const resp = await fetch(serverUrl + url, options);
const text = await resp.text();
return JSON.parse(text);
} catch(e) {
return {
result: "ERR",
message: e.message
}
}
}
export default api;

View File

@@ -1,36 +0,0 @@
body {
background-color: #616161;
}
#home {
width: 90%;
margin: 50px auto;
background-color: #43A047;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
padding: 20px 10px;
}
#logout {
border: none;
text-align: center;
text-decoration: underline;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
float: right;
background-color: yellow;
border-radius: 30px;
padding: 5px 20px;
cursor: pointer;
}
#logout:hover {
background-color: red;
}
#error p {
color: orange;
grid-template-columns: auto auto auto;
}

View File

@@ -1,89 +0,0 @@
import { useState, useParams, useEffect } from 'react'
import './Help.css'
import Request from '../Request.jsx'
let init = false;
function Help() {
const [downloadUrl, setDownloadUrl] = useState(<div>Your server admin has no pre-patched apks to download. See the question below..</div>);
const [downloadUrliOSGL, setDownloadUrliOSGL] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-gl.ipa");
const [downloadUrliOSJP, setDownloadUrliOSJP] = useState("https://ethanthesleepy.one/public/lovelive/sif2/sif2-jp.ipa");
const [assetUrl, setAssetUrl] = useState("https://sif2.sif.moe");
useEffect(() => {
(async () => {
if (init) return;
init = true;
let resp = await Request("/api/webui/serverInfo");
if (resp.result !== "OK") {
return;
}
if (!resp.data.links) return;
if (resp.data.links.global && resp.data.links.japan) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a> or <a href={resp.data.links.global}>Global</a></div>
);
} else if (resp.data.links.global) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.global}>Global</a></div>
);
} else if (resp.data.links.japan) {
setDownloadUrl(
<div>Your server admin has a link to download! Download <a href={resp.data.links.japan}>Japan</a></div>
);
}
if (resp.data.links.assets) {
setAssetUrl(resp.data.links.assets);
}
if (!resp.data.links.ios) return;
if (resp.data.links.ios.japan) {
setDownloadUrliOSJP(resp.data.links.ios.japan);
}
if (resp.data.links.ios.global) {
setDownloadUrliOSGL(resp.data.links.ios.global);
}
})();
});
return (
<div id="home">
<h1>Help/About</h1>
<h2>What is "ew"? What is this server for?</h2>
<p>"ew" is a private server, written in Rust, for the short lived game "Love Live! School idol festival 2 MIRACLE LIVE!", a Love Live! themed mobile rhythm game.</p>
<h2>I'm just trying to play on this server, how do I install the app? (Android)</h2>
<p>{downloadUrl}</p>
<h2>My server admin has no download URLs, how do I patch the apk? (Android)</h2>
<p>You can use <a href="https://arasfon.ru/sif2/patcher/">@arasfon's sif2 apk patcher</a>. Plug and the asset url, which is "{assetUrl}", into the "Assets URL" textbox, and then the server url (Which is likely "{window.location.origin}", though this may not be the case). Select your game version, set "header format" to "Lowercase", and press patch. Once that done, use <a href="https://github.com/patrickfav/uber-apk-signer">uber-apk-signer</a> to sign the apk. Then, get it onto your phone and install!</p>
<h2>How do I install the app? (iOS)</h2>
<p>Running on iOS is much simpler than Android, thanks to triangle on the discord. You first download an ipa file for <a href={downloadUrliOSGL}>global</a> or <a href={downloadUrliOSJP}>Japan</a>, and use <a href="https://sideloadly.io/">Sideloadly</a> (or your preferred application installer) to install the app. Then open settings, navigate to the app you just installed, and input the server url (Which is likely "{window.location.origin}", though this may not be the case), and the asset url, which is "{assetUrl}". If you have any errors opening the app, make sure none of the urls in settings end with a slash (/).</p>
<h2>Help! I'm trying to open the app and it shows as "unavailable" (iOS)</h2>
<p>Do not delete it, Just re-sideload the app. This is an Apple security feature.</p>
<h2>How well does this work?</h2>
<p>Works well enough. The server itself takes up not even 20mb of storage, and it's written in rust. I personally think it's pretty well written.</p>
<h2>Could my computer/laptop run a server?</h2>
<p>Very very likely. If the platform is <a href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">supported by rust</a>, then the answer is yes! It is recommended to manually compile the project until I get the time to setup actions. <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">ew repo</a></p>
<h2>Is the server down right now? I can't connect</h2>
<p>Assuming you have just loaded this page on the server you use, then the answer is no, otherwise please contact your server admin.</p>
<h2>Do events work?</h2>
<p>Most events do not, though most should not crash the game. Star events are partially implemented. You can get your rank up, and compete with other players in a ranking table, but no rewards are currently implemented.</p>
<h2>But then, how do I get event URs?</h2>
<p>There are serial codes for several things, one of which includes all the event URs. I don't remember what does what but it is recommended to look at the serial code file to get the latest codes.</p>
<h2>Why does the game crash when I do x?</h2>
<p>This likely means something on the server is broken. If you're self hosting, please contact me via matrix. Otherwise, contact your server admin and ask them to report the issue.</p>
</div>
);
}
export default Help;

View File

@@ -1,36 +0,0 @@
body {
background-color: #616161;
}
#home {
width: 90%;
margin: 50px auto;
background-color: #43A047;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
padding: 20px 10px;
}
#logout {
border: none;
text-align: center;
text-decoration: underline;
display: inline-block;
font-size: 16px;
transition-duration: 0.4s;
float: right;
background-color: yellow;
border-radius: 30px;
padding: 5px 20px;
cursor: pointer;
}
#logout:hover {
background-color: red;
}
#error p {
color: orange;
grid-template-columns: auto auto auto;
}

View File

@@ -1,196 +0,0 @@
import { useState, useParams, useEffect } from 'react'
import './Home.css'
import Request from '../Request.jsx'
let bonusItems = [];
function getMigrationID(uid) {
return uid.toString()
.replaceAll('1', "A")
.replaceAll('2', "G")
.replaceAll('3', "W")
.replaceAll('4', "Q")
.replaceAll('5', "Y")
.replaceAll('6', "6")
.replaceAll('7', "I")
.replaceAll('8', "P")
.replaceAll('9', "U")
.replaceAll('0', "M")
+ "7";
}
function Bonus() {
const [inputValue, setInputValue] = useState('');
const error = useState("");
let itemz = [];
bonusItems.forEach((e) => {
itemz.push(e.master_login_bonus_id);
})
const [submittedItems, setSubmittedItems] = useState(itemz);
const handleSubmit = async (event) => {
event.preventDefault();
let input = parseInt(inputValue.trim());
if (isNaN(input) || submittedItems.includes(input)) return;
let resp = await Request("/api/webui/startLoginbonus", {
bonus_id: input
});
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
error[1]("");
setSubmittedItems([...submittedItems, resp.id]);
setInputValue('');
};
const handleRemoveItem = (index) => {
const updatedItems = [...submittedItems];
updatedItems.splice(index, 1);
setSubmittedItems(updatedItems);
};
// <button onClick={() => handleRemoveItem(index)}>X</button>
return (
<div>
<h2>Current login bonus list</h2>
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
<ul>
{submittedItems.map((item, index) => (
<li key={index}>
{item}
</li>
))}
</ul>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter login bonus ID"
/>
<button type="submit">Submit</button>
</form>
<p>You can find a list of available login bonus IDs <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/src/router/databases/json/login_bonus.json">here</a>. You should input the <code>id</code> field</p>
</div>
);
}
function Home() {
const [user, userdata] = useState();
const [inputValue, setInputValue] = useState('');
const [serverTime, setServerTime] = useState('');
const error = useState("");
const logout = () => {
window.location.href = "/webui/logout";
}
const downloadFile = (contents, name) => {
let a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([contents], {type: "application/json"}));
a.download = name;
a.click();
}
const expor = async (e) => {
e.preventDefault();
let resp = await Request("/api/webui/export");
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
downloadFile(resp.data.userdata, "userdata.json");
downloadFile(resp.data.userhome, "userhome.json");
downloadFile(resp.data.missions, "missions.json");
downloadFile(resp.data.sifcards, "sifcards.json");
}
const handleSubmit = async (event) => {
event.preventDefault();
let time = Math.round(new Date(inputValue.trim()).getTime() / 1000);
if (inputValue.trim() === "-1") {
time = 1711741114;
} else if (inputValue.trim() === "0") {
time = 0;
}
if (time < 0 || isNaN(time)) return;
let resp = await Request("/api/webui/set_time", {
timestamp: time
});
if (resp.result !== "OK") {
error[1](resp.message);
return;
}
error[1]("");
if (time === 0) {
setServerTime("now");
} else {
setServerTime((new Date(time * 1000)).toString());
}
setInputValue('');
};
useEffect(() => {
if (user) return;
(async () => {
let resp = await Request("/api/webui/userInfo");
if (resp.result !== "OK") {
window.location.href = "/?message=" + encodeURIComponent(resp.message);
return;
}
let user = resp.data.userdata;
bonusItems = resp.data.loginbonus.bonus_list;
/*
bonusItems = [{"master_login_bonus_id":1,"day_counts":[1,2],"event_bonus_list":[]}];
let user = {
user: {
id: 1,
rank: 3,
exp: 10,
last_login_time: 5
},
time: new Date()
}*/
if (resp.data.time === 0) {
setServerTime("now");
} else {
setServerTime((new Date(resp.data.time * 1000)).toString());
}
userdata(
<div>
<p>User id: { user.user.id } </p>
<p>Migration id: { getMigrationID(user.user.id) } </p>
<p>Rank: { user.user.rank } ({ user.user.exp } exp)</p>
<p>Last Login: { (new Date(user.user.last_login_time * 1000)).toString() } </p>
<Bonus />
</div>
);
})();
});
return (
<div id="home">
<button id="logout" onClick={expor}>Export account</button><br/><br/><br/>
<button id="logout" onClick={logout}>Logout</button>
<h1>Home</h1>
{ user ? <div>
{ user }
<h2>Server time</h2>
<div id="error"> { error[0] ? <p>Error: { error[0] } </p> : <p></p> } </div>
<p>Currently set to { serverTime }. Setting to 0 will set it to now, and -1 will reset it. Time will still progress, based off of when you set this timestamp.</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
placeholder="Enter Server Time"
/>
<button type="submit">Submit</button>
</form></div>
: <p>Loading...</p> }
</div>
);
}
export default Home;

View File

@@ -1,95 +0,0 @@
body {
background-color: #616161;
}
#login-form {
width: 400px;
max-width: 100%;
margin: 50px auto;
background-color: green;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
}
#login-form h1 {
text-align: center;
margin: 0;
padding: 20px 0;
font-size: 28px;
font-weight: bold;
color: white;
}
#login-form form {
padding: 20px;
background-color: white;
border-radius: 10px;
font-family: "Poppins", sans-serif;
}
#login-form form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: black;
font-family: "Poppins", sans-serif;
}
#login-form form input[type="file"],
#login-form form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid lightgray;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 20px;
}
#login-form form input[type="submit"] {
width: 100%;
padding: 12px;
background-color: dodgerblue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#login-form form input[type="submit"]:hover {
background-color: deepskyblue;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
#sub_div p {
color: red;
grid-template-columns: auto auto auto;
}
#sub_div {
padding-top: 10px;
}
#sub_div button {
width: 40%;
padding: 12px;
background-color: blue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#sub_div button:hover {
background-color: green;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

View File

@@ -1,84 +0,0 @@
import { useState } from 'react'
import './Import.css'
import Request from '../Request.jsx'
function Login() {
const error = useState(new URL(window.location).searchParams.get("message") || "");
const status = useState("");
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
let file=[], file1=[], file2=[], file3=[], password;
let has_imported = false;
const handleSubmit = async (event) => {
event.preventDefault();
if (!file[0] || !file1[0] || has_imported || !password) return;
try {
has_imported = true;
let data = {
userdata: JSON.parse(await file[0].text()),
home: JSON.parse(await file1[0].text()),
missions: file2[0] ? JSON.parse(await file2[0].text()) : undefined,
sif_cards: file3[0] ? JSON.parse(await file3[0].text()) : undefined,
password: password,
jp: true
};
if (!data.userdata || !data.userdata.user || !data.userdata.user.id) {
error[1]("Incorrect user data file format");
return;
}
if (!data.home || !data.home.home || !data.home.home.information_list) {
error[1]("Incorrect home data file format");
return;
}
if (!Array.isArray(data.missions) && data.missions) {
error[1]("Incorrect mission data file format");
return;
}
if (!Array.isArray(data.sif_cards) && data.sif_cards) {
error[1]("Incorrect sif card data file format");
return;
}
let resp = await Request(
"/api/webui/import",
data
);
if (resp.result == "OK") {
status[1](<div><p>Account imported!</p><p>User id: {resp.uid}</p><p>Migration token: {resp.migration_token}</p></div>);
} else {
error[1](resp.message);
}
} catch(e) {
error[1](e.message);
}
};
return (
<div id="login-form">
<h1>Transfer</h1>
<form>
{ <p>{ status[0] } </p> }
<label htmlFor="id">User data file (required):</label>
<input type="file" id="id" name="id" onChange={(event) => {file = event.target.files}} accept="application/json"/>
<label htmlFor="file1">User Home data file (required):</label>
<input type="file" id="file1" name="file1" onChange={(event) => {file1 = event.target.files}} accept="application/json"/>
<label htmlFor="file2">User Missions data file (optional):</label>
<input type="file" id="file2" name="file2" onChange={(event) => {file2 = event.target.files}} accept="application/json"/>
<label htmlFor="file3">Sif cards data file (optional):</label>
<input type="file" id="file3" name="file3" onChange={(event) => {file3 = event.target.files}} accept="application/json"/>
<label htmlFor="password">Transfer passcode (game will not recognize special characters, only use letters and numbers or you will be locked out):</label>
<input type="password" id="password" name="password" onChange={(event) => {password = event.target.value}} />
<input type="submit" value="Submit" onClick={handleSubmit}/>
<div id="sub_div">
{ error[0] ? <p>Error: { error[0] }. Please reload the page and try again.</p> : <p></p> }
</div>
</form>
</div>
);
}
export default Login;

View File

@@ -1,95 +0,0 @@
body {
background-color: #616161;
}
#login-form {
width: 400px;
max-width: 100%;
margin: 50px auto;
background-color: green;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
}
#login-form h1 {
text-align: center;
margin: 0;
padding: 20px 0;
font-size: 28px;
font-weight: bold;
color: white;
}
#login-form form {
padding: 20px;
background-color: white;
border-radius: 10px;
font-family: "Poppins", sans-serif;
}
#login-form form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: black;
font-family: "Poppins", sans-serif;
}
#login-form form input[type="text"],
#login-form form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid lightgray;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 20px;
}
#login-form form input[type="submit"] {
width: 100%;
padding: 12px;
background-color: dodgerblue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#login-form form input[type="submit"]:hover {
background-color: deepskyblue;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
#sub_div p {
color: red;
grid-template-columns: auto auto auto;
}
#sub_div {
padding-top: 10px;
}
#sub_div button {
width: 40%;
padding: 12px;
background-color: blue;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#sub_div button:hover {
background-color: green;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

View File

@@ -1,69 +0,0 @@
import { useState } from 'react'
import './Login.css'
import Request from '../Request.jsx'
function Login() {
const error = useState(new URL(window.location).searchParams.get("message") || "");
const uid = useState((window.localStorage && window.localStorage.getItem("ew_uid")) || "");
const password = useState("");
function showError(message) {
error[1](message);
}
const handleSubmit = async (event) => {
event.preventDefault();
if (!uid[0] || !password[0]) {
showError("Missing userid/password");
return;
}
if (isNaN(uid[0])) {
showError("UserID should be a number. (The \"Friend ID\" in your profile)");
return;
}
if (window.localStorage) window.localStorage.setItem("ew_uid", uid[0]);
let resp = await Request(
"/api/webui/login",
{
uid: parseInt(uid[0]),
password: password[0]
}
);
if (resp.result == "OK") {
window.location.href = "/home/";
} else {
showError(resp.message);
}
};
const import_user = (e) => {
e.preventDefault();
window.location.href = "/import/";
}
const help = (e) => {
e.preventDefault();
window.location.href = "/help/";
}
return (
<div id="login-form">
<h1>Login</h1>
<form>
<label htmlFor="id">SIF2 ID:</label>
<input type="text" id="id" name="id" onChange={(event) => {uid[1](event.target.value)}} value={uid[0]} />
<label htmlFor="password">Transfer passcode:</label>
<input type="password" id="password" name="password" onChange={(event) => {password[1](event.target.value)}} />
<input type="submit" value="Submit" onClick={handleSubmit}/>
<div id="sub_div">
<button onClick={import_user}>Import User</button><br/><br/>
<button onClick={help}>Need help?</button><br/><br/>
{ error[0] ? <p>Error: { error[0] } </p> : <p></p> }
</div>
<p>EW Version 1.0.0 - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew">View source</a> - <a href="https://git.ethanthesleepy.one/ethanaobrien/ew/src/branch/main/LICENSE">View license</a></p>
</form>
</div>
);
}
export default Login;

View File

@@ -1,32 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import Login from './login/Login.jsx'
import Home from './home/Home.jsx'
import Import from './import/Import.jsx'
import Help from './help/Help.jsx'
let Elem;
switch (window.location.pathname) {
case "/":
Elem = Login;
break;
case "/home/":
Elem = Home;
break;
case "/import/":
Elem = Import;
break;
case "/help/":
Elem = Help;
break;
default:
window.location.pathname = "/";
}
if (Elem) {
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Elem />
</React.StrictMode>,
)
}

View File

@@ -1,15 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
entryFileNames: 'index.js',
assetFileNames: 'index.css',
},
},
},
})