ADD: added database connection for players data handling and started login funtion with database
This commit is contained in:
@@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"volleyball/internal/auth"
|
"volleyball/internal/auth"
|
||||||
|
"volleyball/internal/database"
|
||||||
|
"volleyball/internal/player"
|
||||||
"volleyball/internal/tournament"
|
"volleyball/internal/tournament"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
@@ -10,6 +12,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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()
|
r := gin.Default()
|
||||||
|
|
||||||
// CORS
|
// CORS
|
||||||
@@ -21,7 +36,10 @@ func main() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Public
|
// 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)
|
r.GET("/api/tournaments", tournament.ListTournaments)
|
||||||
|
|
||||||
// Protected API
|
// Protected API
|
||||||
@@ -32,6 +50,19 @@ func main() {
|
|||||||
api.POST("/tournaments/:id/join", tournament.JoinTournament)
|
api.POST("/tournaments/:id/join", tournament.JoinTournament)
|
||||||
api.PUT("/tournaments/:id", tournament.UpdateTournament)
|
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")
|
port := os.Getenv("PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "8080"
|
port = "8080"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
|||||||
@@ -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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
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=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
"volleyball/internal/common"
|
||||||
|
"volleyball/internal/player"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -16,23 +19,53 @@ type LoginResponse struct {
|
|||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoginHandler(c *gin.Context) {
|
func LoginHandler(c *gin.Context, db *sql.DB) {
|
||||||
var req LoginRequest
|
var req LoginRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Systemnutzer
|
// Validate input
|
||||||
if req.Email == "test@localhost.de" {
|
if req.Email == "" || req.Password == "" {
|
||||||
token, err := CreateJWT("system-user-id", req.Email, "admin", time.Hour*24*7)
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Email and password are required"})
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, LoginResponse{Token: token})
|
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
12
backend/internal/common/passwordHasher.go
Normal file
12
backend/internal/common/passwordHasher.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -17,6 +17,29 @@ type database struct {
|
|||||||
db *sql.DB // Pointer to sql.DB
|
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 {
|
func New(host string, port int, user, password, dbname string) *database {
|
||||||
return &database{
|
return &database{
|
||||||
host: host,
|
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...")
|
fmt.Println("Connecting to the database...")
|
||||||
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
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)
|
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")
|
log.Println("Connected to the database successfully")
|
||||||
d.db = db
|
d.db = db
|
||||||
return nil
|
return db
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,66 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"volleyball/internal/player"
|
||||||
|
|
||||||
_ "github.com/lib/pq" // Import the PostgreSQL driver
|
_ "github.com/lib/pq" // Import the PostgreSQL driver
|
||||||
)
|
)
|
||||||
|
|
||||||
const playerTable = `
|
const teamTable = `
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
CREATE TABLE IF NOT EXISTS teams (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(100) NOT NULL,
|
player1_id uuid NOT NULL,
|
||||||
age INT NOT NULL,
|
player2_id uuid NOT NULL,
|
||||||
birthday DATE 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{
|
tables := []string{
|
||||||
playerTable,
|
player.PlayerTable,
|
||||||
|
player.RoleTable,
|
||||||
|
teamTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
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)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
145
backend/internal/player/handler.go
Normal file
145
backend/internal/player/handler.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
286
backend/internal/player/model.go
Normal file
286
backend/internal/player/model.go
Normal file
@@ -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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"volleyball/internal/common"
|
"volleyball/internal/common"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ var tournaments = []Tournament{
|
|||||||
{ID: "t1", Name: "Smasher"},
|
{ID: "t1", Name: "Smasher"},
|
||||||
{ID: "t2", Name: "Blockbuster"},
|
{ID: "t2", Name: "Blockbuster"},
|
||||||
},
|
},
|
||||||
OrganizerId: "system-user-id",
|
OrganizerId: []string{"example-user"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "2",
|
ID: "2",
|
||||||
@@ -25,7 +27,7 @@ var tournaments = []Tournament{
|
|||||||
Location: "Hamburg",
|
Location: "Hamburg",
|
||||||
MaxParticipants: 10,
|
MaxParticipants: 10,
|
||||||
Teams: []Team{},
|
Teams: []Team{},
|
||||||
OrganizerId: "other-user",
|
OrganizerId: []string{"example-user"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,8 @@ func UpdateTournament(c *gin.Context) {
|
|||||||
|
|
||||||
for i, t := range tournaments {
|
for i, t := range tournaments {
|
||||||
if t.ID == id {
|
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"})
|
c.JSON(http.StatusForbidden, gin.H{"error": "No permission"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ type Team struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Tournament struct {
|
type Tournament struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
MaxParticipants int `json:"maxParticipants"`
|
MaxParticipants int `json:"maxParticipants"`
|
||||||
Teams []Team `json:"teams"`
|
Teams []Team `json:"teams"`
|
||||||
OrganizerId string `json:"organizerId"`
|
OrganizerId []string `json:"PlayerId"` // List of player IDs who are organizers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Dashboard from './pages/Dashboard';
|
|||||||
import TournamentDetails from './pages/TournamentDetails';
|
import TournamentDetails from './pages/TournamentDetails';
|
||||||
import Players from './pages/Players';
|
import Players from './pages/Players';
|
||||||
import Navigation from './pages/Navigation';
|
import Navigation from './pages/Navigation';
|
||||||
|
import TeamManagement from './pages/Teams';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
@@ -19,6 +20,8 @@ function App() {
|
|||||||
|
|
||||||
{/* Geschützte Routen */}
|
{/* Geschützte Routen */}
|
||||||
<Route path="/players" element={<ProtectedRoute><Players /></ProtectedRoute>} />
|
<Route path="/players" element={<ProtectedRoute><Players /></ProtectedRoute>} />
|
||||||
|
<Route path="/teams" element={<ProtectedRoute><TeamManagement /></ProtectedRoute>} />
|
||||||
|
|
||||||
<Route path="/tournaments/:id" element={<ProtectedRoute><TournamentDetails /></ProtectedRoute>} />
|
<Route path="/tournaments/:id" element={<ProtectedRoute><TournamentDetails /></ProtectedRoute>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
23
frontend/src/components/utils/jwt.tsx
Normal file
23
frontend/src/components/utils/jwt.tsx
Normal file
@@ -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<TokenPayload>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { getUserFromToken } from '../components/utils/jwt';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
@@ -17,10 +18,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedToken = localStorage.getItem('token');
|
const storedToken = localStorage.getItem('token');
|
||||||
const storedUserId = localStorage.getItem('userId');
|
const storedUserId = localStorage.getItem('userId');
|
||||||
|
if (!storedToken) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (storedToken && storedUserId) {
|
if (storedToken && storedUserId) {
|
||||||
setToken(storedToken);
|
setToken(storedToken);
|
||||||
setUserId(storedUserId);
|
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) => {
|
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('token');
|
||||||
localStorage.removeItem('userId');
|
localStorage.removeItem('userId');
|
||||||
localStorage.removeItem('role');
|
localStorage.removeItem('role');
|
||||||
|
localStorage.clear(); // Optional: Entfernt alle gespeicherten Daten
|
||||||
|
// Hier könntest du auch eine Weiterleitung zur Login-Seite hinzufügen
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuth } from './AuthContext';
|
import { useAuth } from './AuthContext';
|
||||||
import { login as apiLogin } from './api';
|
import { login as apiLogin } from './api';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
// import { jwtDecode } from 'jwt-decode';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { getUserFromToken } from '../components/utils/jwt';
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@@ -18,12 +18,13 @@ export default function LoginPage() {
|
|||||||
const data = await apiLogin(email, password);
|
const data = await apiLogin(email, password);
|
||||||
// Token aus JWT extrahieren (hier: UserID im Token Payload)
|
// Token aus JWT extrahieren (hier: UserID im Token Payload)
|
||||||
// Für Demo: Einfach Dummy UserID setzen, oder später JWT decode implementieren
|
// Für Demo: Einfach Dummy UserID setzen, oder später JWT decode implementieren
|
||||||
type MyJwtPayload = {
|
|
||||||
userId: string
|
var decodedData = getUserFromToken(data.token);
|
||||||
email: string;
|
if (!decodedData || !decodedData.userId || !decodedData.role) {
|
||||||
role: string;
|
setError('Ungültiges Token');
|
||||||
} & object;
|
return;
|
||||||
const decodedData = jwtDecode<MyJwtPayload>(data.token);
|
}
|
||||||
|
// Dummy UserID für Demo
|
||||||
login(data.token, decodedData.userId, decodedData.role);
|
login(data.token, decodedData.userId, decodedData.role);
|
||||||
// Nach dem Login zur Dashboard-Seite navigieren
|
// Nach dem Login zur Dashboard-Seite navigieren
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default function Navigation() {
|
|||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<Link to="/">Dashboard</Link>
|
<Link to="/">Dashboard</Link>
|
||||||
{token && <Link to="/players">Spieler</Link>}
|
{token && <Link to="/players">Spieler</Link>}
|
||||||
|
{token && <Link to="/teams">Teams</Link>}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{token ? (
|
{token ? (
|
||||||
@@ -16,7 +17,11 @@ export default function Navigation() {
|
|||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Link to="/login" className="px-3 py-1 border rounded">Login</Link>
|
<>
|
||||||
|
<Link to="/register" className="px-3 py-1 border rounded">Register</Link>
|
||||||
|
<span className="mx-2"></span>
|
||||||
|
<Link to="/login" className="px-3 py-1 border rounded bg-green-500 text-white">Login</Link>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,46 +1,81 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { fetchPlayers,createPlayer,deletePlayer,updatePlayer } from './api';
|
||||||
|
|
||||||
|
|
||||||
interface Player {
|
interface Player {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
position: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlayerManagement() {
|
export default function PlayerManagement() {
|
||||||
const [players, setPlayers] = useState<Player[]>([]);
|
const [players, setPlayers] = useState<Player[]>([]);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [position, setPosition] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
const [password, setPassword] = useState("");
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(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 = () => {
|
const handleAddOrUpdate = () => {
|
||||||
if (!name || !position) return;
|
if (!name || !email) return;
|
||||||
|
|
||||||
if (editingId !== null) {
|
if (editingId !== null) {
|
||||||
setPlayers(players.map(p =>
|
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);
|
setEditingId(null);
|
||||||
} else {
|
} else {
|
||||||
const newPlayer: Player = {
|
const newPlayer: Player = {
|
||||||
id: Date.now(),
|
id: "",
|
||||||
name,
|
name,
|
||||||
position,
|
email,
|
||||||
};
|
};
|
||||||
setPlayers([...players, newPlayer]);
|
|
||||||
|
if (token) {
|
||||||
|
createPlayer(newPlayer, token);
|
||||||
|
setPlayers([...players, newPlayer]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setName("");
|
setName("");
|
||||||
setPosition("");
|
setEmail("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (player: Player) => {
|
const handleEdit = (player: Player) => {
|
||||||
setName(player.name);
|
setName(player.name);
|
||||||
setPosition(player.position);
|
setEmail(player.email);
|
||||||
setEditingId(player.id);
|
setEditingId(player.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: number) => {
|
const handleDelete = (id: string) => {
|
||||||
setPlayers(players.filter(p => p.id !== id));
|
setPlayers(players.filter(p => p.id !== id));
|
||||||
|
if (token) {
|
||||||
|
deletePlayer(id, token);
|
||||||
|
}
|
||||||
|
if (editingId === id) {
|
||||||
|
setEditingId(null);
|
||||||
|
setName("");
|
||||||
|
setEmail("");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,9 +92,16 @@ export default function PlayerManagement() {
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Position (z. B. Zuspieler)"
|
placeholder="Email"
|
||||||
value={position}
|
value={email}
|
||||||
onChange={(e) => setPosition(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="border p-2 rounded"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="border p-2 rounded"
|
className="border p-2 rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +117,7 @@ export default function PlayerManagement() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-100 text-left">
|
<tr className="bg-gray-100 text-left">
|
||||||
<th className="border px-4 py-2">Name</th>
|
<th className="border px-4 py-2">Name</th>
|
||||||
<th className="border px-4 py-2">Position</th>
|
<th className="border px-4 py-2">Email</th>
|
||||||
<th className="border px-4 py-2">Aktionen</th>
|
<th className="border px-4 py-2">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -83,7 +125,7 @@ export default function PlayerManagement() {
|
|||||||
{players.map(player => (
|
{players.map(player => (
|
||||||
<tr key={player.id}>
|
<tr key={player.id}>
|
||||||
<td className="border px-4 py-2">{player.name}</td>
|
<td className="border px-4 py-2">{player.name}</td>
|
||||||
<td className="border px-4 py-2">{player.position}</td>
|
<td className="border px-4 py-2">{player.email}</td>
|
||||||
<td className="border px-4 py-2 space-x-2">
|
<td className="border px-4 py-2 space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEdit(player)}
|
onClick={() => handleEdit(player)}
|
||||||
|
|||||||
142
frontend/src/pages/Teams.tsx
Normal file
142
frontend/src/pages/Teams.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { getUserFromToken } from '../components/utils/jwt'; // Importiere die Funktion zum Decodieren des Tokens
|
||||||
|
|
||||||
|
interface Team {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
players: string[]; // Emails oder Namen
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamManagement = () => {
|
||||||
|
const [teams, setTeams] = useState<Team[]>([]);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [player2, setPlayer2] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [editingTeam, setEditingTeam] = useState<Team | null>(null);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const user = token ? getUserFromToken(token) : null;
|
||||||
|
const isAdmin = user?.role === 'admin';
|
||||||
|
|
||||||
|
const fetchTeams = async () => {
|
||||||
|
const res = await fetch('/api/teams', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) setTeams(data.data || []);
|
||||||
|
else setError(data.error || 'Fehler beim Laden der Teams');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) fetchTeams();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const body = {
|
||||||
|
name,
|
||||||
|
players: [user?.email, player2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const method = editingTeam ? 'PUT' : 'POST';
|
||||||
|
const url = editingTeam ? `/api/teams/${editingTeam.id}` : '/api/teams';
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setName('');
|
||||||
|
setPlayer2('');
|
||||||
|
setEditingTeam(null);
|
||||||
|
fetchTeams();
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Fehler beim Speichern');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
const res = await fetch(`/api/teams/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) fetchTeams();
|
||||||
|
else setError('Löschen fehlgeschlagen');
|
||||||
|
};
|
||||||
|
|
||||||
|
const canEdit = (team: Team) => isAdmin;
|
||||||
|
const canDelete = (team: Team) =>
|
||||||
|
isAdmin || team.createdBy === user?.userId;
|
||||||
|
|
||||||
|
const canCreate = () => isAdmin || true; // Normale Nutzer dürfen eigene Teams erstellen
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto p-6 bg-white rounded-xl shadow-md mt-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Team Management</h2>
|
||||||
|
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||||
|
|
||||||
|
{canCreate() && (
|
||||||
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
||||||
|
<h3>{editingTeam ? 'Team bearbeiten' : 'Team erstellen'}</h3>
|
||||||
|
<input
|
||||||
|
className="border p-2 rounded"
|
||||||
|
type="text"
|
||||||
|
placeholder="Teamname"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="border p-2 rounded"
|
||||||
|
type="email"
|
||||||
|
placeholder="E-Mail von Mitspieler"
|
||||||
|
value={player2}
|
||||||
|
onChange={(e) => setPlayer2(e.target.value)}
|
||||||
|
required={!isAdmin}
|
||||||
|
/>
|
||||||
|
<button type="submit"
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
|
||||||
|
>{editingTeam ? 'Aktualisieren' : 'Erstellen'}</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h3>Alle Teams</h3>
|
||||||
|
<ul>
|
||||||
|
{teams.map((team) => (
|
||||||
|
<li key={team.id}>
|
||||||
|
<strong>{team.name}</strong> – Spieler: {team.players.join(', ')}
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
{canEdit(team) && (
|
||||||
|
<button onClick={() => {
|
||||||
|
setName(team.name);
|
||||||
|
setPlayer2(team.players[1] || '');
|
||||||
|
setEditingTeam(team);
|
||||||
|
}}>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{canDelete(team) && (
|
||||||
|
<button onClick={() => handleDelete(team.id)}>Löschen</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamManagement;
|
||||||
@@ -34,6 +34,63 @@ export async function updateTournament(id: string, data: any, token: string) {
|
|||||||
return res.json();
|
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) {
|
export async function registerTeam(id: string, team: { name: string }, token: string) {
|
||||||
const res = await fetch(`${API_URL}/tournaments/${id}/register`, {
|
const res = await fetch(`${API_URL}/tournaments/${id}/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Reference in New Issue
Block a user