Add driver profile stats API, nickname update editing, and security checks

This commit is contained in:
2026-07-17 23:18:16 +04:00
parent a5d2ee32c6
commit 5d2e0d59df
14 changed files with 882 additions and 403 deletions
+118 -1
View File
@@ -24,9 +24,12 @@ import (
"strconv"
"strings"
"github.com/x0gp/server/internal/auth"
"github.com/x0gp/server/internal/clans"
"github.com/x0gp/server/internal/config"
"github.com/x0gp/server/internal/drivers"
"github.com/x0gp/server/internal/lobby"
"github.com/x0gp/server/internal/races"
"github.com/x0gp/server/internal/transport"
)
@@ -284,13 +287,102 @@ func driversListHandler(svc *drivers.Service) http.HandlerFunc {
// @Router /api/drivers/{id} [put]
// @Router /api/drivers/{id} [delete]
// @Router /api/drivers/{id}/car [put]
func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.HandlerFunc {
func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service, racesSvc *races.Service, cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/drivers/")
if id == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return
}
if strings.HasSuffix(id, "/profile") {
driverID := strings.TrimSuffix(id, "/profile")
if driverID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return
}
if driverID == "me" {
authedID, ok := auth.DriverIDFromContext(r.Context())
if !ok {
if cfg.DevMode {
driverID = "driver-alice"
} else {
writeError(w, http.StatusUnauthorized, "unauthorized", "missing or invalid auth token")
return
}
} else {
driverID = authedID
}
}
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method)
return
}
drv, err := svc.Get(r.Context(), driverID)
if err != nil {
if errors.Is(err, drivers.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "driver not found")
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
dbStats, dbLast, dbBest, err := racesSvc.GetDriverProfileStats(r.Context(), driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
var lastSummary *transport.DriverRaceSummary
if dbLast != nil {
lastSummary = &transport.DriverRaceSummary{
RaceID: dbLast.RaceID,
RaceName: dbLast.RaceName,
FinishedMs: dbLast.FinishedMs,
Position: dbLast.Position,
TotalTimeMs: dbLast.TotalTimeMs,
BestLapMs: dbLast.BestLapMs,
TrackID: dbLast.TrackID,
TrackName: dbLast.TrackName,
}
}
var bestSummary *transport.DriverRaceSummary
if dbBest != nil {
bestSummary = &transport.DriverRaceSummary{
RaceID: dbBest.RaceID,
RaceName: dbBest.RaceName,
FinishedMs: dbBest.FinishedMs,
Position: dbBest.Position,
TotalTimeMs: dbBest.TotalTimeMs,
BestLapMs: dbBest.BestLapMs,
TrackID: dbBest.TrackID,
TrackName: dbBest.TrackName,
}
}
profile := transport.DriverProfileResponse{
Driver: driverToWire(drv),
Stats: transport.DriverStats{
TotalRacesStarted: dbStats.TotalRacesStarted,
TotalRacesFinished: dbStats.TotalRacesFinished,
Wins: dbStats.Wins,
Podiums: dbStats.Podiums,
BestLapMs: dbStats.BestLapMs,
TotalPlaytimeMs: dbStats.TotalPlaytimeMs,
},
LastRace: lastSummary,
BestRace: bestSummary,
}
writeJSON(w, http.StatusOK, profile)
return
}
if strings.HasSuffix(id, "/car") {
driverID := strings.TrimSuffix(id, "/car")
if driverID == "" {
@@ -389,12 +481,21 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
}
writeJSON(w, http.StatusOK, driverToWire(d))
case http.MethodPut:
if !cfg.DevMode {
authedID, ok := auth.DriverIDFromContext(r.Context())
if !ok || authedID != id {
writeError(w, http.StatusForbidden, "forbidden", "cannot update another driver's profile")
return
}
}
var req transport.DriverUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
d, err := svc.Update(r.Context(), id, drivers.UpdateInput{
Nickname: req.Nickname,
Name: req.Name,
AvatarURL: req.AvatarURL,
ClanID: req.ClanID,
@@ -404,11 +505,27 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
writeError(w, http.StatusNotFound, "not_found", "driver not found")
return
}
if errors.Is(err, drivers.ErrAlreadyExists) {
writeError(w, http.StatusConflict, "conflict", fmt.Sprintf("nickname %s is already taken", req.Nickname))
return
}
if errors.Is(err, drivers.ErrInvalidInput) {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
writeJSON(w, http.StatusOK, driverToWire(d))
case http.MethodDelete:
if !cfg.DevMode {
authedID, ok := auth.DriverIDFromContext(r.Context())
if !ok || authedID != id {
writeError(w, http.StatusForbidden, "forbidden", "cannot delete another driver's profile")
return
}
}
if err := svc.Delete(r.Context(), id); err != nil {
if errors.Is(err, drivers.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "driver not found")
+1 -1
View File
@@ -312,7 +312,7 @@ func main() {
apiMux.HandleFunc("/api/clans", clansRouter(clansSvc))
apiMux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc))
apiMux.HandleFunc("/api/drivers", driversRouter(driversSvc))
apiMux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc))
apiMux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc, racesSvc, cfg))
apiMux.HandleFunc("/api/video/stream", videoSvc.StreamHandler())
apiMux.HandleFunc("/api/video/control", videoSvc.ControlHandler())
apiMux.HandleFunc("/api/webrtc/connect", webrtcSvc.ConnectHandler())