From 3818fbf460ed7ef898d3f1811a7a1e7acaa73743 Mon Sep 17 00:00:00 2001 From: hwinkel Date: Tue, 18 Nov 2025 22:23:08 +0100 Subject: [PATCH] ADD: added function to update db scheme automaticly --- backend/internal/auth/handler.go | 2 +- backend/internal/database/initTables.go | 29 +- backend/internal/database/migrateScheme.go | 188 ++++++++ backend/internal/player/handler.go | 25 +- backend/internal/player/model.go | 265 +++++------ frontend/src/App.tsx | 2 + .../interfaces}/types.tsx | 0 frontend/src/components/interfaces/users.tsx | 60 +++ frontend/src/pages/Administration.tsx | 421 ++++++++++++++++++ frontend/src/pages/Navigation.tsx | 3 +- frontend/src/pages/Players.tsx | 40 +- frontend/src/pages/TournamentDetails.tsx | 2 +- frontend/src/pages/Tournaments.tsx | 2 +- frontend/src/pages/ViewEditPlayer.tsx | 21 +- frontend/src/pages/api.tsx | 2 +- 15 files changed, 876 insertions(+), 186 deletions(-) create mode 100644 backend/internal/database/migrateScheme.go rename frontend/src/{pages => components/interfaces}/types.tsx (100%) create mode 100644 frontend/src/components/interfaces/users.tsx create mode 100644 frontend/src/pages/Administration.tsx diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 6f6b290..541b308 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -55,7 +55,7 @@ func LoginHandler(c *gin.Context, db *sql.DB) { return } // Create JWT token - token, err = CreateJWT(loggedInPlayer.ID, req.Email, "player", 60*time.Minute) + token, err = CreateJWT(loggedInPlayer.UUID, req.Email, "player", 60*time.Minute) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Token creation error"}) return diff --git a/backend/internal/database/initTables.go b/backend/internal/database/initTables.go index 055652b..324e7f8 100644 --- a/backend/internal/database/initTables.go +++ b/backend/internal/database/initTables.go @@ -39,22 +39,25 @@ func CheckIfTablesExist(db *sql.DB) (bool, error) { return true, nil // All tables exist } -func tableExists(db *sql.DB, tableName string) (bool, error) { - query := ` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' AND table_name = $1 - ); - ` - var exists bool - err := db.QueryRow(query, tableName).Scan(&exists) - return exists, err -} +// func tableExists(db *sql.DB, tableName string) (bool, error) { +// query := ` +// SELECT EXISTS ( +// SELECT FROM information_schema.tables +// WHERE table_schema = 'public' AND table_name = $1 +// ); +// ` +// var exists bool +// err := db.QueryRow(query, tableName).Scan(&exists) +// return exists, err +// } func InitTables(d *sql.DB) error { + CreateOrUpdateTablePG(d, "users", player.User{}) + CreateOrUpdateTablePG(d, "roles", player.Roles{}) + tables := []string{ - player.PlayerTable, - player.RoleTable, + // player.PlayerTable, + // player.RoleTable, teamTable, } diff --git a/backend/internal/database/migrateScheme.go b/backend/internal/database/migrateScheme.go new file mode 100644 index 0000000..29494a5 --- /dev/null +++ b/backend/internal/database/migrateScheme.go @@ -0,0 +1,188 @@ +package database + +import ( + "database/sql" + "fmt" + "log" + "reflect" + "strings" +) + +// Auto-create table, columns, types, indexes for PostgreSQL +func CreateOrUpdateTablePG(db *sql.DB, table string, model interface{}) error { + // 0) Check if table exists + exists, err := tableExists(db, table) + if err != nil { + return err + } + + if !exists { + // Create table from model + if err := createTable(db, table, model); err != nil { + return err + } + return nil + } + + // Update table if it exists + return alterTable(db, table, model) +} + +func tableExists(db *sql.DB, table string) (bool, error) { + query := `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name=$1);` + var exists bool + err := db.QueryRow(query, table).Scan(&exists) + return exists, err +} + +func createTable(db *sql.DB, table string, model interface{}) error { + t := reflect.TypeOf(model) + if t.Kind() != reflect.Struct { + return fmt.Errorf("model must be a struct") + } + + cols := []string{} + idx := []string{} + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + + // ignore fields not meant for DB + if f.Tag.Get("ignore") == "true" || f.Tag.Get("db") == "-" { + continue + } + + col := f.Tag.Get("db") + if col == "" { + col = strings.ToLower(f.Name) + } + sqlType := f.Tag.Get("sql") + if sqlType == "" { + return fmt.Errorf("field '%s' missing sql tag", f.Name) + } + + cols = append(cols, fmt.Sprintf("\"%s\" %s", col, sqlType)) + + if f.Tag.Get("index") == "true" { + idx = append(idx, col) + } + } + + query := fmt.Sprintf(`CREATE TABLE "%s" (%s);`, table, strings.Join(cols, ", ")) + log.Println("[CREATE TABLE]", query) + + if _, err := db.Exec(query); err != nil { + return err + } + + // Create indexes + for _, col := range idx { + indexName := fmt.Sprintf("%s_%s_idx", table, col) + iq := fmt.Sprintf(`CREATE INDEX "%s" ON "%s" ("%s");`, indexName, table, col) + log.Println("[CREATE INDEX]", iq) + if _, err := db.Exec(iq); err != nil { + return err + } + } + + return nil +} + +func alterTable(db *sql.DB, table string, model interface{}) error { + currentCols, err := getColumnsPG(db, table) + if err != nil { + return err + } + + currentIdx, err := getIndexesPG(db, table) + if err != nil { + return err + } + + t := reflect.TypeOf(model) + + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + col := f.Tag.Get("db") + if col == "" { + col = strings.ToLower(f.Name) + } + sqlType := f.Tag.Get("sql") + + // Missing column + if _, ok := currentCols[col]; !ok { + query := fmt.Sprintf(`ALTER TABLE "%s" ADD COLUMN "%s" %s;`, table, col, sqlType) + log.Println("[ADD COLUMN]", query) + if _, err := db.Exec(query); err != nil { + return err + } + continue + } + + // Type mismatch → alter + pgType := strings.ToLower(currentCols[col]) + expType := strings.ToLower(sqlType) + + if !strings.Contains(pgType, strings.Split(expType, "(")[0]) { + query := fmt.Sprintf(`ALTER TABLE "%s" ALTER COLUMN "%s" TYPE %s;`, table, col, sqlType) + log.Println("[ALTER TYPE]", query) + if _, err := db.Exec(query); err != nil { + return err + } + } + + // Indexing + if f.Tag.Get("index") == "true" { + idxName := fmt.Sprintf("%s_%s_idx", table, col) + + if !currentIdx[idxName] { + iq := fmt.Sprintf(`CREATE INDEX "%s" ON "%s" ("%s");`, idxName, table, col) + log.Println("[CREATE INDEX]", iq) + if _, err := db.Exec(iq); err != nil { + return err + } + } + } + } + + return nil +} + +// Helpers +func getColumnsPG(db *sql.DB, table string) (map[string]string, error) { + q := `SELECT column_name, data_type FROM information_schema.columns WHERE table_name=$1;` + rows, err := db.Query(q, table) + if err != nil { + return nil, err + } + defer rows.Close() + + cols := map[string]string{} + for rows.Next() { + var name, dtype string + if err := rows.Scan(&name, &dtype); err != nil { + return nil, err + } + cols[name] = dtype + } + return cols, nil +} + +func getIndexesPG(db *sql.DB, table string) (map[string]bool, error) { + q := `SELECT indexname FROM pg_indexes WHERE tablename=$1;` + rows, err := db.Query(q, table) + if err != nil { + return nil, err + } + defer rows.Close() + + idx := map[string]bool{} + for rows.Next() { + var i string + if err := rows.Scan(&i); err != nil { + return nil, err + } + idx[i] = true + } + return idx, nil +} diff --git a/backend/internal/player/handler.go b/backend/internal/player/handler.go index 5ac189f..c130654 100644 --- a/backend/internal/player/handler.go +++ b/backend/internal/player/handler.go @@ -32,35 +32,36 @@ func GetPlayers(c *gin.Context, db *sql.DB) { } func CreatePlayer(c *gin.Context, db *sql.DB) { - var newPlayer Player + var newPlayer User var err error if err := c.ShouldBindJSON(&newPlayer); err != nil { log.Printf("Error binding player data: %v", err) common.RespondError(c, http.StatusBadRequest, "Invalid player data") return } - newPlayer.Password, err = common.HashPassword(newPlayer.Password) + newPlayer.password, err = common.HashPassword(newPlayer.password) if err != nil { log.Printf("Error hashing password: %v", err) common.RespondError(c, http.StatusInternalServerError, "Failed to hash password") return } - newPlayer.ID = uuid.New().String() + newPlayer.UUID = uuid.New().String() err = savePlayer(db, newPlayer) if err != nil { log.Printf("Error saving player: %v", err) common.RespondError(c, http.StatusInternalServerError, "Failed to create player") return } - err = AddRoleToPlayer(db, newPlayer.ID, "player") + var roles = []string{"player"} + err = AddRoleToPlayer(db, newPlayer.UUID, roles) if err != nil { log.Printf("Error adding role to player: %v", err) common.RespondError(c, http.StatusInternalServerError, "Failed to assign role to player") return } - log.Printf("User %s (%s) created player: %s", c.GetString("userId"), c.GetString("email"), newPlayer.Name) + log.Printf("User %s (%s) created player: %s", c.GetString("userId"), c.GetString("email"), newPlayer.Username) common.RespondCreated(c, newPlayer) } @@ -80,19 +81,19 @@ func GetPlayer(c *gin.Context, db *sql.DB, id string) { return } - if player.ID == "" { + if player.UUID == "" { log.Printf("Player with ID %s not found", playerID) common.RespondError(c, http.StatusNotFound, "Player not found") return } - role, err := GetPlayerRole(db, player.ID) + role, err := GetPlayerRole(db, player.UUID) if err != nil { - log.Printf("Error retrieving role for player ID %s: %v", player.ID, err) + log.Printf("Error retrieving role for player ID %s: %v", player.UUID, err) common.RespondError(c, http.StatusInternalServerError, "Failed to retrieve player role") return } player.Role = role - log.Printf(player.ID, player.Name, player.Email, player.Role) + log.Printf(player.UUID, player.Username, player.Email, player.Role) c.JSON(http.StatusOK, player) } @@ -109,14 +110,14 @@ func UpdatePlayer(c *gin.Context, db *sql.DB) { return } - var updatedPlayer Player + var updatedPlayer User if err := c.ShouldBindJSON(&updatedPlayer); err != nil { log.Printf("Error binding player data: %v", err) common.RespondError(c, http.StatusBadRequest, "Invalid player data") return } - updatedPlayer.ID = playerID + updatedPlayer.UUID = playerID err := updatePlayer(db, updatedPlayer) if err != nil { log.Printf("Error updating player with ID %s: %v", playerID, err) @@ -124,7 +125,7 @@ func UpdatePlayer(c *gin.Context, db *sql.DB) { return } - log.Printf("User %s (%s) updated player: %s", c.GetString("userId"), c.GetString("email"), updatedPlayer.Name) + log.Printf("User %s (%s) updated player: %s", c.GetString("userId"), c.GetString("email"), updatedPlayer.Username) c.JSON(http.StatusOK, updatedPlayer) } diff --git a/backend/internal/player/model.go b/backend/internal/player/model.go index 661aaa2..964a154 100644 --- a/backend/internal/player/model.go +++ b/backend/internal/player/model.go @@ -3,31 +3,66 @@ package player import ( "database/sql" "log" + "strings" + "time" "github.com/google/uuid" ) -const PlayerTable = ` - CREATE TABLE IF NOT EXISTS players ( - id uuid PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(100) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL - ); - ` -const RoleTable = ` - CREATE TABLE IF NOT EXISTS roles ( - id SERIAL PRIMARY KEY, - player_id uuid NOT NULL, - role VARCHAR(50) NOT NULL CHECK (role IN ('player', 'admin')), - FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE - ); -` +type User struct { + UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"` + Username string `db:"username" sql:"VARCHAR(100)"` + Email string `db:"email" sql:"VARCHAR(255)" index:"true"` + lastname string `db:"lastname" sql:"VARCHAR(100)"` + fistname string `db:"fistname" sql:"VARCHAR(100)"` + password string `db:"password_hash" sql:"VARCHAR(255)"` + phone string `db:"phone" sql:"VARCHAR(20)"` + avatarURL string `db:"avatar_url" sql:"VARCHAR(255)"` + IsActive bool `db:"is_active" sql:"BOOLEAN"` + birthday time.Time `db:"birthday" sql:"DATE"` + createdAt time.Time `db:"created_at" sql:"TIMESTAMP"` + updatedAt time.Time `db:"updated_at" sql:"TIMESTAMP"` + LastLogin time.Time `db:"last_login" sql:"TIMESTAMP"` + Role []string `ignore:"true"` // wird NICHT in der DB angelegt + +} + +type Roles struct { + ID int `db:"id" sql:"SERIAL PRIMARY KEY"` + PlayerID string `db:"player_id" sql:"UUID"` + Role []string `db:"role" sql:"TEXT[]"` +} + +// const PlayerTable = ` +// CREATE TABLE IF NOT EXISTS users ( +// id uuid PRIMARY KEY, +// username VARCHAR(100) NOT NULL, +// lastname VARCHAR(100) NOT NULL, +// fistname VARCHAR(100) NOT NULL, +// email VARCHAR(100) NOT NULL UNIQUE, +// password_hash VARCHAR(255) NOT NULL, +// created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +// updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +// last_login TIMESTAMP, +// phone VARCHAR(20), +// avatar_url VARCHAR(255), +// is_active BOOLEAN NOT NULL DEFAULT TRUE +// ); +// ` + +// const RoleTable = ` +// CREATE TABLE IF NOT EXISTS roles ( +// id SERIAL PRIMARY KEY, +// player_id uuid NOT NULL, +// role VARCHAR(50) NOT NULL CHECK (role IN ('player', 'admin')), +// FOREIGN KEY (player_id) REFERENCES users(id) ON DELETE CASCADE +// ); +//` type Team struct { - ID string `json:"id"` - Name string `json:"name"` - Players []Player `json:"players"` // Players in the team + ID string `json:"id"` + Name string `json:"name"` + Players []User `json:"users"` // Players in the team // Matches []Match `json:"matches"` // Matches the team is involved in MatchesWon int `json:"matchesWon"` // Number of matches won by the team MatchesLost int `json:"matchesLost"` // Number of matches lost by the team @@ -36,22 +71,22 @@ type Team struct { MatchesPlayedCount int `json:"matchesPlayedCount"` // Total matches played by the team } -type Player struct { - ID string `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - Password string `json:"password,omitempty"` // Password is optional for responses - Role string `json:"role"` // e.g., "player","organizer" - Teams []Team `json:"teams"` // Teams the player is part of - // Tournaments []Tournament `json:"tournaments"` // Tournaments the player is registered in - OwnedTournaments []string `json:"ownedTournaments"` // Tournaments the player is organizing - ParticipatedTournaments []string `json:"participatedTournaments"` // Tournaments the player is participating in -} +// type Player struct { +// ID string `json:"id"` +// Name string `json:"name"` +// Email string `json:"email"` +// Password string `json:"password,omitempty"` // Password is optional for responses +// Role string `json:"role"` // e.g., "player","organizer" +// Teams []Team `json:"teams"` // Teams the player is part of +// // Tournaments []Tournament `json:"tournaments"` // Tournaments the player is registered in +// OwnedTournaments []string `json:"ownedTournaments"` // Tournaments the player is organizing +// ParticipatedTournaments []string `json:"participatedTournaments"` // Tournaments the player is participating in +// } type PlayerListResponse struct { - Players []Player `json:"players"` + Players []User `json:"users"` } type PlayerResponse struct { - Player Player `json:"player"` + Player User `json:"player"` } type CreatePlayerRequest struct { Name string `json:"name" binding:"required"` @@ -68,75 +103,79 @@ type DeletePlayerRequest struct { ID string `json:"id" binding:"required"` } type PlayerService interface { - CreatePlayer(req CreatePlayerRequest) (Player, error) - GetPlayer(id string) (Player, error) - UpdatePlayer(id string, req UpdatePlayerRequest) (Player, error) + CreatePlayer(req CreatePlayerRequest) (User, error) + GetPlayer(id string) (User, error) + UpdatePlayer(id string, req UpdatePlayerRequest) (User, error) DeletePlayer(id string) error - ListPlayers() ([]Player, error) + ListPlayers() ([]User, error) GetPlayerTeams(id string) ([]Team, error) - GetPlayerByEmail(email string) (Player, error) - GetPlayerByName(name string) (Player, error) - GetPlayerByID(id string) (Player, error) - GetPlayerByRole(role string) ([]Player, error) - GetPlayerByTeam(teamID string) ([]Player, error) - GetPlayerByTournament(tournamentID string) ([]Player, error) - GetPlayerByOrganizer(organizerID string) ([]Player, error) + GetPlayerByEmail(email string) (User, error) + GetPlayerByName(name string) (User, error) + GetPlayerByID(id string) (User, error) + GetPlayerByRole(role string) ([]User, error) + GetPlayerByTeam(teamID string) ([]User, error) + GetPlayerByTournament(tournamentID string) ([]User, error) + GetPlayerByOrganizer(organizerID string) ([]User, error) } -func LoginPlayer(db *sql.DB, email, password string) (Player, error) { - var player Player +func LoginPlayer(db *sql.DB, email, password string) (User, error) { + var player User var playerPassword string - err := db.QueryRow("SELECT id, name, email, password FROM players WHERE email = $1", email).Scan(&player.ID, &player.Name, &player.Email, playerPassword) + err := db.QueryRow("SELECT id, name, email, password FROM users WHERE email = $1", email).Scan(&player.UUID, &player.Username, &player.Email, playerPassword) if err != nil { log.Printf("Error logging in player with email %s: %v", email, err) - return Player{}, err + return User{}, err } - if player.ID == "" { - log.Printf("Player with email %s not found", email) - return Player{}, sql.ErrNoRows // or a custom error + if player.UUID == "" { + log.Printf("User with email %s not found", email) + return User{}, sql.ErrNoRows // or a custom error } // Check if the password matches if password == playerPassword { - log.Printf("Player %s logged in successfully", player.Name) + log.Printf("User %s logged in successfully", player.Username) } - log.Printf("Player %s logged in successfully", player.Name) + log.Printf("User %s logged in successfully", player.Username) return player, nil } -func GetPlayerRole(db *sql.DB, playerID string) (string, error) { - var role string - err := db.QueryRow("SELECT role FROM roles WHERE player_id = $1", playerID).Scan(&role) +func GetPlayerRole(db *sql.DB, playerID string) ([]string, error) { + var role []string + var tmp string + + err := db.QueryRow("SELECT role FROM roles WHERE player_id = $1", playerID).Scan(&tmp) + log.Println(tmp) + role = strings.Split(tmp, ",") if err != nil { log.Printf("Error retrieving role for player ID %s: %v", playerID, err) - return "", err + return nil, err } - if role == "" { + if len(role) == 0 { log.Printf("No role found for player ID %s", playerID) - return "", sql.ErrNoRows // or a custom error + return nil, sql.ErrNoRows // or a custom error } log.Printf("Retrieved role for player ID %s: %s", playerID, role) return role, nil } -func savePlayer(db *sql.DB, player Player) error { +func savePlayer(db *sql.DB, player User) error { // Ensure the player ID is set - if player.ID == "" { - player.ID = uuid.New().String() // Generate a new UUID if not set - log.Printf("Generated new player ID: %s", player.ID) + if player.UUID == "" { + player.UUID = uuid.New().String() // Generate a new UUID if not set + log.Printf("Generated new player ID: %s", player.UUID) } // Insert the player into the database - if player.Role == "" { - player.Role = "player" // Default role if not specified - log.Printf("Setting default role for player %s to 'player'", player.Name) + if len(player.Role) == 0 { + player.Role = append(player.Role, "player") // Default role if not specified + log.Printf("Setting default role for player %s to 'player'", player.Username) } - log.Printf("Saving player: ID=%v, Name=%v, Email=%v", player.ID, player.Name, player.Email) + log.Printf("Saving player: ID=%v, Name=%v, Email=%v", player.UUID, player.Username, player.Email) - stmt := "INSERT INTO public.players (id, name, email,password_hash) VALUES ($1, $2, $3,$4)" + stmt := "INSERT INTO public.users (id, name, email,password_hash) VALUES ($1, $2, $3,$4)" log.Printf("Generated SQL statement: %s", stmt) - _, err := db.Exec(stmt, player.ID, player.Name, player.Email, player.Password) + _, err := db.Exec(stmt, player.UUID, player.Username, player.Email, player.password) if err != nil { log.Printf("Error saving player to database: %v", err) @@ -147,17 +186,17 @@ func savePlayer(db *sql.DB, player Player) error { // } else { // log.Printf("Last insert ID: %d", lastID) // } - // if err := db.QueryRow("SELECT id, name, email FROM players WHERE id = ?", player.ID).Scan(&player.ID, &player.Name, &player.Email, &player.Role); err != nil { + // if err := db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", player.ID).Scan(&player.ID, &player.Name, &player.Email, &player.Role); err != nil { // log.Printf("Error retrieving player from database: %v", err) // return err // } - log.Printf("Player %s saved to database", player.Name) + log.Printf("User %s saved to database", player.Username) return nil } -func AddRoleToPlayer(db *sql.DB, playerID, role string) error { +func AddRoleToPlayer(db *sql.DB, playerID string, role []string) error { if playerID == "" { - log.Printf("Player ID is required to add a role, but got empty ID") + log.Printf("User ID is required to add a role, but got empty ID") return sql.ErrNoRows // or a custom error } @@ -174,22 +213,22 @@ func AddRoleToPlayer(db *sql.DB, playerID, role string) error { return nil } -func GetAllPlayers(db *sql.DB) ([]Player, error) { - rows, err := db.Query("SELECT id, name, email FROM public.players") +func GetAllPlayers(db *sql.DB) ([]User, error) { + rows, err := db.Query("SELECT id, name, email FROM public.users") if err != nil { - log.Printf("Error retrieving players: %v", err) + log.Printf("Error retrieving users: %v", err) return nil, err } defer rows.Close() - var players []Player + var users []User for rows.Next() { - var player Player - if err := rows.Scan(&player.ID, &player.Name, &player.Email); err != nil { + var player User + if err := rows.Scan(&player.UUID, &player.Username, &player.Email); err != nil { log.Printf("Error scanning player row: %v", err) return nil, err } - players = append(players, player) + users = append(users, player) } if err := rows.Err(); err != nil { @@ -197,33 +236,33 @@ func GetAllPlayers(db *sql.DB) ([]Player, error) { return nil, err } - return players, nil + return users, nil } -func GetPlayerByID(db *sql.DB, id string) (Player, error) { - var player Player - err := db.QueryRow("SELECT id, name, email FROM players WHERE id = $1", id).Scan(&player.ID, &player.Name, &player.Email) +func GetPlayerByID(db *sql.DB, id string) (User, error) { + var player User + err := db.QueryRow("SELECT id, name, email FROM users WHERE id = $1", id).Scan(&player.UUID, &player.Username, &player.Email) if err != nil { log.Printf("Error retrieving player by ID %s: %v", id, err) - return Player{}, err + return User{}, err } return player, nil } -func GetPlayerByEmail(db *sql.DB, email string) (Player, error) { - var player Player - err := db.QueryRow("SELECT id, name, email FROM players WHERE email = $1", email).Scan(&player.ID, &player.Name, &player.Email, &player.Role) +func GetPlayerByEmail(db *sql.DB, email string) (User, error) { + var player User + err := db.QueryRow("SELECT id, name, email FROM users WHERE email = $1", email).Scan(&player.UUID, &player.Username, &player.Email, &player.Role) if err != nil { log.Printf("Error retrieving player by email %s: %v", email, err) - return Player{}, err + return User{}, err } return player, nil } -func GetPlayerByName(db *sql.DB, name string) (Player, error) { - var player Player - err := db.QueryRow("SELECT id, name, email FROM players WHERE name = $1", name).Scan(&player.ID, &player.Name, &player.Email, &player.Role) +func GetPlayerByName(db *sql.DB, name string) (User, error) { + var player User + err := db.QueryRow("SELECT id, name, email FROM users WHERE name = $1", name).Scan(&player.UUID, &player.Username, &player.Email, &player.Role) if err != nil { log.Printf("Error retrieving player by name %s: %v", name, err) - return Player{}, err + return User{}, err } return player, nil } @@ -232,55 +271,33 @@ func GetPlayerByName(db *sql.DB, name string) (Player, error) { func deletePlayer(db *sql.DB, id string) error { // Delete the player from the database log.Printf("Deleting player with ID: %s", id) - _, err := db.Exec("DELETE FROM public.players WHERE id = $1", id) + _, err := db.Exec("DELETE FROM public.users WHERE id = $1", id) if err != nil { log.Printf("Error deleting player with ID %s: %v", id, err) return err } - log.Printf("Player with ID %s deleted successfully", id) + log.Printf("User with ID %s deleted successfully", id) return nil } // UpdatePlayer updates an existing player in the database. -// It requires the player ID to be set in the Player struct. -func updatePlayer(db *sql.DB, player Player) error { +// It requires the player ID to be set in the User struct. +func updatePlayer(db *sql.DB, player User) error { // Ensure the player ID is set - if player.ID == "" { - log.Printf("Player ID is required for update, but got empty ID") + if player.UUID == "" { + log.Printf("User ID is required for update, but got empty ID") return sql.ErrNoRows // or a custom error } - log.Printf("Updating player: ID=%v, Name=%v, Email=%v", player.ID, player.Name, player.Email) + log.Printf("Updating player: ID=%v, Name=%v, Email=%v", player.UUID, player.Username, player.Email) - stmt := "UPDATE public.players SET name = $1, email = $2 WHERE id = $3" - _, err := db.Exec(stmt, player.Name, player.Email, player.ID) + stmt := "UPDATE public.users SET name = $1, email = $2 WHERE id = $3" + _, err := db.Exec(stmt, player.Username, player.Email, player.UUID) if err != nil { log.Printf("Error updating player in database: %v", err) return err } - log.Printf("Player %s updated successfully", player.Name) + log.Printf("User %s updated successfully", player.Username) return nil } - -var players_default = []Player{{ - ID: "1", - Name: "John Doe", - Email: "John.Doe@example.de", - Role: "player", - Teams: []Team{ - {ID: "team1"}, - {ID: "team2"}, - }, -}, - { - ID: "2", - Name: "Jane Smith", - Email: "Jane-Smith@example.de", - Role: "player", - Teams: []Team{ - {ID: "team3"}, - {ID: "team1"}, - }, - }, -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 46fbafe..6211516 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import TeamManagement from './pages/Teams'; import ViewEditPlayer from './pages/ViewEditPlayer'; import Tournaments from './pages/Tournaments'; import NewTournament from './pages/NewTournament'; +import Administration from './pages/Administration'; function App() { @@ -26,6 +27,7 @@ function App() { } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/types.tsx b/frontend/src/components/interfaces/types.tsx similarity index 100% rename from frontend/src/pages/types.tsx rename to frontend/src/components/interfaces/types.tsx diff --git a/frontend/src/components/interfaces/users.tsx b/frontend/src/components/interfaces/users.tsx new file mode 100644 index 0000000..e7dc9ad --- /dev/null +++ b/frontend/src/components/interfaces/users.tsx @@ -0,0 +1,60 @@ +/** + * Interfaces and enums for user-related types + * File: /home/henry/code/Volleyball/frontend/src/components/interfaces/users.tsx + */ + +export enum UserRole { + Admin = 'admin', + Coach = 'coach', + Player = 'player', +} + + +/** Primary user model returned from the API */ +export interface User { + UUID: string + Username: string + Email: string + FirstName?: string + LastName?: string + AvatarUrl?: string + Roles: UserRole[] + IsActive: boolean + Phone?: string + TeamId?: string[] + CreatedAt?: string // ISO date + UpdatedAt?: string // ISO date + LastLogin?: string // ISO date +} + +/** Data required to create a new user */ +export interface CreateUserDTO { + username: string + email: string + password: string + firstName?: string + lastName?: string + roles?: UserRole[] +} + +/** Data for partial updates to a user */ +export interface UpdateUserDTO { + username?: string + email?: string + password?: string + firstName?: string | null + lastName?: string | null + avatarUrl?: string | null + roles?: UserRole[] + isActive?: boolean + teamId?: string | null + metadata?: Record | null +} + +/** Simple auth state slice for frontend state management */ +export interface AuthState { + currentUser?: User | null + token?: string | null + isLoading: boolean + error?: string | null +} \ No newline at end of file diff --git a/frontend/src/pages/Administration.tsx b/frontend/src/pages/Administration.tsx new file mode 100644 index 0000000..1393378 --- /dev/null +++ b/frontend/src/pages/Administration.tsx @@ -0,0 +1,421 @@ +import React, { JSX, useEffect, useState } from "react"; +import { fetchPlayers } from "./api"; +import { User, UserRole } from "../components/interfaces/users"; + +// type User = { +// id: string; +// name: string; +// email: string; +// role: "user" | "admin"; +// banned?: boolean; +// }; + +type Team = { + id: string; + name: string; + members: number; +}; + +type EventItem = { + id: string; + title: string; + date: string; // ISO + location?: string; +}; + +const containerStyle: React.CSSProperties = { + display: "flex", + height: "100%", + gap: 20, + padding: 20, + boxSizing: "border-box", + fontFamily: "Inter, Roboto, Arial, sans-serif", +}; + +const sidebarStyle: React.CSSProperties = { + width: 220, + borderRight: "1px solid #e6e6e6", + paddingRight: 12, +}; + +const tabButtonStyle = (active: boolean): React.CSSProperties => ({ + display: "block", + width: "100%", + textAlign: "left", + padding: "8px 10px", + marginBottom: 6, + cursor: "pointer", + borderRadius: 6, + background: active ? "#0b5fff1a" : "transparent", + border: active ? "1px solid #0b5fff33" : "1px solid transparent", +}); + +const contentStyle: React.CSSProperties = { + flex: 1, + minWidth: 0, + overflowY: "auto", +}; + +export default function Administration(): JSX.Element { + const [activeTab, setActiveTab] = useState< + "users" | "teams" | "events" | "settings" + >("users"); + + const [users, setUsers] = useState([]); + const [teams, setTeams] = useState([]); + const [events, setEvents] = useState([]); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Form state for creating event + const [newEventTitle, setNewEventTitle] = useState(""); + const [newEventDate, setNewEventDate] = useState(""); + const [newEventLocation, setNewEventLocation] = useState(""); + const token = localStorage.getItem('token'); + + useEffect(() => { + // fetch all admin resources when component mounts + if (token) refreshAll(); + }, []); + + async function refreshAll() { + if (!token) return; + setLoading(true); + setError(null); + try { + // Replace these endpoints with your backend API + + const users = await fetchPlayers(token); + setUsers(users); + console.log("Fetched users:", users); + const [uRes, tRes, eRes] = await Promise.all([ + fetch("/api/admin/players"), + fetch("/api/admin/teams"), + fetch("/api/admin/events"), + ]); + + if (!uRes.ok || !tRes.ok || !eRes.ok) { + throw new Error("Failed to fetch admin resources"); + } + + // console.log(uRes); + const [uJson, tJson, eJson] = await Promise.all([ + uRes.json(), + tRes.json(), + eRes.json(), + ]); + + // setUsers(uRes as User[]); + setTeams(tJson as Team[]); + setEvents(eJson as EventItem[]); + + + } catch (err: any) { + setError(err?.message ?? "Unknown error"); + } finally { + setLoading(false); + } + } + + async function toggleAdmin(user: User) { + const promote = user.Roles.includes(UserRole.Admin) ? false : true; + if ( + !window.confirm( + `${promote ? "Promote" : "Demote"} ${user.Username} to ${ + promote ? "admin" : "user" + }?` + ) + ) + return; + + try { + const res = await fetch(`/api/admin/users/${user.UUID}/role`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ role: promote ? "admin" : "user" }), + }); + if (!res.ok) throw new Error("Failed to change role"); + setUsers((prev) => + prev.map((u) => (u.UUID === user.UUID ? { ...u, role: promote ? "admin" : "user" } : u)) + ); + } catch (err: any) { + alert(err?.message ?? "Failed to update role"); + } + } + + // async function toggleBan(user: User) { + // const ban = user.isActive; + // if (!window.confirm(`${ban ? "Ban" : "Unban"} ${user.username}?`)) return; + // try { + // const res = await fetch(`/api/admin/users/${user.id}/ban`, { + // method: "PATCH", + // headers: { "Content-Type": "application/json" }, + // body: JSON.stringify({ banned: ban }), + // }); + // if (!res.ok) throw new Error("Failed to update ban"); + // setUsers((prev) => prev.map((u) => (u.id === user.id ? { ...u, banned: ban } : u))); + // } catch (err: any) { + // alert(err?.message ?? "Failed to update ban"); + // } + // } + + async function deleteTeam(team: Team) { + if (!window.confirm(`Delete team "${team.name}"? This cannot be undone.`)) return; + try { + const res = await fetch(`/api/admin/teams/${team.id}`, { method: "DELETE" }); + if (!res.ok) throw new Error("Failed to delete team"); + setTeams((prev) => prev.filter((t) => t.id !== team.id)); + } catch (err: any) { + alert(err?.message ?? "Failed to delete team"); + } + } + + async function createEvent(e: React.FormEvent) { + e.preventDefault(); + if (!newEventTitle || !newEventDate) { + alert("Title and date required"); + return; + } + try { + const payload = { + title: newEventTitle, + date: new Date(newEventDate).toISOString(), + location: newEventLocation || undefined, + }; + const res = await fetch("/api/admin/events", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) throw new Error("Failed to create event"); + const created: EventItem = await res.json(); + setEvents((prev) => [created, ...prev]); + setNewEventTitle(""); + setNewEventDate(""); + setNewEventLocation(""); + setActiveTab("events"); + } catch (err: any) { + alert(err?.message ?? "Failed to create event"); + } + } + + async function deleteEvent(ev: EventItem) { + if (!window.confirm(`Delete event "${ev.title}"?`)) return; + try { + const res = await fetch(`/api/admin/events/${ev.id}`, { method: "DELETE" }); + if (!res.ok) throw new Error("Failed to delete event"); + setEvents((prev) => prev.filter((x) => x.id !== ev.id)); + } catch (err: any) { + alert(err?.message ?? "Failed to delete event"); + } + } + + return ( +
+ + +
+ {activeTab === "users" && ( +
+

Users

+

Manage user accounts, roles and bans.

+
+ {users.length === 0 ? ( +

No users found.

+ ) : ( + + + + + + + + + + + + {users.map((u) => ( + + + + + {/* */} + {/* */} + + ))} + +
NameEmailRoleStatusActions
{u.Username}{u.Email}{u.Roles}{u.banned ? "Banned" : "Active"} + */} + {/* */} + {/*
+ )} +
+
+ )} + + {activeTab === "teams" && ( +
+

Teams

+

Manage teams and membership.

+
+ {teams.length === 0 ? ( +

No teams found.

+ ) : ( +
    + {teams.map((t) => ( +
  • +
    + {t.name} +
    {t.members} members
    +
    +
    + + +
    +
  • + ))} +
+ )} +
+
+ )} + + {activeTab === "events" && ( +
+

Events

+

Create and manage events

+ +
+
+ setNewEventTitle(e.target.value)} + style={{ padding: 8, flex: 1 }} + /> + setNewEventDate(e.target.value)} + style={{ padding: 8 }} + /> + setNewEventLocation(e.target.value)} + style={{ padding: 8 }} + /> + +
+
+ +
+ {events.length === 0 ? ( +

No events.

+ ) : ( +
    + {events.map((ev) => ( +
  • +
    + {ev.title} +
    + {new Date(ev.date).toLocaleString()} {ev.location ? `• ${ev.location}` : ""} +
    +
    +
    + + +
    +
  • + ))} +
+ )} +
+
+ )} + + {activeTab === "settings" && ( +
+

Settings

+

Application-wide administrative settings.

+
+

+ Use these settings to control global features. Implement actual controls as needed. +

+ +
+ + +
+ +
+
+
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Navigation.tsx b/frontend/src/pages/Navigation.tsx index 0126bdb..0e8413c 100644 --- a/frontend/src/pages/Navigation.tsx +++ b/frontend/src/pages/Navigation.tsx @@ -15,9 +15,10 @@ export default function Navigation() {
{token ? ( + <>Admin + ) : ( <> Register diff --git a/frontend/src/pages/Players.tsx b/frontend/src/pages/Players.tsx index 9409781..03e40f0 100644 --- a/frontend/src/pages/Players.tsx +++ b/frontend/src/pages/Players.tsx @@ -1,16 +1,16 @@ import React, { useEffect, useState } from 'react'; import { fetchPlayers,createPlayer,deletePlayer,updatePlayer } from './api'; import { Navigate, useNavigate } from 'react-router-dom'; +import { User, UserRole } from '../components/interfaces/users'; - -interface Player { - id: string; - name: string; - email: string; -} +// interface Player { +// id: string; +// name: string; +// email: string; +// } export default function PlayerManagement() { - const [players, setPlayers] = useState([]); + const [players, setPlayers] = useState([]); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -39,17 +39,19 @@ export default function PlayerManagement() { if (editingId !== null) { setPlayers(players.map(p => - p.id === editingId ? { ...p, name, email } : p + p.UUID === editingId ? { ...p, name, email } : p )); if (token) { updatePlayer(editingId, { name, email }, token); } setEditingId(null); } else { - const newPlayer: Player = { - id: "", - name, - email, + const newPlayer: User = { + UUID: "", + Username:"", + Email: "", + Roles: [UserRole.Player], + IsActive: true, }; if (token) { @@ -68,7 +70,7 @@ export default function PlayerManagement() { }; const handleDelete = (id: string) => { - setPlayers(players.filter(p => p.id !== id)); + setPlayers(players.filter(p => p.UUID !== id)); if (token) { deletePlayer(id, token); } @@ -124,19 +126,19 @@ export default function PlayerManagement() { {players.map(player => ( - - {player.name} - {player.email} + + {player.Username} + {player.Email}
@@ -91,7 +86,7 @@ const ViewEditPlayer = () => { onChange={(e) => setEmail(e.target.value)} /> ) : ( -

{player.email}

+

{player.Email}

)} @@ -107,7 +102,7 @@ const ViewEditPlayer = () => { ) : ( -

{player.role}

+

{player.Roles}

)} diff --git a/frontend/src/pages/api.tsx b/frontend/src/pages/api.tsx index 994a537..2b0ddfc 100644 --- a/frontend/src/pages/api.tsx +++ b/frontend/src/pages/api.tsx @@ -70,7 +70,7 @@ export async function fetchPlayers(token: string) { return res.json(); } -export async function createPlayer(player: { name: string, email: string }, token: string) { +export async function createPlayer(player: { Username: string, Email: string }, token: string) { const res = await fetch(`${API_URL}/players`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },