ADD: team site and player/usermanagement is working

This commit is contained in:
hwinkel
2025-11-22 14:51:53 +01:00
parent 846a922a41
commit 139a99d96e
13 changed files with 229 additions and 111 deletions

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"log"
"os" "os"
"volleyball/internal/auth" "volleyball/internal/auth"
"volleyball/internal/database" "volleyball/internal/database"
@@ -63,12 +64,16 @@ func main() {
player.CreatePlayer(c, db.GetDB()) player.CreatePlayer(c, db.GetDB())
}) })
api.PUT("/players/:id", func(c *gin.Context) { api.PUT("/players/:id", func(c *gin.Context) {
log.Println("PUT /players/:id called", c.Params)
player.UpdatePlayer(c, db.GetDB()) player.UpdatePlayer(c, db.GetDB())
}) })
api.DELETE("/players/:id", func(c *gin.Context) { api.DELETE("/players/:id", func(c *gin.Context) {
player.DeletePlayer(c, db.GetDB()) player.DeletePlayer(c, db.GetDB())
// c.JSON(http.StatusOK, gin.H{"message": "Player deleted successfully"}) // c.JSON(http.StatusOK, gin.H{"message": "Player deleted successfully"})
}) })
api.GET("/teams", func(c *gin.Context) {
})
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {

View File

@@ -100,13 +100,15 @@ func GetPlayer(c *gin.Context, db *sql.DB, id string) {
} }
func UpdatePlayer(c *gin.Context, db *sql.DB) { func UpdatePlayer(c *gin.Context, db *sql.DB) {
log.Println(c)
playerID := c.Param("id") playerID := c.Param("id")
if playerID == "" { if playerID == "" {
common.RespondError(c, http.StatusBadRequest, "Player ID is required") common.RespondError(c, http.StatusBadRequest, "Player ID is required")
return return
} }
log.Println("role: ", c.GetString("role"))
if playerID != c.GetString("userId") || c.GetString("role") != "admin" { // playerID != c.GetString("userId") ||
if c.GetString("role") != "admin" {
common.RespondError(c, http.StatusForbidden, "You do not have permission to update this player") common.RespondError(c, http.StatusForbidden, "You do not have permission to update this player")
return return
} }

View File

@@ -11,20 +11,20 @@ import (
) )
type User struct { type User struct {
UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"` UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"`
Username string `db:"username" sql:"VARCHAR(100)"` Username string `db:"username" sql:"VARCHAR(100)"`
Email string `db:"email" sql:"VARCHAR(255)" index:"true"` Email string `db:"email" sql:"VARCHAR(255)" index:"true"`
lastname sql.NullString `db:"lastname" sql:"VARCHAR(100)"` lastname sql.NullString `db:"lastname" sql:"VARCHAR(100)"`
firstname sql.NullString `db:"firstname" sql:"VARCHAR(100)"` firstname sql.NullString `db:"firstname" sql:"VARCHAR(100)"`
password string `db:"password_hash" sql:"VARCHAR(255)"` password string `db:"password_hash" sql:"VARCHAR(255)"`
phone sql.NullString `db:"phone" sql:"VARCHAR(20)"` phone sql.NullString `db:"phone" sql:"VARCHAR(20)"`
avatarURL sql.NullString `db:"avatar_url" sql:"VARCHAR(255)"` avatarURL sql.NullString `db:"avatar_url" sql:"VARCHAR(255)"`
IsActive sql.NullBool `db:"is_active" sql:"BOOLEAN"` IsActive sql.NullBool `db:"is_active" sql:"BOOLEAN"`
birthday *time.Time `db:"birthday" sql:"DATE"` birthday *time.Time `db:"birthday" sql:"DATE"`
createdAt *time.Time `db:"created_at" sql:"TIMESTAMP"` Created_at *time.Time `db:"created_at" sql:"TIMESTAMP"`
updatedAt *time.Time `db:"updated_at" sql:"TIMESTAMP"` updatedAt *time.Time `db:"updated_at" sql:"TIMESTAMP"`
LastLogin *time.Time `db:"last_login" sql:"TIMESTAMP"` LastLogin *time.Time `db:"last_login" sql:"TIMESTAMP"`
Role []string `ignore:"true"` // wird NICHT in der DB angelegt Role []string `ignore:"true"` // wird NICHT in der DB angelegt
} }
@@ -236,7 +236,7 @@ func GetAllPlayers(db *sql.DB) ([]User, error) {
&player.lastname, &player.lastname,
&player.birthday, &player.birthday,
&player.IsActive, &player.IsActive,
&player.createdAt, &player.Created_at,
(*pq.StringArray)(&player.Role), (*pq.StringArray)(&player.Role),
); err != nil { ); err != nil {
log.Printf("Error scanning player row: %v", err) log.Printf("Error scanning player row: %v", err)
@@ -305,7 +305,7 @@ func updatePlayer(db *sql.DB, player User) error {
log.Printf("Updating player: ID=%v, Name=%v, Email=%v", player.UUID, player.Username, player.Email) log.Printf("Updating player: ID=%v, Name=%v, Email=%v", player.UUID, player.Username, player.Email)
stmt := "UPDATE public.users SET name = $1, email = $2 WHERE id = $3" stmt := "UPDATE public.users SET username = $1, email = $2 WHERE uuid = $3"
_, err := db.Exec(stmt, player.Username, player.Email, player.UUID) _, err := db.Exec(stmt, player.Username, player.Email, player.UUID)
if err != nil { if err != nil {
log.Printf("Error updating player in database: %v", err) log.Printf("Error updating player in database: %v", err)

View File

@@ -0,0 +1,31 @@
package team
import (
"database/sql"
"log"
"net/http"
"volleyball/internal/common"
"github.com/gin-gonic/gin"
)
func GetTeams(c *gin.Context, db *sql.DB) {
log.Println(c.GetString("userId"), c.GetString("email"), c.GetString("role"))
// Simulate fetching players from a database
teams, err := GetAllTeams(db)
if err != nil {
log.Printf("Error retrieving teams: %v", err)
common.RespondError(c, http.StatusInternalServerError, "Failed to retrieve players")
return
}
if len(teams) > 0 {
log.Printf("User %s (%s) requested players", c.GetString("userId"), c.GetString("email"))
c.JSON(http.StatusOK, teams)
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")
}

View File

@@ -0,0 +1,25 @@
package team
import (
"database/sql"
)
type Player struct {
UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"`
Email string `db:"email" sql:"VARCHAR(255)" index:"true"`
Username string `db:"username" sql:"VARCHAR(100)"`
FirstName string `db:"firstname" sql:"VARCHAR(100)"`
LastName string `db:"lastname" sql:"VARCHAR(100)"`
}
type Team struct {
UUID string `db:"uuid" sql:"VARCHAR(255)" index:"true"`
Name string `db:"username" sql:"VARCHAR(100)"`
Email string `db:"email" sql:"VARCHAR(255)" index:"true"`
PlayersUUID []Player `db:"players" sql:"JSVARCHAR(255)"`
}
func GetAllTeams(db *sql.DB) ([]Team, error) {
// Implementation to retrieve all teams from the database
return []Team{}, nil
}

View File

@@ -0,0 +1,8 @@
import {UserBaseInformation } from "./users";
export interface Team {
teamUUID: string;
name: string;
Player: UserBaseInformation[];
created_at?: string; // ISO date
}

View File

@@ -11,50 +11,54 @@ export enum UserRole {
/** Primary user model returned from the API */ /** Primary user model returned from the API */
export interface User { export interface UserBaseInformation {
UUID: string UUID: string
Username: string Username: string
AvatarUrl?: string
IsActive: boolean
}
export interface User extends UserBaseInformation {
Email: string Email: string
FirstName?: string FirstName?: string
LastName?: string LastName?: string
AvatarUrl?: string
Role: UserRole[] Role: UserRole[]
IsActive: boolean
Phone?: string Phone?: string
TeamId?: string[] Created_at?: string // ISO date
CreatedAt?: string // ISO date Updated_at?: string // ISO date
UpdatedAt?: string // ISO date
LastLogin?: string // ISO date LastLogin?: string // ISO date
} }
/** Data required to create a new user */ /** Data required to create a new user */
export interface CreateUserDTO { // export interface CreateUserDTO {
username: string // username: string
email: string // email: string
password: string // password: string
firstName?: string // firstName?: string
lastName?: string // lastName?: string
roles?: UserRole[] // roles?: UserRole[]
} // }
/** Data for partial updates to a user */ /** Data for partial updates to a user */
export interface UpdateUserDTO { // export interface UpdateUserDTO {
username?: string // username?: string
email?: string // email?: string
password?: string // password?: string
firstName?: string | null // firstName?: string | null
lastName?: string | null // lastName?: string | null
avatarUrl?: string | null // avatarUrl?: string | null
roles?: UserRole[] // roles?: UserRole[]
isActive?: boolean // isActive?: boolean
teamId?: string | null // teamId?: string | null
metadata?: Record<string, unknown> | null // metadata?: Record<string, unknown> | null
} // }
/** Simple auth state slice for frontend state management */ /** Simple auth state slice for frontend state management */
export interface AuthState { // export interface AuthState {
currentUser?: User | null // currentUser?: User | null
token?: string | null // token?: string | null
isLoading: boolean // isLoading: boolean
error?: string | null // error?: string | null
} // }

View File

@@ -0,0 +1,10 @@
const API_URL = 'http://localhost:8080/api';
export async function getTeams(token:string)
{
const res = await fetch(`${API_URL}/team/`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error('Fehler beim Laden des Spielers');
return res.json();
}

View File

@@ -3,6 +3,8 @@ import { fetchPlayers } from "../api";
import { User, UserRole } from "../../components/interfaces/users"; import { User, UserRole } from "../../components/interfaces/users";
import UsersPage from "./Users"; import UsersPage from "./Users";
import TMP from "./TMP"; import TMP from "./TMP";
import TeamManagement from "../Teams";
import Teams from "./Teams";
// type User = { // type User = {
// id: string; // id: string;
@@ -227,36 +229,8 @@ export default function Administration(): JSX.Element {
{activeTab === "teams" && ( {activeTab === "teams" && (
<section> <section>
<h2>Teams</h2> <Teams />
<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> </section>
)} )}

View File

@@ -0,0 +1,51 @@
import React, { useEffect, useState } from "react";
import { Team } from "../../components/interfaces/Team";
import { get } from "http";
import { getTeams } from "../../components/requests/TeamData";
const Teams: React.FC = () => {
const [teams, setTeams] = useState<Team[]>([]);
useEffect(() => {
// Replace with API call in production
load();
}, []);
async function load()
{
try {
const teams : Team[] = await getTeams(localStorage.getItem("token") || "");
setTeams(teams);
} catch (error) {
console.error("Fehler beim Laden der Teams:", error);
}
finally {
}
}
return (
<div style={{ padding: "2rem" }}>
<h1>Teams Administration</h1>
<table style={{ width: "100%", borderCollapse: "collapse", marginTop: "1rem" }}>
<thead>
<tr>
<th style={{ borderBottom: "1px solid #ccc", textAlign: "left" }}>Team Name</th>
</tr>
</thead>
<tbody>
{teams.map(team => (
<tr key={team.teamUUID}>
<td style={{ padding: "0.5rem 0" }}>{team.name}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default Teams;

View File

@@ -51,19 +51,25 @@ export default function UsersPage(): JSX.Element {
params.set("page", String(page)); params.set("page", String(page));
params.set("limit", String(pageSize)); params.set("limit", String(pageSize));
const token = localStorage.getItem('token'); console.log("Fetching users with params:", params.toString());
if (!token) throw new Error("No auth token found");
const data = await fetchPlayers(token); const token = localStorage.getItem('token');
console.log("Loaded users:", data); if (!token) throw new Error("No auth token found");
// const res = await fetch(`/api/users?${params.toString()}`, { const data = await fetchPlayers(token);
// signal: ac.signal,
// }); const filteredData = data.filter((user: User) =>
user.Username.toLowerCase().includes(query.toLowerCase()) ||
user.Email.toLowerCase().includes(query.toLowerCase())
);
setUsers(filteredData);
console.log("Loaded users:", data);
// if (!res.ok) throw new Error(`Failed to load users: ${res.status}`); // if (!res.ok) throw new Error(`Failed to load users: ${res.status}`);
// const data = await res.json(); // const data = await res.json();
// Expecting { users: User[] } or User[] — handle both. // Expecting { users: User[] } or User[] — handle both.
const list: User[] = Array.isArray(data) ? data : data.users ?? []; // const list: User[] = Array.isArray(data) ? data : data.users ?? [];
setUsers(list); // setUsers(list);
} catch (err: any) { } catch (err: any) {
if (err.name !== "AbortError") setError(err.message || String(err)); if (err.name !== "AbortError") setError(err.message || String(err));
} finally { } finally {
@@ -99,31 +105,21 @@ export default function UsersPage(): JSX.Element {
const payload = { ...form }; const payload = { ...form };
let res: Response; let res: Response;
if (editing) { if (editing) {
console.log("Editing user:", editing.UUID, "with data:", form);
res = await updatePlayer(editing.UUID, { res = await updatePlayer(editing.UUID, {
Username: form.name, Username: form.name,
Email: form.email, Email: form.email,
Role: form.role.split(","),
}, localStorage.getItem('token') || ""); }, localStorage.getItem('token') || "");
// res = await createPlayer({
// Username: form.name,
// Email: form.email,
// }, localStorage.getItem('token') || "");
// res = await fetch(`/api/users/${editing.UUID}`, {
// method: "PUT",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// });
} else { } else {
res = await createPlayer({ res = await createPlayer({
Username: form.name, Username: form.name,
Email: form.email, Email: form.email,
}, localStorage.getItem('token') || ""); }, localStorage.getItem('token') || "");
console.log("Create response:", res); console.log("Create response:", res);
// res = await fetch(`/api/users`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// body: JSON.stringify(payload),
// });
} }
// Parse response body (support both fetch Response and already-parsed results) // Parse response body (support both fetch Response and already-parsed results)
@@ -176,8 +172,8 @@ export default function UsersPage(): JSX.Element {
placeholder="Search by name or email..." placeholder="Search by name or email..."
value={query} value={query}
onChange={(e) => { onChange={(e) => {
console.log("Search query:", e.target.value);
setQuery(e.target.value); setQuery(e.target.value);
setPage(1);
}} }}
style={{ padding: 6, flex: 1 }} style={{ padding: 6, flex: 1 }}
/> />
@@ -229,7 +225,7 @@ export default function UsersPage(): JSX.Element {
</td> </td>
<td style={{ padding: 8 }}>{u.IsActive ? "Yes" : "No"}</td> <td style={{ padding: 8 }}>{u.IsActive ? "Yes" : "No"}</td>
<td style={{ padding: 8 }}> <td style={{ padding: 8 }}>
{u.CreatedAt ? new Date(u.CreatedAt).toLocaleString() : "—"} {u.Created_at ? new Date(u.Created_at).toLocaleString() : "—"}
</td> </td>
<td style={{ padding: 8 }}> <td style={{ padding: 8 }}>
<button onClick={() => openEdit(u)} style={{ marginRight: 8 }}> <button onClick={() => openEdit(u)} style={{ marginRight: 8 }}>
@@ -321,11 +317,21 @@ export default function UsersPage(): JSX.Element {
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<label style={{ display: "block", fontSize: 13, marginBottom: 4 }}>Role</label> <label style={{ display: "block", fontSize: 13, marginBottom: 4 }}>Role</label>
<select <select
value={form.role} multiple
onChange={(e) => setForm((f) => ({ ...f, role: e.target.value }))} value={form.role.split(",")}
onChange={(e) => {
const options = e.target.options;
const selectedRoles: string[] = [];
for (let i = 0; i < options.length; i++) {
if (options[i].selected) {
selectedRoles.push(options[i].value);
}
}
setForm((f) => ({ ...f, role: selectedRoles.join(",") }));
}}
style={{ width: "100%", padding: 8 }} style={{ width: "100%", padding: 8 }}
> >
<option value="user">User</option> <option value="player">Player</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
<option value="coach">Coach</option> <option value="coach">Coach</option>
</select> </select>

View File

@@ -42,7 +42,8 @@ export default function PlayerManagement() {
p.UUID === editingId ? { ...p, Username, Email } : p p.UUID === editingId ? { ...p, Username, Email } : p
)); ));
if (token) { if (token) {
updatePlayer(editingId, { Username, Email }, token); // updatePlayer(editingId, { Username, Email }, token);
throw new Error("Not implemented yet");
} }
setEditingId(null); setEditingId(null);
} else { } else {

View File

@@ -80,12 +80,13 @@ export async function createPlayer(player: { Username: string, Email: string },
return res.json(); return res.json();
} }
export async function updatePlayer(id: string, player: { Username?: string, Email?: string }, token: string) { export async function updatePlayer(id: string, player: { Username?: string, Email?: string, Role: string[] }, token: string) {
const res = await fetch(`${API_URL}/players/${id}`, { const res = await fetch(`${API_URL}/players/${id}`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(player), body: JSON.stringify(player),
}); });
console.log("Update response:", res);
if (!res.ok) throw new Error('Spieler-Aktualisierung fehlgeschlagen'); if (!res.ok) throw new Error('Spieler-Aktualisierung fehlgeschlagen');
return res.json(); return res.json();
} }