From 1a2eec44a9b0f656c98fe0013dccef2866126898 Mon Sep 17 00:00:00 2001 From: hwinkel Date: Fri, 30 May 2025 15:02:23 +0200 Subject: [PATCH] ADD: added database connection for players data handling and started login funtion with database --- backend/cmd/server/main.go | 33 ++- backend/go.mod | 1 + backend/go.sum | 2 + backend/internal/auth/handler.go | 53 +++- backend/internal/common/passwordHasher.go | 12 + backend/internal/database/database.go | 27 +- backend/internal/database/initTables.go | 56 ++++- backend/internal/player/handler.go | 145 +++++++++++ backend/internal/player/model.go | 286 ++++++++++++++++++++++ backend/internal/tournament/handler.go | 9 +- backend/internal/tournament/model.go | 12 +- frontend/src/App.tsx | 3 + frontend/src/components/utils/jwt.tsx | 23 ++ frontend/src/pages/AuthContext.tsx | 19 ++ frontend/src/pages/LoginPage.tsx | 17 +- frontend/src/pages/Navigation.tsx | 7 +- frontend/src/pages/Players.tsx | 78 ++++-- frontend/src/pages/Teams.tsx | 142 +++++++++++ frontend/src/pages/api.tsx | 57 +++++ 19 files changed, 924 insertions(+), 58 deletions(-) create mode 100644 backend/internal/common/passwordHasher.go create mode 100644 backend/internal/player/handler.go create mode 100644 backend/internal/player/model.go create mode 100644 frontend/src/components/utils/jwt.tsx create mode 100644 frontend/src/pages/Teams.tsx diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 1a77409..486acf4 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -3,6 +3,8 @@ package main import ( "os" "volleyball/internal/auth" + "volleyball/internal/database" + "volleyball/internal/player" "volleyball/internal/tournament" "github.com/gin-contrib/cors" @@ -10,6 +12,19 @@ import ( ) func main() { + + var host = "localhost" + var DBport = 5432 + var user = "volleyball" + + db := database.New(host, DBport, user, "volleyball", "volleyball") + db.Connect() + + // Setup the database and tables + if err := db.SetupTables(); err != nil { + os.Exit(1) + } + r := gin.Default() // CORS @@ -21,7 +36,10 @@ func main() { })) // Public - r.POST("/api/login", auth.LoginHandler) + r.POST("/api/login", func(c *gin.Context) { + auth.LoginHandler(c, db.GetDB()) + }) + r.GET("/api/tournaments", tournament.ListTournaments) // Protected API @@ -32,6 +50,19 @@ func main() { api.POST("/tournaments/:id/join", tournament.JoinTournament) api.PUT("/tournaments/:id", tournament.UpdateTournament) + api.GET("/players", func(c *gin.Context) { + player.GetPlayers(c, db.GetDB()) + }) + api.POST("/players", func(c *gin.Context) { + player.CreatePlayer(c, db.GetDB()) + }) + api.PUT("/players/:id", func(c *gin.Context) { + player.UpdatePlayer(c, db.GetDB()) + }) + api.DELETE("/players/:id", func(c *gin.Context) { + player.DeletePlayer(c, db.GetDB()) + }) + port := os.Getenv("PORT") if port == "" { port = "8080" diff --git a/backend/go.mod b/backend/go.mod index bdde34b..6a106aa 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -23,6 +23,7 @@ require ( github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/google/uuid v1.6.0 github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index cb53659..7261979 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -33,6 +33,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 84d3e12..6f6b290 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -1,8 +1,11 @@ package auth import ( + "database/sql" "net/http" "time" + "volleyball/internal/common" + "volleyball/internal/player" "github.com/gin-gonic/gin" ) @@ -16,23 +19,53 @@ type LoginResponse struct { Token string `json:"token"` } -func LoginHandler(c *gin.Context) { +func LoginHandler(c *gin.Context, db *sql.DB) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"}) return } - // Systemnutzer - if req.Email == "test@localhost.de" { - token, err := CreateJWT("system-user-id", req.Email, "admin", time.Hour*24*7) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Token error"}) - return - } - c.JSON(http.StatusOK, LoginResponse{Token: token}) + // Validate input + if req.Email == "" || req.Password == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Email and password are required"}) return } - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + // Systemnutzer + var token string + var err error + if req.Email == "test@localhost.de" { + token, err = CreateJWT("system-user-id", req.Email, "admin", 60*time.Minute) + } else { + + hash, err := common.HashPassword(req.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Password hashing error"}) + return + } + + loggedInPlayer, err := player.LoginPlayer(db, req.Email, string(hash)) + if err != nil { + if err == sql.ErrNoRows { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) + return + } + // Create JWT token + token, err = CreateJWT(loggedInPlayer.ID, req.Email, "player", 60*time.Minute) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Token creation error"}) + return + } + } + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Token error"}) + return + } + c.JSON(http.StatusOK, LoginResponse{Token: token}) + return } diff --git a/backend/internal/common/passwordHasher.go b/backend/internal/common/passwordHasher.go new file mode 100644 index 0000000..0d79efe --- /dev/null +++ b/backend/internal/common/passwordHasher.go @@ -0,0 +1,12 @@ +package common + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + // Use bcrypt to hash the password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPassword), nil +} diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go index fd0ca97..e0dd4c1 100644 --- a/backend/internal/database/database.go +++ b/backend/internal/database/database.go @@ -17,6 +17,29 @@ type database struct { db *sql.DB // Pointer to sql.DB } +func (d *database) SetupTables() error { + InitTables(d.db) + log.Println("Database setup completed successfully.") + return nil +} + +func (d *database) GetDB() *sql.DB { + if d.db == nil { + log.Println("Database connection is not established. Call Connect() first.") + return nil + } + return d.db +} + +// New creates a new database instance with the provided configuration. +// It initializes the database connection parameters but does not connect to the database. +// This function should be called before calling Connect() to establish the connection. +// It returns a pointer to the database instance. +// Example usage: +// db := database.New("localhost", 5432, "user", "password", "dbname") +// It is intended to be used in the main application or setup phase. +// This function is not intended to be called during normal application operation. +// It is not intended to be called during normal application operation. func New(host string, port int, user, password, dbname string) *database { return &database{ host: host, @@ -28,7 +51,7 @@ func New(host string, port int, user, password, dbname string) *database { } } -func (d *database) Connect() error { +func (d *database) Connect() (db *sql.DB) { fmt.Println("Connecting to the database...") psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", d.host, d.port, d.user, d.password, d.dbname) @@ -49,5 +72,5 @@ func (d *database) Connect() error { log.Println("Connected to the database successfully") d.db = db - return nil + return db } diff --git a/backend/internal/database/initTables.go b/backend/internal/database/initTables.go index 6bc4679..055652b 100644 --- a/backend/internal/database/initTables.go +++ b/backend/internal/database/initTables.go @@ -5,28 +5,66 @@ import ( "fmt" "log" + "volleyball/internal/player" + _ "github.com/lib/pq" // Import the PostgreSQL driver ) -const playerTable = ` - CREATE TABLE IF NOT EXISTS players ( +const teamTable = ` + CREATE TABLE IF NOT EXISTS teams ( id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - age INT NOT NULL, - birthday DATE NOT NULL + player1_id uuid NOT NULL, + player2_id uuid NOT NULL, + formation_date DATE NOT NULL DEFAULT CURRENT_DATE, + FOREIGN KEY (player1_id) REFERENCES players(id), + FOREIGN KEY (player2_id) REFERENCES players(id) ); ` -func InitTables(db *sql.DB) { +var tableNames = []string{ + "players", + "teams", +} + +func CheckIfTablesExist(db *sql.DB) (bool, error) { + for _, tableName := range tableNames { + exists, err := tableExists(db, tableName) + if err != nil { + return false, fmt.Errorf("error checking if table %s exists: %w", tableName, err) + } + if !exists { + return false, nil // At least one table does not exist + } + } + 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 InitTables(d *sql.DB) error { tables := []string{ - playerTable, + player.PlayerTable, + player.RoleTable, + teamTable, } for _, table := range tables { - if _, err := db.Exec(table); err != nil { + if _, err := d.Exec(table); err != nil { log.Fatalf("Error creating table: %v", err) + return fmt.Errorf("error creating table: %w", err) } } - fmt.Println("Tables initialized successfully.") + log.Println("Tables initialized successfully.") + return nil } diff --git a/backend/internal/player/handler.go b/backend/internal/player/handler.go new file mode 100644 index 0000000..5364b71 --- /dev/null +++ b/backend/internal/player/handler.go @@ -0,0 +1,145 @@ +package player + +import ( + "database/sql" + "log" + "net/http" + "volleyball/internal/common" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func GetPlayers(c *gin.Context, db *sql.DB) { + log.Println(c.GetString("userId"), c.GetString("email"), c.GetString("role")) + // Simulate fetching players from a database + + players, err := GetAllPlayers(db) + if err != nil { + log.Printf("Error retrieving players: %v", err) + common.RespondError(c, http.StatusInternalServerError, "Failed to retrieve players") + return + } + + if len(players) > 0 { + log.Printf("User %s (%s) requested players", c.GetString("userId"), c.GetString("email")) + c.JSON(http.StatusOK, players) + return + } + log.Printf("User %s (%s) requested players, but none found", c.GetString("userId"), c.GetString("email")) + + common.RespondError(c, http.StatusNotFound, "No Players found") +} + +func CreatePlayer(c *gin.Context, db *sql.DB) { + var newPlayer Player + 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) + 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() + err = savePlayer(db, newPlayer) + if err != nil { + log.Printf("Error saving player: %v", err) + common.RespondError(c, http.StatusInternalServerError, "Failed to create player") + return + } + AddRoleToPlayer(db, newPlayer.ID, "player") + 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) + + common.RespondCreated(c, newPlayer) +} + +func GetPlayer(c *gin.Context, db *sql.DB) { + playerID := c.Param("id") + if playerID == "" { + common.RespondError(c, http.StatusBadRequest, "Player ID is required") + return + } + + player, err := GetPlayerByID(db, playerID) + if err != nil { + log.Printf("Error retrieving player with ID %s: %v", playerID, err) + common.RespondError(c, http.StatusInternalServerError, "Failed to retrieve player") + return + } + + if player.ID == "" { + log.Printf("Player with ID %s not found", playerID) + common.RespondError(c, http.StatusNotFound, "Player not found") + return + } + + log.Printf("User %s (%s) requested player: %s", c.GetString("userId"), c.GetString("email"), player.Name) + + c.JSON(http.StatusOK, player) +} + +func UpdatePlayer(c *gin.Context, db *sql.DB) { + playerID := c.Param("id") + if playerID == "" { + common.RespondError(c, http.StatusBadRequest, "Player ID is required") + return + } + + if playerID != c.GetString("userId") || c.GetString("role") != "admin" { + common.RespondError(c, http.StatusForbidden, "You do not have permission to update this player") + return + } + + var updatedPlayer Player + 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 + err := updatePlayer(db, updatedPlayer) + if err != nil { + log.Printf("Error updating player with ID %s: %v", playerID, err) + common.RespondError(c, http.StatusInternalServerError, "Failed to update player") + return + } + + log.Printf("User %s (%s) updated player: %s", c.GetString("userId"), c.GetString("email"), updatedPlayer.Name) + + c.JSON(http.StatusOK, updatedPlayer) +} +func DeletePlayer(c *gin.Context, db *sql.DB) { + playerID := c.Param("id") + if playerID == "" { + common.RespondError(c, http.StatusBadRequest, "Player ID is required") + return + } + if c.GetString("role") != "admin" { + common.RespondError(c, http.StatusForbidden, "You do not have permission to delete this player") + return + } + + err := deletePlayer(db, playerID) + if err != nil { + log.Printf("Error deleting player with ID %s: %v", playerID, err) + common.RespondError(c, http.StatusInternalServerError, "Failed to delete player") + return + } + + log.Printf("User %s (%s) deleted player with ID: %s", c.GetString("userId"), c.GetString("email"), playerID) + + common.RespondMessage(c, "Player deleted successfully") +} diff --git a/backend/internal/player/model.go b/backend/internal/player/model.go new file mode 100644 index 0000000..2f04d05 --- /dev/null +++ b/backend/internal/player/model.go @@ -0,0 +1,286 @@ +package player + +import ( + "database/sql" + "log" + + "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 Team struct { + ID string `json:"id"` + Name string `json:"name"` + Players []Player `json:"players"` // 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 + MatchesDrawn int `json:"matchesDrawn"` // Number of matches drawn by the team + MatchesPlayed int `json:"matchesPlayed"` // Total matches played by the team + 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 PlayerListResponse struct { + Players []Player `json:"players"` +} +type PlayerResponse struct { + Player Player `json:"player"` +} +type CreatePlayerRequest struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` + Role string `json:"role" binding:"required,oneof=player organizer"` +} +type UpdatePlayerRequest struct { + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` + Role string `json:"role" binding:"required,oneof=player organizer"` + Teams []Team `json:"teams"` // Teams the player is part of +} +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) + DeletePlayer(id string) error + ListPlayers() ([]Player, 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) +} + +func LoginPlayer(db *sql.DB, email, password string) (Player, error) { + var player Player + 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) + if err != nil { + log.Printf("Error logging in player with email %s: %v", email, err) + return Player{}, err + } + if player.ID == "" { + log.Printf("Player with email %s not found", email) + return Player{}, 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("Player %s logged in successfully", player.Name) + 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) + if err != nil { + log.Printf("Error retrieving role for player ID %s: %v", playerID, err) + return "", err + } + if role == "" { + log.Printf("No role found for player ID %s", playerID) + return "", 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 { + + // 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) + } + // 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) + } + + log.Printf("Saving player: ID=%v, Name=%v, Email=%v", player.ID, player.Name, player.Email) + + stmt := "INSERT INTO public.players (id, name, email) VALUES ($1, $2, $3)" + log.Printf("Generated SQL statement: %s", stmt) + _, err := db.Exec(stmt, player.ID, player.Name, player.Email) + + if err != nil { + log.Printf("Error saving player to database: %v", err) + } + // lastID, err := res.LastInsertId() + // if err != nil { + // log.Printf("Error getting last insert ID: %v", err) + // } 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 { + // log.Printf("Error retrieving player from database: %v", err) + // return err + // } + log.Printf("Player %s saved to database", player.Name) + return nil +} + +func AddRoleToPlayer(db *sql.DB, playerID, role string) error { + if playerID == "" { + log.Printf("Player ID is required to add a role, but got empty ID") + return sql.ErrNoRows // or a custom error + } + + log.Printf("Adding role '%s' to player with ID: %s", role, playerID) + + stmt := "INSERT INTO public.roles (player_id, role) VALUES ($1, $2)" + _, err := db.Exec(stmt, playerID, role) + if err != nil { + log.Printf("Error adding role to player with ID %s: %v", playerID, err) + return err + } + + log.Printf("Role '%s' added to player with ID %s successfully", role, playerID) + return nil +} + +func GetAllPlayers(db *sql.DB) ([]Player, error) { + rows, err := db.Query("SELECT id, name, email FROM public.players") + if err != nil { + log.Printf("Error retrieving players: %v", err) + return nil, err + } + defer rows.Close() + + var players []Player + for rows.Next() { + var player Player + if err := rows.Scan(&player.ID, &player.Name, &player.Email); err != nil { + log.Printf("Error scanning player row: %v", err) + return nil, err + } + players = append(players, player) + } + + if err := rows.Err(); err != nil { + log.Printf("Error iterating over player rows: %v", err) + return nil, err + } + + return players, 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, &player.Role) + if err != nil { + log.Printf("Error retrieving player by ID %s: %v", id, err) + return Player{}, 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) + if err != nil { + log.Printf("Error retrieving player by email %s: %v", email, err) + return Player{}, 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) + if err != nil { + log.Printf("Error retrieving player by name %s: %v", name, err) + return Player{}, err + } + return player, nil +} + +// DeletePlayer deletes a player from the database by ID. +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) + 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) + 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 { + // Ensure the player ID is set + if player.ID == "" { + log.Printf("Player 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) + + stmt := "UPDATE public.players SET name = $1, email = $2 WHERE id = $3" + _, err := db.Exec(stmt, player.Name, player.Email, player.ID) + if err != nil { + log.Printf("Error updating player in database: %v", err) + return err + } + + log.Printf("Player %s updated successfully", player.Name) + 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/backend/internal/tournament/handler.go b/backend/internal/tournament/handler.go index 3eb6cd6..2cbd922 100644 --- a/backend/internal/tournament/handler.go +++ b/backend/internal/tournament/handler.go @@ -4,6 +4,8 @@ import ( "net/http" "volleyball/internal/common" + "slices" + "github.com/gin-gonic/gin" ) @@ -17,7 +19,7 @@ var tournaments = []Tournament{ {ID: "t1", Name: "Smasher"}, {ID: "t2", Name: "Blockbuster"}, }, - OrganizerId: "system-user-id", + OrganizerId: []string{"example-user"}, }, { ID: "2", @@ -25,7 +27,7 @@ var tournaments = []Tournament{ Location: "Hamburg", MaxParticipants: 10, Teams: []Team{}, - OrganizerId: "other-user", + OrganizerId: []string{"example-user"}, }, } @@ -74,7 +76,8 @@ func UpdateTournament(c *gin.Context) { for i, t := range tournaments { if t.ID == id { - if t.OrganizerId != userId { + isOrganizer := slices.Contains(t.OrganizerId, userId) + if !isOrganizer { c.JSON(http.StatusForbidden, gin.H{"error": "No permission"}) return } diff --git a/backend/internal/tournament/model.go b/backend/internal/tournament/model.go index 37d43b4..c38aecf 100644 --- a/backend/internal/tournament/model.go +++ b/backend/internal/tournament/model.go @@ -6,10 +6,10 @@ type Team struct { } type Tournament struct { - ID string `json:"id"` - Name string `json:"name"` - Location string `json:"location"` - MaxParticipants int `json:"maxParticipants"` - Teams []Team `json:"teams"` - OrganizerId string `json:"organizerId"` + ID string `json:"id"` + Name string `json:"name"` + Location string `json:"location"` + MaxParticipants int `json:"maxParticipants"` + Teams []Team `json:"teams"` + OrganizerId []string `json:"PlayerId"` // List of player IDs who are organizers } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d4a3bcc..9d37ea9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,6 +6,7 @@ import Dashboard from './pages/Dashboard'; import TournamentDetails from './pages/TournamentDetails'; import Players from './pages/Players'; import Navigation from './pages/Navigation'; +import TeamManagement from './pages/Teams'; function App() { @@ -19,6 +20,8 @@ function App() { {/* Geschützte Routen */} } /> + } /> + } /> diff --git a/frontend/src/components/utils/jwt.tsx b/frontend/src/components/utils/jwt.tsx new file mode 100644 index 0000000..eb6a848 --- /dev/null +++ b/frontend/src/components/utils/jwt.tsx @@ -0,0 +1,23 @@ +// utils/jwt.ts +import { jwtDecode } from 'jwt-decode'; + +export interface TokenPayload { + userId: string; + email: string; + role: string; + exp: number; +} + +export function getUserFromToken(token: string): TokenPayload | null { + try { + const decoded = jwtDecode(token); + if (decoded.exp && decoded.exp < Date.now() / 1000) { + console.warn("Token ist abgelaufen"); + return null; + } + return decoded; + } catch (error) { + console.error("Fehler beim Decodieren des Tokens:", error); + return null; + } +} diff --git a/frontend/src/pages/AuthContext.tsx b/frontend/src/pages/AuthContext.tsx index 16ac1a7..a23f6a6 100644 --- a/frontend/src/pages/AuthContext.tsx +++ b/frontend/src/pages/AuthContext.tsx @@ -1,4 +1,5 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; +import { getUserFromToken } from '../components/utils/jwt'; interface AuthContextType { token: string | null; @@ -17,10 +18,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { const storedToken = localStorage.getItem('token'); const storedUserId = localStorage.getItem('userId'); + if (!storedToken) { + return + } if (storedToken && storedUserId) { setToken(storedToken); setUserId(storedUserId); } + + const user = getUserFromToken(storedToken); + if (!user) { + logout(); // z. B. localStorage.clear() + navigate("/login") + return; + } + // ⏳ Logout bei Ablauf + const timeout = setTimeout(() => { + logout(); + }, user.exp * 1000 - Date.now()); + + return () => clearTimeout(timeout); + }, []); const login = (token: string, userId: string, role: string) => { @@ -39,6 +56,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children localStorage.removeItem('token'); localStorage.removeItem('userId'); localStorage.removeItem('role'); + localStorage.clear(); // Optional: Entfernt alle gespeicherten Daten + // Hier könntest du auch eine Weiterleitung zur Login-Seite hinzufügen }; return ( diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 5a982b5..a20da92 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; import { useAuth } from './AuthContext'; import { login as apiLogin } from './api'; -import { jwtDecode } from 'jwt-decode'; +// import { jwtDecode } from 'jwt-decode'; import { useNavigate } from 'react-router-dom'; - +import { getUserFromToken } from '../components/utils/jwt'; export default function LoginPage() { const { login } = useAuth(); const [email, setEmail] = useState(''); @@ -18,12 +18,13 @@ export default function LoginPage() { const data = await apiLogin(email, password); // Token aus JWT extrahieren (hier: UserID im Token Payload) // Für Demo: Einfach Dummy UserID setzen, oder später JWT decode implementieren - type MyJwtPayload = { - userId: string - email: string; - role: string; - } & object; - const decodedData = jwtDecode(data.token); + + var decodedData = getUserFromToken(data.token); + if (!decodedData || !decodedData.userId || !decodedData.role) { + setError('Ungültiges Token'); + return; + } + // Dummy UserID für Demo login(data.token, decodedData.userId, decodedData.role); // Nach dem Login zur Dashboard-Seite navigieren setTimeout(() => { diff --git a/frontend/src/pages/Navigation.tsx b/frontend/src/pages/Navigation.tsx index 6c79db6..0a4fa74 100644 --- a/frontend/src/pages/Navigation.tsx +++ b/frontend/src/pages/Navigation.tsx @@ -9,6 +9,7 @@ export default function Navigation() {
Dashboard {token && Spieler} + {token && Teams}
{token ? ( @@ -16,7 +17,11 @@ export default function Navigation() { Logout ) : ( - Login + <> + Register + + Login + )}
diff --git a/frontend/src/pages/Players.tsx b/frontend/src/pages/Players.tsx index eb2e392..bd4f201 100644 --- a/frontend/src/pages/Players.tsx +++ b/frontend/src/pages/Players.tsx @@ -1,46 +1,81 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { fetchPlayers,createPlayer,deletePlayer,updatePlayer } from './api'; + interface Player { - id: number; + id: string; name: string; - position: string; + email: string; } export default function PlayerManagement() { const [players, setPlayers] = useState([]); const [name, setName] = useState(""); - const [position, setPosition] = useState(""); - const [editingId, setEditingId] = useState(null); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [editingId, setEditingId] = useState(null); + const token = localStorage.getItem('token'); + + useEffect(() => { + if (token) loadPlayers(token); + }, [token]); + + + const loadPlayers = async (token: string) => { + try { + const data = await fetchPlayers(token); + console.log("Geladene Spieler:", data); + setPlayers(data); + } catch (error) { + console.error("Fehler beim Laden der Spieler:", error); + } + }; + const handleAddOrUpdate = () => { - if (!name || !position) return; + if (!name || !email) return; if (editingId !== null) { setPlayers(players.map(p => - p.id === editingId ? { ...p, name, position } : p + p.id === editingId ? { ...p, name, email } : p )); + if (token) { + updatePlayer(editingId, { name, email }, token); + } setEditingId(null); } else { const newPlayer: Player = { - id: Date.now(), + id: "", name, - position, + email, }; - setPlayers([...players, newPlayer]); + + if (token) { + createPlayer(newPlayer, token); + setPlayers([...players, newPlayer]); + } } setName(""); - setPosition(""); + setEmail(""); }; const handleEdit = (player: Player) => { setName(player.name); - setPosition(player.position); + setEmail(player.email); setEditingId(player.id); }; - const handleDelete = (id: number) => { + const handleDelete = (id: string) => { setPlayers(players.filter(p => p.id !== id)); + if (token) { + deletePlayer(id, token); + } + if (editingId === id) { + setEditingId(null); + setName(""); + setEmail(""); + } }; return ( @@ -57,9 +92,16 @@ export default function PlayerManagement() { /> setPosition(e.target.value)} + placeholder="Email" + value={email} + onChange={(e) => setEmail(e.target.value)} + className="border p-2 rounded" + /> + setPassword(e.target.value)} className="border p-2 rounded" /> @@ -75,7 +117,7 @@ export default function PlayerManagement() { Name - Position + Email Aktionen @@ -83,7 +125,7 @@ export default function PlayerManagement() { {players.map(player => ( {player.name} - {player.position} + {player.email} + + )} + +
+

Alle Teams

+
    + {teams.map((team) => ( +
  • + {team.name} – Spieler: {team.players.join(', ')} +
    + {canEdit(team) && ( + + )} + {canDelete(team) && ( + + )} +
    +
  • + ))} +
+ + ); +}; + +export default TeamManagement; diff --git a/frontend/src/pages/api.tsx b/frontend/src/pages/api.tsx index 654c2fd..8cfa839 100644 --- a/frontend/src/pages/api.tsx +++ b/frontend/src/pages/api.tsx @@ -34,6 +34,63 @@ export async function updateTournament(id: string, data: any, token: string) { return res.json(); } +export async function createTournament(data: any, token: string) { + const res = await fetch(`${API_URL}/tournaments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error('Turnier-Erstellung fehlgeschlagen'); + return res.json(); +} + +export async function deleteTournament(id: string, token: string) { + const res = await fetch(`${API_URL}/tournaments/${id}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error('Turnier-Löschung fehlgeschlagen'); + return res.json(); +} + +export async function fetchPlayers(token: string) { + const res = await fetch(`${API_URL}/players`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error('Fehler beim Laden der Spieler'); + return res.json(); +} + +export async function createPlayer(player: { name: string, email: string }, token: string) { + const res = await fetch(`${API_URL}/players`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify(player), + }); + if (!res.ok) throw new Error('Spieler-Erstellung fehlgeschlagen'); + return res.json(); +} + +export async function updatePlayer(id: string, player: { name?: string, email?: string }, token: string) { + const res = await fetch(`${API_URL}/players/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify(player), + }); + if (!res.ok) throw new Error('Spieler-Aktualisierung fehlgeschlagen'); + return res.json(); +} + +export async function deletePlayer(id: string, token: string) { + const res = await fetch(`${API_URL}/players/${id}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) throw new Error('Spieler-Löschung fehlgeschlagen'); + return res.json(); +} + + export async function registerTeam(id: string, team: { name: string }, token: string) { const res = await fetch(`${API_URL}/tournaments/${id}/register`, { method: 'POST',