Compare commits
No commits in common. "b756204514654531d938d08f9a8b35015e5fb2c4" and "8601d7ced116bae35f94664e0cdcfe28ca20b3b5" have entirely different histories.
b756204514
...
8601d7ced1
809
Cargo.lock
generated
809
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,19 +6,17 @@ default-run = "backend"
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
prost = "0.14"
|
prost = "0.14.1"
|
||||||
prost-types = "0.14"
|
prost-types = "0.14.1"
|
||||||
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", features = ["v4"] }
|
uuid = { version = "1.19.0", features = ["v4"] }
|
||||||
bcrypt = "0.17.1"
|
bcrypt = "0.17.1"
|
||||||
bincode = "2.0"
|
bincode = "2.0.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
reqwest = { version = "0.13", features = ["json"] }
|
reqwest = { version = "0.12.24", features = ["json"] }
|
||||||
aes-gcm = "0.10"
|
|
||||||
base64 = "0.22"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.14"
|
prost-build = "0.14.1"
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
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;
|
||||||
@ -9,16 +8,12 @@ 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(tokens),
|
tokens: Mutex::new(HashMap::new()),
|
||||||
storage,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,8 +138,7 @@ 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.clone());
|
tokens.insert(token.clone(), req.username);
|
||||||
state.storage.save_tokens(&tokens);
|
|
||||||
|
|
||||||
return items::LoginResponse {
|
return items::LoginResponse {
|
||||||
token,
|
token,
|
||||||
@ -169,7 +163,6 @@ 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(),
|
||||||
|
|||||||
@ -1,261 +0,0 @@
|
|||||||
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,7 +6,6 @@ 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,7 +3,6 @@ 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};
|
||||||
@ -112,7 +111,6 @@ 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();
|
||||||
@ -127,14 +125,13 @@ 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 users.iter_mut() {
|
for person in user_list.lock().await.iter_mut() {
|
||||||
let opinion = person
|
let opinion = person
|
||||||
.person
|
.person
|
||||||
.opinion
|
.opinion
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|o| o.title == old_title);
|
.find(|o| o.title == existing.title);
|
||||||
if let Some(opinion) = opinion {
|
if let Some(opinion) = opinion {
|
||||||
opinion.title = game.title.clone();
|
opinion.title = game.title.clone();
|
||||||
}
|
}
|
||||||
@ -152,6 +149,7 @@ 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
|
||||||
@ -191,16 +189,13 @@ 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,
|
||||||
@ -217,12 +212,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,
|
||||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
|
||||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||||
|
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||||
req: proto_utils::Proto<items::AddOpinionRequest>,
|
req: proto_utils::Proto<items::AddOpinionRequest>,
|
||||||
) -> Option<items::Person> {
|
) -> Option<items::Person> {
|
||||||
let games = game_list.lock().await;
|
|
||||||
let mut users = user_list.lock().await;
|
let mut users = user_list.lock().await;
|
||||||
|
let games = game_list.lock().await;
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
|
||||||
// Validate game exists
|
// Validate game exists
|
||||||
@ -266,12 +261,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,
|
||||||
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
|
||||||
user_list: &rocket::State<Mutex<Vec<User>>>,
|
user_list: &rocket::State<Mutex<Vec<User>>>,
|
||||||
|
game_list: &rocket::State<Mutex<Vec<Game>>>,
|
||||||
req: proto_utils::Proto<items::RemoveOpinionRequest>,
|
req: proto_utils::Proto<items::RemoveOpinionRequest>,
|
||||||
) -> Option<items::Person> {
|
) -> Option<items::Person> {
|
||||||
let games = game_list.lock().await;
|
|
||||||
let mut users = user_list.lock().await;
|
let mut users = user_list.lock().await;
|
||||||
|
let games = game_list.lock().await;
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
|
||||||
if let Some(user) = users
|
if let Some(user) = users
|
||||||
@ -345,13 +340,9 @@ async fn get_game_thumbnail(
|
|||||||
.json::<serde_json::Value>()
|
.json::<serde_json::Value>()
|
||||||
.await
|
.await
|
||||||
.ok()?
|
.ok()?
|
||||||
.get("universeId")
|
.get("universeId")?
|
||||||
.and_then(|v| v.as_u64())
|
.as_u64()
|
||||||
};
|
.unwrap()
|
||||||
|
|
||||||
let universe_id = match universe_id {
|
|
||||||
Some(id) => id,
|
|
||||||
None => return None.into(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let api_url = format!(
|
let api_url = format!(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect } 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,21 +62,10 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
const addToast = (message: string, type: ToastType = "info") => {
|
||||||
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={() => navigate(0)} />;
|
if (error) return <ErrorState message={error} onRetry={() => window.location.reload()} />;
|
||||||
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.dispatchEvent(new CustomEvent("unauthorized"));
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
throw new Error(`Request failed with status ${response.status}`);
|
throw new Error(`Request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,6 @@ 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>>();
|
||||||
@ -114,7 +108,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