mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-07-12 00:32:20 +08:00
Compare commits
8 Commits
d61b27af8f
...
956634d8ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956634d8ab | ||
|
|
8a1d33bd2b | ||
|
|
aef2c4986d | ||
|
|
b35aa35b1a | ||
|
|
0ef0a83685 | ||
|
|
586f4612ed | ||
|
|
f0183370be | ||
|
|
f4fe727a0e |
@@ -18,11 +18,6 @@ jobs:
|
||||
shell: bash
|
||||
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
|
||||
id: setup-ndk
|
||||
uses: https://github.com/nttld/setup-ndk@v1
|
||||
@@ -33,12 +28,6 @@ jobs:
|
||||
run: |
|
||||
mkdir out
|
||||
|
||||
- name: Build webui
|
||||
run: |
|
||||
cd webui
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Install cargo ndk
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "assets"]
|
||||
path = assets
|
||||
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
690
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -4,12 +4,12 @@ version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.12"
|
||||
rusqlite = { version = "0.37.0", features = ["bundled"] }
|
||||
clap = { version = "4.5.37", features = ["derive"]}
|
||||
actix-web = "4.13"
|
||||
rusqlite = { version = "0.38.0", features = ["bundled"] }
|
||||
clap = { version = "4.5.59", features = ["derive"]}
|
||||
base64 = "0.22.1"
|
||||
json = "0.12.4"
|
||||
rand = "0.9.2"
|
||||
rand = "0.10.0"
|
||||
lazy_static = "1.5.0"
|
||||
hex = "0.4.3"
|
||||
hmac = "0.12.1"
|
||||
@@ -17,8 +17,8 @@ md5 = "0.8.0"
|
||||
urlencoding = "2.1.3"
|
||||
sha1 = "0.10.6"
|
||||
substring = "1.4.5"
|
||||
uuid = { version = "1.18.1", features = ["v7"] }
|
||||
rsa = { version = "0.9.9", features = ["sha1"] }
|
||||
uuid = { version = "1.21.0", features = ["v7"] }
|
||||
rsa = { version = "0.9.10", features = ["sha1"] }
|
||||
mime = "0.3.17"
|
||||
sha2 = "0.10.9"
|
||||
include-flate-codegen = "0.3.1"
|
||||
@@ -26,22 +26,23 @@ libflate = "2.2.1"
|
||||
cbc = { version = "0.1.2", features = ["alloc"] }
|
||||
aes = "0.8.4"
|
||||
pem = "3.0.6"
|
||||
ureq = "3.1.4"
|
||||
include_dir = {version = "0.7.4", optional = true }
|
||||
ureq = "3.2.0"
|
||||
mime_guess = "2.0.5"
|
||||
include_dir = "0.7.4"
|
||||
|
||||
[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]
|
||||
objc2 = { version = "0.6.3", optional = true }
|
||||
objc2-foundation = { version = "0.3.2", features = ["NSFileManager"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0"
|
||||
cc = "1.2"
|
||||
|
||||
# To enable this library you MUST comment out lib block below and add --features library
|
||||
[features]
|
||||
library = ["jni", "objc2", "objc2-foundation", "include_dir"]
|
||||
library = ["jni", "objc2", "objc2-foundation"]
|
||||
|
||||
#[lib]
|
||||
#crate-type = ["cdylib"]
|
||||
|
||||
4
build.rs
4
build.rs
@@ -9,4 +9,8 @@ fn main() {
|
||||
.compile("libc_code.a");
|
||||
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?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,12 @@ FROM docker.io/library/debian:latest AS builder
|
||||
# First - build
|
||||
|
||||
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
|
||||
|
||||
WORKDIR /ew/
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
WORKDIR /ew/webui/
|
||||
|
||||
RUN npm i && npm run build
|
||||
|
||||
WORKDIR /ew/
|
||||
|
||||
RUN . "$HOME/.cargo/env" && cargo build --release
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
sif2-ew:
|
||||
image: sif2-ew:latest
|
||||
@@ -14,6 +13,7 @@ services:
|
||||
HIDDEN: false # Will disable the webui
|
||||
DISABLE_IMPORTS: false # Will disable account imports
|
||||
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
|
||||
#ANDROID_GLOBAL: "link.to/patched/android/global.apk"
|
||||
@@ -25,4 +25,7 @@ services:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./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
|
||||
|
||||
@@ -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 "")
|
||||
|
||||
/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
|
||||
|
||||
24
readme.md
24
readme.md
@@ -5,27 +5,15 @@ A (mostly functioning) server for Love Live! School idol festival 2 MIRACLE LIVE
|
||||
|
||||
## Building
|
||||
|
||||
### Linux
|
||||
|
||||
#### 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/)
|
||||
### Requirements
|
||||
- [rust](https://www.rust-lang.org/tools/install)
|
||||
|
||||
### Packaging/Running
|
||||
|
||||
**Build npm:**
|
||||
`cd webui && npm install && npm run build`
|
||||
|
||||
**Build Rust:**
|
||||
**Build:**
|
||||
Debug: `cargo run`
|
||||
Release: `cargo build --release`
|
||||
|
||||
**Docker:**
|
||||
`docker build --tag ew --file docker/Dockerfile .`
|
||||
Will create docker image with tag "ew"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use aes::cipher::BlockEncryptMut;
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
|
||||
|
||||
@@ -48,9 +48,7 @@ pub async fn run_server(in_thread: bool) -> std::io::Result<()> {
|
||||
srv.call(req)
|
||||
})
|
||||
.app_data(web::PayloadConfig::default().limit(1024 * 1024 * 25))
|
||||
.service(static_handlers::css)
|
||||
.service(static_handlers::maintenance)
|
||||
.service(static_handlers::js)
|
||||
.service(static_handlers::files_jp)
|
||||
.service(static_handlers::files_gl)
|
||||
.default_service(web::route().to(router::request))
|
||||
|
||||
@@ -57,7 +57,10 @@ pub struct Args {
|
||||
pub en_android_asset_hash: String,
|
||||
|
||||
#[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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use json::{JsonValue, object, array};
|
||||
use actix_web::HttpRequest;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::encryption;
|
||||
use crate::include_file;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use json::{array, object, JsonValue};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use actix_web::{HttpRequest, http::header::{HeaderMap, HeaderValue}};
|
||||
use crate::encryption;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use json::{object, array, JsonValue};
|
||||
use actix_web::{HttpRequest};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::router::{global, userdata, items, databases};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use json::{array, object, JsonValue};
|
||||
use actix_web::{HttpRequest};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::router::{global, userdata, items, databases};
|
||||
use crate::encryption;
|
||||
|
||||
@@ -3,7 +3,7 @@ pub mod user;
|
||||
use rusqlite::params;
|
||||
use lazy_static::lazy_static;
|
||||
use json::{JsonValue, array, object};
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::router::global;
|
||||
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();
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use rusqlite::params;
|
||||
use json::{JsonValue, object};
|
||||
use crate::router::userdata;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use sha2::{Digest, Sha256};
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ use actix_web::{
|
||||
};
|
||||
use json::{JsonValue, object};
|
||||
use lazy_static::lazy_static;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use std::fs;
|
||||
|
||||
use crate::include_file;
|
||||
use crate::router::{userdata, items};
|
||||
|
||||
//pub const FRONTEND_DOMAIN: &str = "https://sif2-api.ethanthesleepy.one";
|
||||
|
||||
fn get_config() -> JsonValue {
|
||||
let args = crate::get_args();
|
||||
object!{
|
||||
@@ -35,7 +35,6 @@ fn error(msg: &str) -> HttpResponse {
|
||||
message: msg
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
@@ -53,8 +52,6 @@ pub fn login(_req: HttpRequest, body: String) -> HttpResponse {
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.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())))
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
@@ -79,7 +76,6 @@ pub fn import(_req: HttpRequest, body: String) -> HttpResponse {
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
@@ -101,8 +97,6 @@ pub fn user(req: HttpRequest) -> HttpResponse {
|
||||
data: data
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
//.insert_header(("Access-Control-Allow-Credentials", "true"))
|
||||
.insert_header(ContentType::json())
|
||||
.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());
|
||||
|
||||
HttpResponse::Ok()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
//.insert_header(("Access-Control-Allow-Credentials", "true"))
|
||||
.insert_header(ContentType::json())
|
||||
.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());
|
||||
|
||||
HttpResponse::Ok()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
//.insert_header(("Access-Control-Allow-Credentials", "true"))
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
@@ -146,34 +136,63 @@ pub fn logout(req: HttpRequest) -> HttpResponse {
|
||||
result: "OK"
|
||||
};
|
||||
HttpResponse::Found()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
//.insert_header(("Access-Control-Allow-Credentials", "true"))
|
||||
.insert_header(ContentType::json())
|
||||
.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))
|
||||
}
|
||||
|
||||
static WEBUI_ASSETS: Dir<'_> = include_dir!("webui/");
|
||||
|
||||
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);
|
||||
if token.is_some() {
|
||||
let data = userdata::webui_get_user(&token.unwrap());
|
||||
if data.is_some() {
|
||||
return HttpResponse::Found()
|
||||
.insert_header(("Location", "/home/"))
|
||||
.insert_header(("Location", "/account.html"))
|
||||
.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) {
|
||||
let body = file.contents();
|
||||
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::Found()
|
||||
.insert_header(("Location", "/"))
|
||||
.body("");
|
||||
}
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::html())
|
||||
.body(include_file!("webui/dist/index.html"))
|
||||
.body("")
|
||||
}
|
||||
|
||||
pub fn export(req: HttpRequest) -> HttpResponse {
|
||||
@@ -213,7 +232,6 @@ pub fn server_info(_req: HttpRequest) -> HttpResponse {
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.insert_header(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
@@ -244,7 +262,6 @@ pub fn get_card_info(req: HttpRequest) -> HttpResponse {
|
||||
|
||||
return HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(resp));
|
||||
}
|
||||
|
||||
@@ -268,20 +285,20 @@ pub fn get_card_info(req: HttpRequest) -> HttpResponse {
|
||||
|
||||
if page_items.is_empty() {
|
||||
return HttpResponse::NotFound()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let total_pages = (total_len as f64 / max as f64).ceil() as usize;
|
||||
let args = crate::get_args();
|
||||
|
||||
let resp = object!{
|
||||
total_pages: total_pages,
|
||||
current: page_items
|
||||
current: page_items,
|
||||
image_path: args.image_asset_path
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
@@ -306,7 +323,6 @@ pub fn get_music_info(req: HttpRequest) -> HttpResponse {
|
||||
|
||||
if page_items.is_empty() {
|
||||
return HttpResponse::NotFound()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.finish();
|
||||
}
|
||||
|
||||
@@ -320,7 +336,6 @@ pub fn get_music_info(req: HttpRequest) -> HttpResponse {
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
@@ -332,14 +347,12 @@ lazy_static! {
|
||||
pub fn list_login_bonus(_req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(LOGIN_BONUS.clone()))
|
||||
}
|
||||
|
||||
pub fn list_items(_req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
.body(json::stringify(ITEM.clone()))
|
||||
}
|
||||
|
||||
@@ -379,8 +392,6 @@ pub fn cheat(req: HttpRequest, _body: String) -> HttpResponse {
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
//.insert_header(("Access-Control-Allow-Origin", FRONTEND_DOMAIN))
|
||||
//.insert_header(("Access-Control-Allow-Credentials", "true"))
|
||||
.insert_header(ContentType::json())
|
||||
.body(json::stringify(resp))
|
||||
}
|
||||
|
||||
@@ -5,20 +5,7 @@ use actix_web::{
|
||||
http::header::ContentType
|
||||
};
|
||||
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")]
|
||||
async fn maintenance(_req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
|
||||
1
webui
Submodule
1
webui
Submodule
Submodule webui added at 7fba8d617e
@@ -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
24
webui/.gitignore
vendored
@@ -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?
|
||||
@@ -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
4554
webui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>,
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user