ADD: added function to update db scheme automaticly

This commit is contained in:
hwinkel
2025-11-18 22:23:08 +01:00
parent ef396af480
commit 3818fbf460
15 changed files with 876 additions and 186 deletions

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"},
},
},
}

View File

@@ -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() {
<Route path="/players/:id" element={<ViewEditPlayer />} />
<Route path="/teams" element={<ProtectedRoute><TeamManagement /></ProtectedRoute>} />
<Route path="/Administration" element={<ProtectedRoute><Administration /></ProtectedRoute>} />
<Route path="/tournament/:id" element={<ProtectedRoute><TournamentDetails /></ProtectedRoute>} />
<Route path="/tournament/new" element={<ProtectedRoute><NewTournament /></ProtectedRoute>} />

View File

@@ -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<string, unknown> | null
}
/** Simple auth state slice for frontend state management */
export interface AuthState {
currentUser?: User | null
token?: string | null
isLoading: boolean
error?: string | null
}

View File

@@ -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<User[]>([]);
const [teams, setTeams] = useState<Team[]>([]);
const [events, setEvents] = useState<EventItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 (
<div style={containerStyle}>
<aside style={sidebarStyle}>
<h3>Administration</h3>
<div>
<button
style={tabButtonStyle(activeTab === "users")}
onClick={() => setActiveTab("users")}
>
Users
</button>
<button
style={tabButtonStyle(activeTab === "teams")}
onClick={() => setActiveTab("teams")}
>
Teams
</button>
<button
style={tabButtonStyle(activeTab === "events")}
onClick={() => setActiveTab("events")}
>
Events
</button>
<button
style={tabButtonStyle(activeTab === "settings")}
onClick={() => setActiveTab("settings")}
>
Settings
</button>
</div>
<div style={{ marginTop: 20 }}>
<button onClick={refreshAll} style={{ padding: "6px 10px", cursor: "pointer" }}>
Refresh
</button>
</div>
{loading && <p style={{ marginTop: 12 }}>Loading...</p>}
{error && <p style={{ marginTop: 12, color: "red" }}>{error}</p>}
</aside>
<main style={contentStyle}>
{activeTab === "users" && (
<section>
<h2>Users</h2>
<p>Manage user accounts, roles and bans.</p>
<div style={{ marginTop: 12 }}>
{users.length === 0 ? (
<p>No users found.</p>
) : (
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr style={{ textAlign: "left", borderBottom: "1px solid #eee" }}>
<th style={{ padding: "8px 6px" }}>Name</th>
<th style={{ padding: "8px 6px" }}>Email</th>
<th style={{ padding: "8px 6px" }}>Role</th>
<th style={{ padding: "8px 6px" }}>Status</th>
<th style={{ padding: "8px 6px" }}>Actions</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.UUID} style={{ borderBottom: "1px solid #fafafa" }}>
<td style={{ padding: "8px 6px" }}>{u.Username}</td>
<td style={{ padding: "8px 6px" }}>{u.Email}</td>
<td style={{ padding: "8px 6px" }}>{u.Roles}</td>
{/* <td style={{ padding: "8px 6px" }}>{u.banned ? "Banned" : "Active"}</td> */}
{/* <td style={{ padding: "8px 6px" }}>
<button onClick={() => toggleAdmin(u)} style={{ marginRight: 8 }}>
{u.roles.includes(UserRole.Admin) ? "Demote" : "Promote"}
</button> */}
{/* <button onClick={() => toggleBan(u)}>{u.banned ? "Unban" : "Ban"}</button> */}
{/* </td> */}
</tr>
))}
</tbody>
</table>
)}
</div>
</section>
)}
{activeTab === "teams" && (
<section>
<h2>Teams</h2>
<p>Manage teams and membership.</p>
<div style={{ marginTop: 12 }}>
{teams.length === 0 ? (
<p>No teams found.</p>
) : (
<ul style={{ paddingLeft: 0, listStyle: "none" }}>
{teams.map((t) => (
<li
key={t.id}
style={{
display: "flex",
justifyContent: "space-between",
padding: "8px 0",
borderBottom: "1px solid #f2f2f2",
}}
>
<div>
<strong>{t.name}</strong>
<div style={{ fontSize: 13, color: "#666" }}>{t.members} members</div>
</div>
<div>
<button style={{ marginRight: 8 }}>Edit</button>
<button onClick={() => deleteTeam(t)}>Delete</button>
</div>
</li>
))}
</ul>
)}
</div>
</section>
)}
{activeTab === "events" && (
<section>
<h2>Events</h2>
<p>Create and manage events</p>
<form onSubmit={createEvent} style={{ marginTop: 12, marginBottom: 24 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
placeholder="Event title"
value={newEventTitle}
onChange={(e) => setNewEventTitle(e.target.value)}
style={{ padding: 8, flex: 1 }}
/>
<input
type="date"
value={newEventDate}
onChange={(e) => setNewEventDate(e.target.value)}
style={{ padding: 8 }}
/>
<input
placeholder="Location (optional)"
value={newEventLocation}
onChange={(e) => setNewEventLocation(e.target.value)}
style={{ padding: 8 }}
/>
<button type="submit" style={{ padding: "8px 12px" }}>
Create
</button>
</div>
</form>
<div>
{events.length === 0 ? (
<p>No events.</p>
) : (
<ul style={{ paddingLeft: 0, listStyle: "none" }}>
{events.map((ev) => (
<li
key={ev.id}
style={{
display: "flex",
justifyContent: "space-between",
padding: "8px 0",
borderBottom: "1px solid #f2f2f2",
}}
>
<div>
<strong>{ev.title}</strong>
<div style={{ fontSize: 13, color: "#666" }}>
{new Date(ev.date).toLocaleString()} {ev.location ? `${ev.location}` : ""}
</div>
</div>
<div>
<button style={{ marginRight: 8 }}>Edit</button>
<button onClick={() => deleteEvent(ev)}>Delete</button>
</div>
</li>
))}
</ul>
)}
</div>
</section>
)}
{activeTab === "settings" && (
<section>
<h2>Settings</h2>
<p>Application-wide administrative settings.</p>
<div style={{ marginTop: 12 }}>
<p style={{ fontSize: 14, color: "#444" }}>
Use these settings to control global features. Implement actual controls as needed.
</p>
<div style={{ marginTop: 12 }}>
<label style={{ display: "block", marginBottom: 8 }}>
<input type="checkbox" /> Allow public registration
</label>
<label style={{ display: "block", marginBottom: 8 }}>
<input type="checkbox" /> Require email verification
</label>
<div style={{ marginTop: 12 }}>
<button onClick={() => alert("Settings saved (stub)")}>Save settings</button>
</div>
</div>
</div>
</section>
)}
</main>
</div>
);
}

View File

@@ -15,9 +15,10 @@ export default function Navigation() {
</div>
<div>
{token ? (
<><Link to="/Administration" className="px-3 py-1 border rounded mr-2">Admin</Link>
<button onClick={logout} className="bg-red-500 px-3 py-1 rounded">
Logout
</button>
</button></>
) : (
<>
<Link to="/register" className="px-3 py-1 border rounded">Register</Link>

View File

@@ -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<Player[]>([]);
const [players, setPlayers] = useState<User[]>([]);
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() {
</thead>
<tbody>
{players.map(player => (
<tr key={player.id}>
<td className="border px-4 py-2">{player.name}</td>
<td className="border px-4 py-2">{player.email}</td>
<tr key={player.UUID}>
<td className="border px-4 py-2">{player.Username}</td>
<td className="border px-4 py-2">{player.Email}</td>
<td className="border px-4 py-2 space-x-2">
<button
key={player.id}
onClick={handleViewEdit(player.id)}
key={player.UUID}
onClick={handleViewEdit(player.UUID)}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Bearbeiten
</button>
<button
onClick={() => handleDelete(player.id)}
onClick={() => handleDelete(player.UUID)}
className="bg-red-500 text-white px-2 py-1 rounded"
>
Löschen

View File

@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { fetchTournament, updateTournament, registerTeam } from './api';
import { useAuth } from './AuthContext';
import { Tournament } from './types';
import { Tournament } from '../components/interfaces/types';
export default function TournamentDetails() {
const { id } = useParams<{ id: string }>();

View File

@@ -1,7 +1,7 @@
import { Link, useLocation } from 'react-router-dom';
import { use, useEffect, useState } from 'react';
import { fetchTournaments, fetchTournament } from './api'; // Importiere die API-Funktion
import type { Tournament } from './types';
import type { Tournament } from '../components/interfaces/types';
function useQuery() {
return new URLSearchParams(useLocation().search);

View File

@@ -2,13 +2,8 @@ import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getUserFromToken } from '../components/utils/jwt'; // Importiere die Funktion zum Decodieren des Tokens
import {fetchPlayer} from './api'; // Importiere die Funktion zum Abrufen des Spielers
import { User } from '../components/interfaces/users';
interface Player {
id: string;
name: string;
email: string;
role: string;
}
const ViewEditPlayer = () => {
const { id } = useParams<{ id: string }>();
@@ -16,7 +11,7 @@ const ViewEditPlayer = () => {
const currentUser = token ? getUserFromToken(token) : null;
const isAdmin = currentUser?.role === 'admin';
const [player, setPlayer] = useState<Player | null>(null);
const [player, setPlayer] = useState<User | null>(null);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('');
@@ -32,9 +27,9 @@ const ViewEditPlayer = () => {
const data = await fetchPlayer(token, id);
console.log("Geladener Spieler:", data);
setPlayer(data);
setName(data.name);
setEmail(data.email);
setRole(data.role);
setName(data.Username);
setEmail(data.Email);
setRole(data.Roles);
} catch (error) {
setMessage('Spieler konnte nicht geladen werden.');
}
@@ -77,7 +72,7 @@ const ViewEditPlayer = () => {
onChange={(e) => setName(e.target.value)}
/>
) : (
<p>{player.name}</p>
<p>{player.Username}</p>
)}
</div>
@@ -91,7 +86,7 @@ const ViewEditPlayer = () => {
onChange={(e) => setEmail(e.target.value)}
/>
) : (
<p>{player.email}</p>
<p>{player.Email}</p>
)}
</div>
@@ -107,7 +102,7 @@ const ViewEditPlayer = () => {
<option value="admin">Admin</option>
</select>
) : (
<p>{player.role}</p>
<p>{player.Roles}</p>
)}
</div>

View File

@@ -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}` },