Compare commits
3 Commits
eb183521bc
...
0f780ef415
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f780ef415 | ||
| b756204514 | |||
| 76281892a2 |
811
Cargo.lock
generated
811
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,17 +6,19 @@ default-run = "backend"
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
prost = "0.14.1"
|
prost = "0.14"
|
||||||
prost-types = "0.14.1"
|
prost-types = "0.14"
|
||||||
rocket = { git = "https://github.com/rwf2/Rocket", rev = "504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" }
|
rocket = { git = "https://github.com/rwf2/Rocket", rev = "504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
rocket_prost_responder_derive = { path = "rocket_prost_responder_derive" }
|
rocket_prost_responder_derive = { path = "rocket_prost_responder_derive" }
|
||||||
uuid = { version = "1.19.0", features = ["v4"] }
|
uuid = { version = "1.19", features = ["v4"] }
|
||||||
bcrypt = "0.17.1"
|
bcrypt = "0.17.1"
|
||||||
bincode = "2.0.1"
|
bincode = "2.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
reqwest = { version = "0.12.24", features = ["json"] }
|
reqwest = { version = "0.13", features = ["json"] }
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.14.1"
|
prost-build = "0.14"
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::auth_persistence::AuthStorage;
|
||||||
use crate::items;
|
use crate::items;
|
||||||
use crate::proto_utils::Proto;
|
use crate::proto_utils::Proto;
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
@ -8,12 +9,16 @@ use uuid::Uuid;
|
|||||||
pub struct AuthState {
|
pub struct AuthState {
|
||||||
// Map token -> username
|
// Map token -> username
|
||||||
tokens: Mutex<HashMap<String, String>>,
|
tokens: Mutex<HashMap<String, String>>,
|
||||||
|
storage: AuthStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthState {
|
impl AuthState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let storage = AuthStorage::new();
|
||||||
|
let tokens = storage.load_tokens();
|
||||||
Self {
|
Self {
|
||||||
tokens: Mutex::new(HashMap::new()),
|
tokens: Mutex::new(tokens),
|
||||||
|
storage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,7 +143,8 @@ pub async fn login(
|
|||||||
{
|
{
|
||||||
let token = Uuid::new_v4().to_string();
|
let token = Uuid::new_v4().to_string();
|
||||||
let mut tokens = state.tokens.lock().await;
|
let mut tokens = state.tokens.lock().await;
|
||||||
tokens.insert(token.clone(), req.username);
|
tokens.insert(token.clone(), req.username.clone());
|
||||||
|
state.storage.save_tokens(&tokens);
|
||||||
|
|
||||||
return items::LoginResponse {
|
return items::LoginResponse {
|
||||||
token,
|
token,
|
||||||
@ -163,6 +169,7 @@ pub async fn logout(
|
|||||||
let mut tokens = state.tokens.lock().await;
|
let mut tokens = state.tokens.lock().await;
|
||||||
|
|
||||||
if tokens.remove(&req.token).is_some() {
|
if tokens.remove(&req.token).is_some() {
|
||||||
|
state.storage.save_tokens(&tokens);
|
||||||
items::LogoutResponse {
|
items::LogoutResponse {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Logged out successfully".to_string(),
|
message: "Logged out successfully".to_string(),
|
||||||
|
|||||||
261
backend/src/auth_persistence.rs
Normal file
261
backend/src/auth_persistence.rs
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
use aes_gcm::{
|
||||||
|
Aes256Gcm, Nonce,
|
||||||
|
aead::{Aead, KeyInit},
|
||||||
|
};
|
||||||
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||||
|
use bincode::{config, decode_from_slice, encode_to_vec};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, BufReader, BufWriter, Write};
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, bincode::Encode, bincode::Decode)]
|
||||||
|
pub struct TokenEntry {
|
||||||
|
pub token: String,
|
||||||
|
pub username: String,
|
||||||
|
pub created_at: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTH_KEY_FILE: &str = ".auth_key";
|
||||||
|
const TOKENS_FILE: &str = "tokens.bin";
|
||||||
|
const NONCE_SIZE: usize = 12;
|
||||||
|
const KEY_SIZE: usize = 32;
|
||||||
|
|
||||||
|
pub struct AuthStorage {
|
||||||
|
cipher: Aes256Gcm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AuthStorage {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthStorage {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let key = Self::load_or_create_key();
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key).expect("Invalid key length");
|
||||||
|
Self { cipher }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_or_create_key() -> Vec<u8> {
|
||||||
|
if let Ok(existing_key) = Self::load_key_from_file() {
|
||||||
|
return existing_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = Self::generate_key();
|
||||||
|
if let Err(e) = Self::save_key_to_file(&key) {
|
||||||
|
eprintln!("Warning: Failed to save auth key to file: {}", e);
|
||||||
|
}
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_key() -> Vec<u8> {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
let mut key = [0u8; KEY_SIZE];
|
||||||
|
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos();
|
||||||
|
|
||||||
|
let mut seed = timestamp as u64;
|
||||||
|
for byte in key.iter_mut() {
|
||||||
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
||||||
|
*byte = (seed >> (seed % 8)) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
key.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_key_from_file() -> io::Result<Vec<u8>> {
|
||||||
|
let file = File::open(AUTH_KEY_FILE)?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let encoded = std::io::read_to_string(reader)?;
|
||||||
|
let key = STANDARD.decode(encoded).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("Base64 decode error: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if key.len() != KEY_SIZE {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"Invalid key length",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_key_to_file(key: &[u8]) -> io::Result<()> {
|
||||||
|
let encoded = STANDARD.encode(key);
|
||||||
|
let mut file = File::create(AUTH_KEY_FILE)?;
|
||||||
|
|
||||||
|
let mut permissions = file.metadata()?.permissions();
|
||||||
|
permissions.set_mode(0o600);
|
||||||
|
file.set_permissions(permissions)?;
|
||||||
|
|
||||||
|
file.write_all(encoded.as_bytes())?;
|
||||||
|
file.sync_all()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_tokens(&self) -> HashMap<String, String> {
|
||||||
|
let file = match File::open(TOKENS_FILE) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: No existing tokens file, starting with empty token list");
|
||||||
|
return HashMap::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let encrypted_data = match std::io::read_to_string(reader) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: Failed to read tokens file, starting with empty token list");
|
||||||
|
return HashMap::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoded = match STANDARD.decode(&encrypted_data) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: Failed to decode tokens file, starting with empty token list");
|
||||||
|
return HashMap::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if decoded.len() <= NONCE_SIZE {
|
||||||
|
eprintln!("Warning: Invalid tokens file format, starting with empty token list");
|
||||||
|
return HashMap::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (nonce_bytes, ciphertext) = decoded.split_at(NONCE_SIZE);
|
||||||
|
let nonce = Nonce::from_slice(nonce_bytes);
|
||||||
|
|
||||||
|
let plaintext = match self.cipher.decrypt(nonce, ciphertext.as_ref()) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: Failed to decrypt tokens file, starting with empty token list");
|
||||||
|
return HashMap::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = config::standard();
|
||||||
|
let (entries, _): (Vec<TokenEntry>, usize) = match decode_from_slice(&plaintext, config) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("Warning: Failed to deserialize tokens, starting with empty token list");
|
||||||
|
return HashMap::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| (entry.token, entry.username))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_tokens(&self, tokens: &HashMap<String, String>) {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
let entries: Vec<TokenEntry> = tokens
|
||||||
|
.iter()
|
||||||
|
.map(|(token, username)| TokenEntry {
|
||||||
|
token: token.clone(),
|
||||||
|
username: username.clone(),
|
||||||
|
created_at: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let config = config::standard();
|
||||||
|
let plaintext: Vec<u8> = match encode_to_vec(&entries, config) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Failed to serialize tokens: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce_bytes: [u8; NONCE_SIZE] = rand::thread_rng().generate_bytes();
|
||||||
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||||
|
let ciphertext = match self.cipher.encrypt(nonce, plaintext.as_ref()) {
|
||||||
|
Ok(encrypted) => encrypted,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Failed to encrypt tokens: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encrypted_data = nonce_bytes.to_vec();
|
||||||
|
encrypted_data.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
let encoded = STANDARD.encode(&encrypted_data);
|
||||||
|
|
||||||
|
match File::create(TOKENS_FILE) {
|
||||||
|
Ok(file) => {
|
||||||
|
if let Ok(metadata) = file.metadata() {
|
||||||
|
let mut permissions = metadata.permissions();
|
||||||
|
permissions.set_mode(0o600);
|
||||||
|
if let Err(e) = file.set_permissions(permissions) {
|
||||||
|
eprintln!("Warning: Failed to set permissions on tokens file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut writer = BufWriter::new(file);
|
||||||
|
|
||||||
|
if let Err(e) = writer.write_all(encoded.as_bytes()) {
|
||||||
|
eprintln!("Warning: Failed to write tokens file: {}", e);
|
||||||
|
}
|
||||||
|
if let Err(e) = writer.flush() {
|
||||||
|
eprintln!("Warning: Failed to flush tokens file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Failed to create tokens file: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod rand {
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static STATE: Cell<u64> = Cell::new(
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos() as u64
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThreadRng;
|
||||||
|
|
||||||
|
pub fn thread_rng() -> ThreadRng {
|
||||||
|
ThreadRng
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadRng {
|
||||||
|
pub fn generate_bytes<const N: usize>(&mut self) -> [u8; N] {
|
||||||
|
let mut result = [0u8; N];
|
||||||
|
STATE.with(|state| {
|
||||||
|
let mut s = state.get();
|
||||||
|
for byte in result.iter_mut() {
|
||||||
|
s = s.wrapping_mul(1103515245).wrapping_add(12345);
|
||||||
|
*byte = (s >> (s % 8)) as u8;
|
||||||
|
}
|
||||||
|
state.set(s);
|
||||||
|
});
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ pub mod items {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod auth_persistence;
|
||||||
pub mod proto_utils;
|
pub mod proto_utils;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use rocket::fs::FileServer;
|
|||||||
use rocket::futures::lock::Mutex;
|
use rocket::futures::lock::Mutex;
|
||||||
|
|
||||||
use backend::auth;
|
use backend::auth;
|
||||||
|
use backend::auth::AdminState;
|
||||||
use backend::items::{self, Game};
|
use backend::items::{self, Game};
|
||||||
use backend::proto_utils;
|
use backend::proto_utils;
|
||||||
use backend::store::{self, User, save_state};
|
use backend::store::{self, User, save_state};
|
||||||
@ -111,6 +112,7 @@ async fn update_game(
|
|||||||
game: proto_utils::Proto<items::Game>,
|
game: proto_utils::Proto<items::Game>,
|
||||||
) -> Option<items::Game> {
|
) -> Option<items::Game> {
|
||||||
let mut games = game_list.lock().await;
|
let mut games = game_list.lock().await;
|
||||||
|
let mut users = user_list.lock().await;
|
||||||
let mut game = game.into_inner();
|
let mut game = game.into_inner();
|
||||||
|
|
||||||
game.title = game.title.trim().to_string();
|
game.title = game.title.trim().to_string();
|
||||||
@ -125,13 +127,14 @@ async fn update_game(
|
|||||||
(g.remote_id == game.remote_id && g.source == game.source) || (g.title == game.title)
|
(g.remote_id == game.remote_id && g.source == game.source) || (g.title == game.title)
|
||||||
}) {
|
}) {
|
||||||
if existing.title != game.title {
|
if existing.title != game.title {
|
||||||
|
let old_title = existing.title.clone();
|
||||||
// Update title for every opinion
|
// Update title for every opinion
|
||||||
for person in user_list.lock().await.iter_mut() {
|
for person in users.iter_mut() {
|
||||||
let opinion = person
|
let opinion = person
|
||||||
.person
|
.person
|
||||||
.opinion
|
.opinion
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|o| o.title == existing.title);
|
.find(|o| o.title == old_title);
|
||||||
if let Some(opinion) = opinion {
|
if let Some(opinion) = opinion {
|
||||||
opinion.title = game.title.clone();
|
opinion.title = game.title.clone();
|
||||||
}
|
}
|
||||||
@ -149,7 +152,6 @@ async fn update_game(
|
|||||||
|
|
||||||
games.sort_unstable_by(|g1, g2| g1.title.cmp(&g2.title));
|
games.sort_unstable_by(|g1, g2| g1.title.cmp(&g2.title));
|
||||||
|
|
||||||
let users = user_list.lock().await;
|
|
||||||
save_state(&games, &users);
|
save_state(&games, &users);
|
||||||
|
|
||||||
r_existing
|
r_existing
|
||||||
@ -189,13 +191,16 @@ async fn refresh_state(
|
|||||||
_token: auth::AdminToken,
|
_token: auth::AdminToken,
|
||||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||||
|
admin_state: &rocket::State<AdminState>,
|
||||||
) -> items::RefreshResponse {
|
) -> items::RefreshResponse {
|
||||||
if let Some((new_games, new_users)) = store::load_state() {
|
if let Some((new_games, new_users)) = store::load_state() {
|
||||||
let mut games = game_list.lock().await;
|
let mut games = game_list.lock().await;
|
||||||
let mut users = user_list.lock().await;
|
let mut users = user_list.lock().await;
|
||||||
|
let mut admins = admin_state.admins.lock().await;
|
||||||
|
|
||||||
*games = new_games;
|
*games = new_games;
|
||||||
*users = new_users;
|
*users = new_users;
|
||||||
|
*admins = store::load_admins();
|
||||||
|
|
||||||
items::RefreshResponse {
|
items::RefreshResponse {
|
||||||
success: true,
|
success: true,
|
||||||
@ -212,12 +217,12 @@ async fn refresh_state(
|
|||||||
#[post("/opinion", data = "<req>")]
|
#[post("/opinion", data = "<req>")]
|
||||||
async fn add_opinion(
|
async fn add_opinion(
|
||||||
token: auth::Token,
|
token: auth::Token,
|
||||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
|
||||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||||
|
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||||
req: proto_utils::Proto<items::AddOpinionRequest>,
|
req: proto_utils::Proto<items::AddOpinionRequest>,
|
||||||
) -> Option<items::Person> {
|
) -> Option<items::Person> {
|
||||||
let mut users = user_list.lock().await;
|
|
||||||
let games = game_list.lock().await;
|
let games = game_list.lock().await;
|
||||||
|
let mut users = user_list.lock().await;
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
|
||||||
// Validate game exists
|
// Validate game exists
|
||||||
@ -261,12 +266,12 @@ async fn add_opinion(
|
|||||||
#[patch("/opinion", data = "<req>")]
|
#[patch("/opinion", data = "<req>")]
|
||||||
async fn remove_opinion(
|
async fn remove_opinion(
|
||||||
token: auth::Token,
|
token: auth::Token,
|
||||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
|
||||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||||
|
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||||
req: proto_utils::Proto<items::RemoveOpinionRequest>,
|
req: proto_utils::Proto<items::RemoveOpinionRequest>,
|
||||||
) -> Option<items::Person> {
|
) -> Option<items::Person> {
|
||||||
let mut users = user_list.lock().await;
|
|
||||||
let games = game_list.lock().await;
|
let games = game_list.lock().await;
|
||||||
|
let mut users = user_list.lock().await;
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
|
||||||
if let Some(user) = users
|
if let Some(user) = users
|
||||||
@ -340,9 +345,13 @@ async fn get_game_thumbnail(
|
|||||||
.json::<serde_json::Value>()
|
.json::<serde_json::Value>()
|
||||||
.await
|
.await
|
||||||
.ok()?
|
.ok()?
|
||||||
.get("universeId")?
|
.get("universeId")
|
||||||
.as_u64()
|
.and_then(|v| v.as_u64())
|
||||||
.unwrap()
|
};
|
||||||
|
|
||||||
|
let universe_id = match universe_id {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return None.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let api_url = format!(
|
let api_url = format!(
|
||||||
|
|||||||
159
frontend/pnpm-lock.yaml
generated
159
frontend/pnpm-lock.yaml
generated
@ -59,7 +59,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: ^8.48.1
|
specifier: ^8.48.1
|
||||||
version: 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
version: 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: npm:rolldown-vite@7.2.5
|
specifier: npm:rolldown-vite@7.2.5
|
||||||
version: rolldown-vite@7.2.5(@types/node@24.10.1)
|
version: rolldown-vite@7.2.5(@types/node@24.10.1)
|
||||||
@ -167,6 +167,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1':
|
||||||
|
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2':
|
'@eslint-community/regexpp@4.12.2':
|
||||||
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
@ -362,70 +368,70 @@ packages:
|
|||||||
'@types/react@19.2.7':
|
'@types/react@19.2.7':
|
||||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.48.1':
|
'@typescript-eslint/eslint-plugin@8.52.0':
|
||||||
resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
|
resolution: {integrity: sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': ^8.48.1
|
'@typescript-eslint/parser': ^8.52.0
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.48.1':
|
'@typescript-eslint/parser@8.52.0':
|
||||||
resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==}
|
resolution: {integrity: sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.48.1':
|
'@typescript-eslint/project-service@8.52.0':
|
||||||
resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==}
|
resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.48.1':
|
'@typescript-eslint/scope-manager@8.52.0':
|
||||||
resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==}
|
resolution: {integrity: sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.48.1':
|
'@typescript-eslint/tsconfig-utils@8.52.0':
|
||||||
resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==}
|
resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.48.1':
|
'@typescript-eslint/type-utils@8.52.0':
|
||||||
resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==}
|
resolution: {integrity: sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/types@8.48.1':
|
'@typescript-eslint/types@8.52.0':
|
||||||
resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==}
|
resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.48.1':
|
'@typescript-eslint/typescript-estree@8.52.0':
|
||||||
resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==}
|
resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.48.1':
|
'@typescript-eslint/utils@8.52.0':
|
||||||
resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==}
|
resolution: {integrity: sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
typescript: '>=4.8.4 <6.0.0'
|
typescript: '>=4.8.4 <6.0.0'
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.48.1':
|
'@typescript-eslint/visitor-keys@8.52.0':
|
||||||
resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==}
|
resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
'@vitejs/plugin-react@5.1.2':
|
'@vitejs/plugin-react@5.1.2':
|
||||||
resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
|
resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: npm:rolldown-vite@7.2.5
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
@ -646,9 +652,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
|
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
graphemer@1.4.0:
|
|
||||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
|
||||||
|
|
||||||
has-flag@4.0.0:
|
has-flag@4.0.0:
|
||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -981,8 +984,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
ts-api-utils@2.1.0:
|
ts-api-utils@2.4.0:
|
||||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
|
||||||
engines: {node: '>=18.12'}
|
engines: {node: '>=18.12'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4'
|
typescript: '>=4.8.4'
|
||||||
@ -1004,8 +1007,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
typescript-eslint@8.48.1:
|
typescript-eslint@8.52.0:
|
||||||
resolution: {integrity: sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==}
|
resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0
|
eslint: ^8.57.0 || ^9.0.0
|
||||||
@ -1190,6 +1193,11 @@ snapshots:
|
|||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.1)':
|
||||||
|
dependencies:
|
||||||
|
eslint: 9.39.1
|
||||||
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.2': {}
|
'@eslint-community/regexpp@4.12.2': {}
|
||||||
|
|
||||||
'@eslint/config-array@0.21.1':
|
'@eslint/config-array@0.21.1':
|
||||||
@ -1362,96 +1370,95 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/scope-manager': 8.48.1
|
'@typescript-eslint/scope-manager': 8.52.0
|
||||||
'@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/type-utils': 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.48.1
|
'@typescript-eslint/visitor-keys': 8.52.0
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
graphemer: 1.4.0
|
|
||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/parser@8.52.0(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 8.48.1
|
'@typescript-eslint/scope-manager': 8.52.0
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.48.1
|
'@typescript-eslint/visitor-keys': 8.52.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.48.1(typescript@5.9.3)':
|
'@typescript-eslint/project-service@8.52.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/scope-manager@8.48.1':
|
'@typescript-eslint/scope-manager@8.52.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
'@typescript-eslint/visitor-keys': 8.48.1
|
'@typescript-eslint/visitor-keys': 8.52.0
|
||||||
|
|
||||||
'@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)':
|
'@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@8.48.1(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/type-utils@8.52.0(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/types@8.48.1': {}
|
'@typescript-eslint/types@8.52.0': {}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)':
|
'@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/project-service': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/project-service': 8.52.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
'@typescript-eslint/visitor-keys': 8.48.1
|
'@typescript-eslint/visitor-keys': 8.52.0
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.4.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@8.48.1(eslint@9.39.1)(typescript@5.9.3)':
|
'@typescript-eslint/utils@8.52.0(eslint@9.39.1)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1)
|
||||||
'@typescript-eslint/scope-manager': 8.48.1
|
'@typescript-eslint/scope-manager': 8.52.0
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3)
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/visitor-keys@8.48.1':
|
'@typescript-eslint/visitor-keys@8.52.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 8.48.1
|
'@typescript-eslint/types': 8.52.0
|
||||||
eslint-visitor-keys: 4.2.1
|
eslint-visitor-keys: 4.2.1
|
||||||
|
|
||||||
'@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.1))':
|
'@vitejs/plugin-react@5.1.2(rolldown-vite@7.2.5(@types/node@24.10.1))':
|
||||||
@ -1677,8 +1684,6 @@ snapshots:
|
|||||||
|
|
||||||
globals@16.5.0: {}
|
globals@16.5.0: {}
|
||||||
|
|
||||||
graphemer@1.4.0: {}
|
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
hermes-estree@0.25.1: {}
|
hermes-estree@0.25.1: {}
|
||||||
@ -1930,7 +1935,7 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
ts-api-utils@2.1.0(typescript@5.9.3):
|
ts-api-utils@2.4.0(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
|
|
||||||
@ -1956,12 +1961,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
|
|
||||||
typescript-eslint@8.48.1(eslint@9.39.1)(typescript@5.9.3):
|
typescript-eslint@8.52.0(eslint@9.39.1)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/eslint-plugin': 8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/parser': 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
'@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.48.1(eslint@9.39.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.52.0(eslint@9.39.1)(typescript@5.9.3)
|
||||||
eslint: 9.39.1
|
eslint: 9.39.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Person, PersonList as PersonListProto } from "../items";
|
import { Person, PersonList as PersonListProto } from "../items";
|
||||||
import { Login } from "./Login";
|
import { Login } from "./Login";
|
||||||
import { PersonList } from "./PersonList";
|
import { PersonList } from "./PersonList";
|
||||||
@ -62,10 +62,21 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
const addToast = (message: string, type: ToastType = "info") => {
|
useEffect(() => {
|
||||||
|
const handleUnauthorized = () => {
|
||||||
|
setToken("");
|
||||||
|
setPeople([]);
|
||||||
|
addToast("Session expired. Please log in again.", "info");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("unauthorized", handleUnauthorized);
|
||||||
|
return () => window.removeEventListener("unauthorized", handleUnauthorized);
|
||||||
|
}, [addToast]);
|
||||||
|
|
||||||
|
const addToast = useCallback((message: string, type: ToastType = "info") => {
|
||||||
const id = Date.now();
|
const id = Date.now();
|
||||||
setToasts((prev) => [...prev, { id, message, type }]);
|
setToasts((prev) => [...prev, { id, message, type }]);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeToast = (id: number) => {
|
const removeToast = (id: number) => {
|
||||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export function GameDetails({ onShowToast }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <LoadingState message="Loading game details..." />;
|
if (loading) return <LoadingState message="Loading game details..." />;
|
||||||
if (error) return <ErrorState message={error} onRetry={() => window.location.reload()} />;
|
if (error) return <ErrorState message={error} onRetry={() => navigate(0)} />;
|
||||||
if (!game) return <EmptyState icon="🎮" title="Game not found" description="This game doesn't exist or has been deleted" />;
|
if (!game) return <EmptyState icon="🎮" title="Game not found" description="This game doesn't exist or has been deleted" />;
|
||||||
|
|
||||||
const getExternalLink = () => {
|
const getExternalLink = () => {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const apiFetch = async (
|
|||||||
if (response.status == 401) {
|
if (response.status == 401) {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("isAdmin");
|
localStorage.removeItem("isAdmin");
|
||||||
window.location.href = "/";
|
window.dispatchEvent(new CustomEvent("unauthorized"));
|
||||||
}
|
}
|
||||||
throw new Error(`Request failed with status ${response.status}`);
|
throw new Error(`Request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,12 @@ export function useGameFilter(
|
|||||||
const [fetchedTitles, setFetchedTitles] = useState<string[]>([]);
|
const [fetchedTitles, setFetchedTitles] = useState<string[]>([]);
|
||||||
const metaDataRef = useRef<{ [key: string]: GameProto }>({});
|
const metaDataRef = useRef<{ [key: string]: GameProto }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
metaDataRef.current = {};
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { gameToNegative, gameToPositiveOpinion } = useMemo(() => {
|
const { gameToNegative, gameToPositiveOpinion } = useMemo(() => {
|
||||||
const gameToNegative = new Map<string, Set<string>>();
|
const gameToNegative = new Map<string, Set<string>>();
|
||||||
const gameToPositiveOpinion = new Map<string, Set<string>>();
|
const gameToPositiveOpinion = new Map<string, Set<string>>();
|
||||||
@ -108,7 +114,7 @@ export function useGameFilter(
|
|||||||
|
|
||||||
const gamesMap = useMemo(() => {
|
const gamesMap = useMemo(() => {
|
||||||
return new Map(Object.entries(metaDataRef.current));
|
return new Map(Object.entries(metaDataRef.current));
|
||||||
}, [fetchedTitles]);
|
}, []);
|
||||||
|
|
||||||
return { filteredGames, gameToPositive: gameToPositiveOpinion, games: gamesMap };
|
return { filteredGames, gameToPositive: gameToPositiveOpinion, games: gamesMap };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user