forked from Roflin/gamenight
		
	Adds an AdminPanel with currently active registration tokens.
This commit is contained in:
		
							parent
							
								
									34737bfb6b
								
							
						
					
					
						commit
						65d2dece55
					
				
							
								
								
									
										4
									
								
								backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -238,6 +238,7 @@ dependencies = [
 | 
			
		||||
 "libc",
 | 
			
		||||
 "num-integer",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "time 0.1.44",
 | 
			
		||||
 "winapi 0.3.9",
 | 
			
		||||
]
 | 
			
		||||
@ -338,6 +339,7 @@ checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "bitflags",
 | 
			
		||||
 "byteorder",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "diesel_derives",
 | 
			
		||||
 "pq-sys",
 | 
			
		||||
 "r2d2",
 | 
			
		||||
@ -598,6 +600,7 @@ name = "gamenight"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "argon2",
 | 
			
		||||
 "base64",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "diesel",
 | 
			
		||||
 "diesel-derive-enum",
 | 
			
		||||
@ -605,6 +608,7 @@ dependencies = [
 | 
			
		||||
 "futures",
 | 
			
		||||
 "jsonwebtoken",
 | 
			
		||||
 "password-hash",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "rand_core",
 | 
			
		||||
 "rocket",
 | 
			
		||||
 "rocket_dyn_templates",
 | 
			
		||||
 | 
			
		||||
@ -10,10 +10,10 @@ edition = "2018"
 | 
			
		||||
rocket = { version = "0.5.0-rc.2", features = ["default", "json"] }
 | 
			
		||||
rocket_sync_db_pools = { version = "0.1.0-rc.2", features =  ["diesel_postgres_pool"] }
 | 
			
		||||
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["handlebars"] }
 | 
			
		||||
diesel = {version = "1.4.8", features = ["uuidv07", "r2d2", "postgres"]}
 | 
			
		||||
diesel = {version = "1.4.8", features = ["uuidv07", "r2d2", "postgres", "chrono"]}
 | 
			
		||||
diesel_migrations = "1.4.0"
 | 
			
		||||
diesel-derive-enum = { version = "1.1", features = ["postgres"] }
 | 
			
		||||
chrono = "0.4.19"
 | 
			
		||||
chrono = {version = "0.4.19", features = ["serde"] }
 | 
			
		||||
serde = "1.0.136"
 | 
			
		||||
password-hash = "0.4"
 | 
			
		||||
argon2 = "0.4"
 | 
			
		||||
@ -21,4 +21,6 @@ rand_core = { version = "0.6", features = ["std"] }
 | 
			
		||||
jsonwebtoken = "8.1"
 | 
			
		||||
validator = { version = "0.15", features = ["derive"] }
 | 
			
		||||
uuid = { version = "0.8.2", features = ["v4", "serde"] }
 | 
			
		||||
futures = "0.3.21"
 | 
			
		||||
futures = "0.3.21"
 | 
			
		||||
rand = "0.8.5"
 | 
			
		||||
base64 = "0.13.0"
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
-- This file should undo anything in `up.sql`
 | 
			
		||||
 | 
			
		||||
drop table registration_tokens;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE gamenight
 | 
			
		||||
ALTER datetime TYPE VARCHAR;
 | 
			
		||||
@ -0,0 +1,11 @@
 | 
			
		||||
-- Your SQL goes here
 | 
			
		||||
 | 
			
		||||
create table registration_tokens (
 | 
			
		||||
    id UUID PRIMARY KEY,
 | 
			
		||||
    token CHARACTER(32) NOT NULL,
 | 
			
		||||
    single_use BOOLEAN NOT NULL,
 | 
			
		||||
    expires TIMESTAMPTZ
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
ALTER TABLE gamenight
 | 
			
		||||
ALTER datetime TYPE TIMESTAMPTZ using datetime::timestamp;
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
use crate::schema;
 | 
			
		||||
use crate::schema::admin::RegistrationToken;
 | 
			
		||||
use crate::schema::gamenight::*;
 | 
			
		||||
use crate::schema::users::*;
 | 
			
		||||
use crate::schema::DatabaseError;
 | 
			
		||||
use crate::schema::DbConn;
 | 
			
		||||
use crate::AppConfig;
 | 
			
		||||
use chrono::DateTime;
 | 
			
		||||
use chrono::Utc;
 | 
			
		||||
use futures::future::join_all;
 | 
			
		||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
 | 
			
		||||
@ -31,6 +34,8 @@ pub enum ApiData {
 | 
			
		||||
    Gamenight(GamenightOutput),
 | 
			
		||||
    #[serde(rename = "games")]
 | 
			
		||||
    Games(Vec<Game>),
 | 
			
		||||
    #[serde(rename = "registration_tokens")]
 | 
			
		||||
    RegistrationTokens(Vec<RegistrationToken>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug)]
 | 
			
		||||
@ -94,6 +99,14 @@ impl ApiResponse {
 | 
			
		||||
            data: Some(ApiData::Games(games)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn registration_tokens_response(tokens: Vec<RegistrationToken>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            result: Self::SUCCES_RESULT,
 | 
			
		||||
            message: None,
 | 
			
		||||
            data: Some(ApiData::RegistrationTokens(tokens)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
@ -249,7 +262,7 @@ pub async fn gamenights_unauthorized() -> ApiResponseVariant {
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Clone)]
 | 
			
		||||
pub struct GamenightInput {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub datetime: String,
 | 
			
		||||
    pub datetime: DateTime<Utc>,
 | 
			
		||||
    pub owner_id: Option<Uuid>,
 | 
			
		||||
    pub game_list: Vec<Game>,
 | 
			
		||||
}
 | 
			
		||||
@ -474,3 +487,87 @@ pub async fn delete_participants(
 | 
			
		||||
pub async fn delete_participants_unauthorized() -> ApiResponseVariant {
 | 
			
		||||
    ApiResponseVariant::Status(Status::Unauthorized)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
pub struct RegistrationTokenData {
 | 
			
		||||
    single_use: bool,
 | 
			
		||||
    expires: Option<DateTime<Utc>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Into<RegistrationToken> for RegistrationTokenData {
 | 
			
		||||
    fn into(self) -> RegistrationToken {
 | 
			
		||||
        use rand::Rng;
 | 
			
		||||
        let random_bytes = rand::thread_rng().gen::<[u8; 24]>();
 | 
			
		||||
        RegistrationToken {
 | 
			
		||||
            id: Uuid::new_v4(),
 | 
			
		||||
            token: base64::encode_config(random_bytes, base64::URL_SAFE),
 | 
			
		||||
            single_use: self.single_use,
 | 
			
		||||
            expires: self.expires,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post(
 | 
			
		||||
    "/admin/registration_tokens",
 | 
			
		||||
    format = "application/json",
 | 
			
		||||
    data = "<token_json>"
 | 
			
		||||
)]
 | 
			
		||||
pub async fn add_registration_token(
 | 
			
		||||
    conn: DbConn,
 | 
			
		||||
    user: User,
 | 
			
		||||
    token_json: Json<RegistrationTokenData>,
 | 
			
		||||
) -> ApiResponseVariant {
 | 
			
		||||
    if user.role != Role::Admin {
 | 
			
		||||
        return ApiResponseVariant::Status(Status::Unauthorized);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match schema::admin::add_registration_token(&conn, token_json.into_inner().into()).await {
 | 
			
		||||
        Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
 | 
			
		||||
        Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[post("/admin/registration_tokens", rank = 2)]
 | 
			
		||||
pub async fn add_registration_token_unauthorized() -> ApiResponseVariant {
 | 
			
		||||
    ApiResponseVariant::Status(Status::Unauthorized)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/admin/registration_tokens")]
 | 
			
		||||
pub async fn get_registration_tokens(conn: DbConn, user: User) -> ApiResponseVariant {
 | 
			
		||||
    if user.role != Role::Admin {
 | 
			
		||||
        return ApiResponseVariant::Status(Status::Unauthorized);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match schema::admin::get_all_registration_tokens(&conn).await {
 | 
			
		||||
        Ok(results) => {
 | 
			
		||||
            ApiResponseVariant::Value(json!(ApiResponse::registration_tokens_response(results)))
 | 
			
		||||
        }
 | 
			
		||||
        Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/admin/registration_tokens", rank = 2)]
 | 
			
		||||
pub async fn get_registration_tokens_unauthorized() -> ApiResponseVariant {
 | 
			
		||||
    ApiResponseVariant::Status(Status::Unauthorized)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[delete("/admin/registration_tokens/<gamenight_id>")]
 | 
			
		||||
pub async fn delete_registration_tokens(
 | 
			
		||||
    conn: DbConn,
 | 
			
		||||
    user: User,
 | 
			
		||||
    gamenight_id: String,
 | 
			
		||||
) -> ApiResponseVariant {
 | 
			
		||||
    if user.role != Role::Admin {
 | 
			
		||||
        return ApiResponseVariant::Status(Status::Unauthorized);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let uuid = Uuid::parse_str(&gamenight_id).unwrap();
 | 
			
		||||
    match schema::admin::delete_registration_token(&conn, uuid).await {
 | 
			
		||||
        Ok(_) => ApiResponseVariant::Value(json!(ApiResponse::SUCCES)),
 | 
			
		||||
        Err(err) => ApiResponseVariant::Value(json!(ApiResponse::error(err.to_string()))),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
#[delete("/admin/registration_tokens", rank = 2)]
 | 
			
		||||
pub async fn delete_registration_tokens_unauthorized() -> ApiResponseVariant {
 | 
			
		||||
    ApiResponseVariant::Status(Status::Unauthorized)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,12 @@ async fn rocket() -> _ {
 | 
			
		||||
                api::post_participants_unauthorized,
 | 
			
		||||
                api::delete_participants,
 | 
			
		||||
                api::delete_participants_unauthorized,
 | 
			
		||||
                api::add_registration_token,
 | 
			
		||||
                api::add_registration_token_unauthorized,
 | 
			
		||||
                api::get_registration_tokens,
 | 
			
		||||
                api::get_registration_tokens_unauthorized,
 | 
			
		||||
                api::delete_registration_tokens,
 | 
			
		||||
                api::delete_registration_tokens_unauthorized,
 | 
			
		||||
            ],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										54
									
								
								backend/src/schema/admin.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								backend/src/schema/admin.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
			
		||||
use crate::schema::{DatabaseError, DbConn};
 | 
			
		||||
use chrono::DateTime;
 | 
			
		||||
use chrono::Utc;
 | 
			
		||||
use diesel::{QueryDsl, RunQueryDsl, ExpressionMethods};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
 | 
			
		||||
table! {
 | 
			
		||||
    registration_tokens (id) {
 | 
			
		||||
        id -> diesel::sql_types::Uuid,
 | 
			
		||||
        token -> Char,
 | 
			
		||||
        single_use -> Bool,
 | 
			
		||||
        expires -> Nullable<Timestamptz>,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)]
 | 
			
		||||
#[table_name = "registration_tokens"]
 | 
			
		||||
pub struct RegistrationToken {
 | 
			
		||||
    pub id: Uuid,
 | 
			
		||||
    pub token: String,
 | 
			
		||||
    pub single_use: bool,
 | 
			
		||||
    pub expires: Option<DateTime<Utc>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn get_all_registration_tokens(
 | 
			
		||||
    conn: &DbConn,
 | 
			
		||||
) -> Result<Vec<RegistrationToken>, DatabaseError> {
 | 
			
		||||
    Ok(conn
 | 
			
		||||
        .run(|c| registration_tokens::table.load::<RegistrationToken>(c))
 | 
			
		||||
        .await?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn add_registration_token(
 | 
			
		||||
    conn: &DbConn,
 | 
			
		||||
    token: RegistrationToken,
 | 
			
		||||
) -> Result<usize, DatabaseError> {
 | 
			
		||||
    Ok(conn
 | 
			
		||||
        .run(|c| {
 | 
			
		||||
            diesel::insert_into(registration_tokens::table)
 | 
			
		||||
                .values(token)
 | 
			
		||||
                .execute(c)
 | 
			
		||||
        })
 | 
			
		||||
        .await?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn delete_registration_token(conn: &DbConn, id: Uuid) -> Result<usize, DatabaseError> {
 | 
			
		||||
    Ok(conn
 | 
			
		||||
        .run(move |c| {
 | 
			
		||||
            diesel::delete(registration_tokens::table.filter(registration_tokens::id.eq(id)))
 | 
			
		||||
                .execute(c)
 | 
			
		||||
        })
 | 
			
		||||
        .await?)
 | 
			
		||||
}
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
use crate::schema::users::{users, User};
 | 
			
		||||
use crate::schema::{DatabaseError, DbConn};
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl};
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use uuid::Uuid;
 | 
			
		||||
@ -8,7 +9,7 @@ table! {
 | 
			
		||||
    gamenight (id) {
 | 
			
		||||
        id -> diesel::sql_types::Uuid,
 | 
			
		||||
        name -> VarChar,
 | 
			
		||||
        datetime -> VarChar,
 | 
			
		||||
        datetime -> Timestamptz,
 | 
			
		||||
        owner_id -> Uuid,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -46,7 +47,7 @@ pub struct Game {
 | 
			
		||||
pub struct Gamenight {
 | 
			
		||||
    pub id: Uuid,
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub datetime: String,
 | 
			
		||||
    pub datetime: DateTime<Utc>,
 | 
			
		||||
    pub owner_id: Uuid,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
pub mod admin;
 | 
			
		||||
pub mod gamenight;
 | 
			
		||||
pub mod users;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,32 +4,56 @@ import MenuBar from './components/MenuBar';
 | 
			
		||||
import Login from './components/Login';
 | 
			
		||||
import Gamenights from './components/Gamenights';
 | 
			
		||||
import Gamenight from './components/Gamenight';
 | 
			
		||||
import AdminPanel from './components/AdminPanel';
 | 
			
		||||
 | 
			
		||||
import { get_gamenights, get_games, unpack_api_result, login } from './api/Api';
 | 
			
		||||
 | 
			
		||||
const localStorageUserKey = 'user';
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
 | 
			
		||||
  const [user, setUser] = useState(null);
 | 
			
		||||
  const [gamenights, setGamenights] = useState([]);
 | 
			
		||||
  const [flashData, setFlashData] = useState({});
 | 
			
		||||
  const [games, setGames] = useState([]);
 | 
			
		||||
  const [activeGamenightId, setActiveGamenightId] = useState(null);
 | 
			
		||||
  const [appState, setAppState] = useState('LoggedOut')
 | 
			
		||||
 | 
			
		||||
  const handleLogin = (input) => {
 | 
			
		||||
    unpack_api_result(login(input), setFlashData)
 | 
			
		||||
      .then(result => {
 | 
			
		||||
        setUser(result.user);
 | 
			
		||||
        localStorage.setItem(localStorageUserKey, JSON.stringify(result.user));
 | 
			
		||||
      });
 | 
			
		||||
      })
 | 
			
		||||
      .then(() => setAppState('LoggedIn'))
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if(activeGamenightId !== null) {
 | 
			
		||||
      setAppState('GamenightDetails');
 | 
			
		||||
    } else {
 | 
			
		||||
      setAppState('LoggedIn')
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
  }, [activeGamenightId])
 | 
			
		||||
 | 
			
		||||
  const onLogout = () => {
 | 
			
		||||
    setUser(null);
 | 
			
		||||
    localStorage.removeItem(localStorageUserKey);
 | 
			
		||||
    setAppState('LoggedOut')
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onAdmin = () => {
 | 
			
		||||
    setAppState('AdminPanel')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onUser = () => {
 | 
			
		||||
    setAppState('UserPage')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onReset = () => {
 | 
			
		||||
    setAppState('LoggedIn')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const setFlash = (data) => {
 | 
			
		||||
    setFlashData(data);
 | 
			
		||||
  };
 | 
			
		||||
@ -38,64 +62,67 @@ function App() {
 | 
			
		||||
    setUser({...user});
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const dismissActiveGamenight = () => {
 | 
			
		||||
    setActiveGamenightId(null);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (user !== null) {
 | 
			
		||||
    if (appState === 'LoggedIn') {
 | 
			
		||||
      unpack_api_result(get_gamenights(user.jwt), setFlashData)
 | 
			
		||||
        .then(result => setGamenights(result.gamenights));
 | 
			
		||||
    }
 | 
			
		||||
  }, [user])
 | 
			
		||||
  }, [appState])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (user !== null) {
 | 
			
		||||
    if (appState === 'LoggedIn') {
 | 
			
		||||
      unpack_api_result(get_games(user.jwt), setFlashData)
 | 
			
		||||
        .then(result => setGames(result.games));
 | 
			
		||||
    }        
 | 
			
		||||
  }, [user])
 | 
			
		||||
  }, [appState])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setUser(JSON.parse(localStorage.getItem(localStorageUserKey)));
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  let page;
 | 
			
		||||
  if(user === null) {
 | 
			
		||||
    page = (
 | 
			
		||||
  let mainview;
 | 
			
		||||
  if(appState === 'LoggedOut') {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="App">
 | 
			
		||||
        <Login onChange={handleLogin}/>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    let mainview;
 | 
			
		||||
    if(activeGamenightId === null) {
 | 
			
		||||
      mainview = (
 | 
			
		||||
        <Gamenights 
 | 
			
		||||
          user={user}
 | 
			
		||||
          games={games}
 | 
			
		||||
          setFlash={setFlash}
 | 
			
		||||
          refetchGamenights={refetchGamenights}
 | 
			
		||||
          gamenights={gamenights}
 | 
			
		||||
          onSelectGamenight={(g) => setActiveGamenightId(g.id)}/>
 | 
			
		||||
      )
 | 
			
		||||
    } else {
 | 
			
		||||
      mainview = (
 | 
			
		||||
      <Gamenight
 | 
			
		||||
        gamenightId={activeGamenightId}
 | 
			
		||||
        onDismis={dismissActiveGamenight}
 | 
			
		||||
        setFlash={setFlash}
 | 
			
		||||
  } else if(appState === 'GamenightDetails') {
 | 
			
		||||
    mainview = (
 | 
			
		||||
    <Gamenight
 | 
			
		||||
      gamenightId={activeGamenightId}
 | 
			
		||||
      onDismis={() => setActiveGamenightId(null)}
 | 
			
		||||
      setFlash={setFlash}
 | 
			
		||||
      user={user}
 | 
			
		||||
    />)
 | 
			
		||||
  } else if(appState === 'LoggedIn') {
 | 
			
		||||
    mainview = (
 | 
			
		||||
      <Gamenights 
 | 
			
		||||
        user={user}
 | 
			
		||||
      />)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    page = (
 | 
			
		||||
      <>
 | 
			
		||||
        <MenuBar user={user} onLogout={onLogout} />
 | 
			
		||||
        {mainview}
 | 
			
		||||
      </>
 | 
			
		||||
        games={games}
 | 
			
		||||
        setFlash={setFlash}
 | 
			
		||||
        refetchGamenights={refetchGamenights}
 | 
			
		||||
        gamenights={gamenights}
 | 
			
		||||
        onSelectGamenight={(g) => setActiveGamenightId(g.id)}/>
 | 
			
		||||
    );
 | 
			
		||||
  } else if(appState === 'AdminPanel') {
 | 
			
		||||
    mainview = (
 | 
			
		||||
      <AdminPanel
 | 
			
		||||
        user={user}
 | 
			
		||||
        setFlash={setFlash}/>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  let page = (
 | 
			
		||||
    <>
 | 
			
		||||
      <MenuBar
 | 
			
		||||
        user={user}
 | 
			
		||||
        onUser={onUser}
 | 
			
		||||
        onAdmin={onAdmin}
 | 
			
		||||
        onLogout={onLogout}
 | 
			
		||||
        onReset={onReset}/>
 | 
			
		||||
      {mainview}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return page;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,4 +87,33 @@ export function login(body) {
 | 
			
		||||
    },
 | 
			
		||||
    body: JSON.stringify(body)
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function get_registration_tokens(token) {
 | 
			
		||||
  return fetchResource('api/admin/registration_tokens', {
 | 
			
		||||
    method: 'GET',
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Authorization': `Bearer ${token}`
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function add_registration_token(token, registration_token) {
 | 
			
		||||
  return fetchResource('api/admin/registration_tokens', {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Content-Type': 'application/json',
 | 
			
		||||
      'Authorization': `Bearer ${token}`
 | 
			
		||||
    },
 | 
			
		||||
    body: JSON.stringify(registration_token)
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function delete_registration_token(token, registration_token_id) {
 | 
			
		||||
  return fetchResource(`api/admin/registration_tokens/${registration_token_id}`, {
 | 
			
		||||
    method: 'DELETE',
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Authorization': `Bearer ${token}`,
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										178
									
								
								frontend/src/components/AdminPanel.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								frontend/src/components/AdminPanel.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
			
		||||
import {useState, useEffect} from 'react';
 | 
			
		||||
import Checkbox from '@mui/material/Checkbox';
 | 
			
		||||
import Table from '@mui/material/Table';
 | 
			
		||||
import TableBody from '@mui/material/TableBody';
 | 
			
		||||
import TableCell from '@mui/material/TableCell';
 | 
			
		||||
import TableContainer from '@mui/material/TableContainer';
 | 
			
		||||
import TableHead from '@mui/material/TableHead';
 | 
			
		||||
import TablePagination from '@mui/material/TablePagination';
 | 
			
		||||
import TableRow from '@mui/material/TableRow';
 | 
			
		||||
import Button from '@mui/material/Button';
 | 
			
		||||
import TextField from '@mui/material/TextField';
 | 
			
		||||
import IconButton from '@mui/material/IconButton';
 | 
			
		||||
import DeleteIcon from '@mui/icons-material/Delete';
 | 
			
		||||
import { DateTimePicker } from '@mui/x-date-pickers';
 | 
			
		||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
 | 
			
		||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
 | 
			
		||||
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
import {get_registration_tokens, add_registration_token, delete_registration_token, unpack_api_result} from '../api/Api';
 | 
			
		||||
 | 
			
		||||
function AdminPanel(props) {
 | 
			
		||||
 | 
			
		||||
  const [page, setPage] = useState(0);
 | 
			
		||||
  const [rowsPerPage, setRowsPerPage] = useState(10);
 | 
			
		||||
  const [registrationTokens, setRegistrationTokens] = useState([]);
 | 
			
		||||
  const [expires, setExpires] = useState(null);
 | 
			
		||||
  const [isSingleUse, setIsSingleUse] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const handleChangePage = (event, newPage) => {
 | 
			
		||||
    setPage(newPage);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleChangeRowsPerPage = (event) => {
 | 
			
		||||
    setRowsPerPage(+event.target.value);
 | 
			
		||||
    setPage(0);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const refetchTokens = () => {
 | 
			
		||||
    if(props.user !== null) {
 | 
			
		||||
      unpack_api_result(get_registration_tokens(props.user.jwt), props.setFlash)
 | 
			
		||||
        .then(result => setRegistrationTokens(result.registration_tokens));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const deleteToken = (id) => {
 | 
			
		||||
    if(props.user !== null) {
 | 
			
		||||
      unpack_api_result(delete_registration_token(props.user.jwt, id), props.setFlash)
 | 
			
		||||
        .then(() => refetchTokens())
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleAddToken = () => {
 | 
			
		||||
    let input = {
 | 
			
		||||
      single_use: isSingleUse,
 | 
			
		||||
      expires: expires,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(props.user !== null) {
 | 
			
		||||
      unpack_api_result(add_registration_token(props.user.jwt, input), props.setFlash)
 | 
			
		||||
        .then(() => refetchTokens())
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    refetchTokens()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  let columns = [
 | 
			
		||||
    {
 | 
			
		||||
      id: 'single_use', 
 | 
			
		||||
      label: 'Single Use',
 | 
			
		||||
      minWidth: 30,
 | 
			
		||||
      format: value => (value ? "Yes" : "No")
 | 
			
		||||
    },
 | 
			
		||||
    { id: 'token', label: 'Token', minwidht: 300},
 | 
			
		||||
    {
 | 
			
		||||
      id: 'expires',
 | 
			
		||||
      label: 'Expires',
 | 
			
		||||
      minwidth: 200,
 | 
			
		||||
      format: value => (moment(value).format('LL HH:mm'))
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'delete_button',
 | 
			
		||||
      label: '',
 | 
			
		||||
      minwidth: 20,
 | 
			
		||||
    }
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <LocalizationProvider dateAdapter={AdapterDateFns}>
 | 
			
		||||
        <div className="Add-GameNight">
 | 
			
		||||
          <form autoComplete="off" onSubmit={e => { e.preventDefault(); }}>
 | 
			
		||||
            <DateTimePicker
 | 
			
		||||
              label="Gamenight date and time"
 | 
			
		||||
              variant="standard"
 | 
			
		||||
              value={expires}
 | 
			
		||||
              onChange={setExpires}
 | 
			
		||||
              inputFormat="dd-MM-yyyy HH:mm"
 | 
			
		||||
              renderInput={(params) => <TextField {...params} />}/> 
 | 
			
		||||
            
 | 
			
		||||
            <Checkbox 
 | 
			
		||||
              label="Single use"
 | 
			
		||||
              value={isSingleUse}
 | 
			
		||||
              onChange={(e) => setIsSingleUse(e.target.checked)}/>
 | 
			
		||||
 | 
			
		||||
            <Button 
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              color="success"
 | 
			
		||||
              onClick={handleAddToken}>
 | 
			
		||||
              Create
 | 
			
		||||
            </Button>
 | 
			
		||||
          </form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </LocalizationProvider>
 | 
			
		||||
 | 
			
		||||
      <TableContainer sx={{ maxHeight: 440 }}>
 | 
			
		||||
        <Table stickyHeader>
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              {columns.map((column) => (
 | 
			
		||||
                <TableCell
 | 
			
		||||
                  key={column.id}
 | 
			
		||||
                  align={column.align}
 | 
			
		||||
                  style={{ minWidth: column.minWidth }}
 | 
			
		||||
                >
 | 
			
		||||
                  {column.label}
 | 
			
		||||
                </TableCell>
 | 
			
		||||
              ))}
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {registrationTokens
 | 
			
		||||
              .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
 | 
			
		||||
              .map((row) => {
 | 
			
		||||
                return (
 | 
			
		||||
                  <TableRow hover role="checkbox" tabIndex={-1} key={row.code}>
 | 
			
		||||
                    {columns.map((column) => {
 | 
			
		||||
                      const value = row[column.id];
 | 
			
		||||
                      return (
 | 
			
		||||
                        <TableCell key={column.id} align={column.align}>
 | 
			
		||||
                          {column.format
 | 
			
		||||
                            ? column.format(value)
 | 
			
		||||
                            : value}
 | 
			
		||||
                        </TableCell>
 | 
			
		||||
                      );
 | 
			
		||||
                    })}
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      <IconButton 
 | 
			
		||||
                        edge="end"
 | 
			
		||||
                        color="error"
 | 
			
		||||
                        onClick={(e) => {
 | 
			
		||||
                          e.stopPropagation();
 | 
			
		||||
                          deleteToken(row.id)
 | 
			
		||||
                        }}>
 | 
			
		||||
          <DeleteIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                  </TableRow>
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </TableContainer>
 | 
			
		||||
      {registrationTokens.length > rowsPerPage && <TablePagination
 | 
			
		||||
        rowsPerPageOptions={[10, 25, 100]}
 | 
			
		||||
        component="div"
 | 
			
		||||
        count={registrationTokens.length}
 | 
			
		||||
        rowsPerPage={rowsPerPage}
 | 
			
		||||
        page={page}
 | 
			
		||||
        onPageChange={handleChangePage}
 | 
			
		||||
        onRowsPerPageChange={handleChangeRowsPerPage}/>
 | 
			
		||||
      }
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AdminPanel
 | 
			
		||||
@ -7,6 +7,29 @@ import IconButton from '@mui/material/IconButton';
 | 
			
		||||
import MenuIcon from '@mui/icons-material/Menu';
 | 
			
		||||
 | 
			
		||||
function MenuBar(props) {
 | 
			
		||||
 | 
			
		||||
  let adminPanelButton = null;
 | 
			
		||||
  if (props.user?.role === 'Admin') {
 | 
			
		||||
    adminPanelButton = (
 | 
			
		||||
      <Button 
 | 
			
		||||
        color="inherit" 
 | 
			
		||||
        onClick={props.onAdmin}>
 | 
			
		||||
          AdminPanel
 | 
			
		||||
      </Button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let userButton = null;
 | 
			
		||||
  if (props.user != null) {
 | 
			
		||||
    userButton = (
 | 
			
		||||
      <Button 
 | 
			
		||||
        color="inherit" 
 | 
			
		||||
        onClick={props.onUser}>
 | 
			
		||||
          {props.user.username}
 | 
			
		||||
      </Button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AppBar position="static">
 | 
			
		||||
      <Toolbar>
 | 
			
		||||
@ -15,16 +38,19 @@ function MenuBar(props) {
 | 
			
		||||
          edge="start"
 | 
			
		||||
          color="inherit"
 | 
			
		||||
          aria-label="menu"
 | 
			
		||||
          sx={{ mr: 2 }}
 | 
			
		||||
        >
 | 
			
		||||
          sx={{ mr: 2 }}>
 | 
			
		||||
          <MenuIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
        <Typography
 | 
			
		||||
          style={{cursor:'pointer'}}
 | 
			
		||||
          variant="h6"
 | 
			
		||||
          component="div"
 | 
			
		||||
          sx={{ flexGrow: 1 }}>
 | 
			
		||||
          sx={{ flexGrow: 1 }}
 | 
			
		||||
          onClick={props.onReset}>
 | 
			
		||||
            Gamenight!
 | 
			
		||||
        </Typography>
 | 
			
		||||
        {userButton !== null && userButton}
 | 
			
		||||
        {adminPanelButton !== null && adminPanelButton}
 | 
			
		||||
        <Button 
 | 
			
		||||
          color="inherit" 
 | 
			
		||||
          onClick={props.onLogout}>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user