package auth import ( "context" "net/http" "regexp" "time" "chessstars/internal/httpx" ) const sessionCookieName = "session" var usernameRe = regexp.MustCompile(`^[a-zA-Z0-9_]{3,32}$`) type contextKey int const userContextKey contextKey = 0 func UserFromContext(ctx context.Context) (*User, bool) { u, ok := ctx.Value(userContextKey).(*User) return u, ok } func Middleware(store *Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(sessionCookieName) if err == nil && cookie.Value != "" { if u, err := store.GetSessionUser(cookie.Value); err == nil { ctx := context.WithValue(r.Context(), userContextKey, u) r = r.WithContext(ctx) } } next.ServeHTTP(w, r) }) } } type Handlers struct { store *Store } func NewHandlers(store *Store) *Handlers { return &Handlers{store: store} } type credentials struct { Username string `json:"username"` Password string `json:"password"` } func (h *Handlers) Register(w http.ResponseWriter, r *http.Request) { var creds credentials if err := httpx.ReadJSON(r, &creds); err != nil { httpx.Error(w, http.StatusBadRequest, "invalid json") return } if !usernameRe.MatchString(creds.Username) { httpx.Error(w, http.StatusBadRequest, "invalid username") return } if len(creds.Password) < 6 || len(creds.Password) > 256 { httpx.Error(w, http.StatusBadRequest, "invalid password") return } u, err := h.store.CreateUser(creds.Username, creds.Password) if err != nil { httpx.Error(w, http.StatusConflict, "username taken") return } h.startSession(w, u.ID) httpx.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "username": u.Username}) } func (h *Handlers) Login(w http.ResponseWriter, r *http.Request) { var creds credentials if err := httpx.ReadJSON(r, &creds); err != nil { httpx.Error(w, http.StatusBadRequest, "invalid json") return } u, err := h.store.VerifyLogin(creds.Username, creds.Password) if err != nil { httpx.Error(w, http.StatusUnauthorized, "invalid credentials") return } h.startSession(w, u.ID) httpx.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "username": u.Username}) } func (h *Handlers) startSession(w http.ResponseWriter, userID int64) { token, err := h.store.CreateSession(userID) if err != nil { return } http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(30 * 24 * time.Hour), }) } func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(sessionCookieName); err == nil { _ = h.store.DeleteSession(cookie.Value) } http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1}) httpx.WriteJSON(w, http.StatusOK, map[string]any{"ok": true}) } type ownProfile struct { Username string `json:"username"` DisplayName string `json:"display_name"` Country string `json:"country"` Bio string `json:"bio"` PrivateNote string `json:"private_note"` Rating int `json:"rating"` Wins int `json:"wins"` Losses int `json:"losses"` Draws int `json:"draws"` } type publicProfile struct { Username string `json:"username"` DisplayName string `json:"display_name"` Country string `json:"country"` Bio string `json:"bio"` Rating int `json:"rating"` Wins int `json:"wins"` Losses int `json:"losses"` Draws int `json:"draws"` } func (h *Handlers) Me(w http.ResponseWriter, r *http.Request) { u, ok := UserFromContext(r.Context()) if !ok { httpx.Error(w, http.StatusUnauthorized, "not logged in") return } httpx.WriteJSON(w, http.StatusOK, ownProfile{ Username: u.Username, DisplayName: u.DisplayName, Country: u.Country, Bio: u.Bio, PrivateNote: u.PrivateNote, Rating: u.Rating, Wins: u.Wins, Losses: u.Losses, Draws: u.Draws, }) } type updateProfileReq struct { DisplayName string `json:"display_name"` Country string `json:"country"` Bio string `json:"bio"` PrivateNote string `json:"private_note"` } func (h *Handlers) UpdateMe(w http.ResponseWriter, r *http.Request) { u, ok := UserFromContext(r.Context()) if !ok { httpx.Error(w, http.StatusUnauthorized, "not logged in") return } var req updateProfileReq if err := httpx.ReadJSON(r, &req); err != nil { httpx.Error(w, http.StatusBadRequest, "invalid json") return } if err := h.store.UpdateProfile(u.ID, req.DisplayName, req.Country, req.Bio, req.PrivateNote); err != nil { httpx.Error(w, http.StatusInternalServerError, "update failed") return } httpx.WriteJSON(w, http.StatusOK, map[string]any{"ok": true}) } func (h *Handlers) PublicProfile(w http.ResponseWriter, r *http.Request) { username := r.PathValue("username") u, err := h.store.GetUserByUsername(username) if err != nil { httpx.Error(w, http.StatusNotFound, "no such user") return } httpx.WriteJSON(w, http.StatusOK, publicProfile{ Username: u.Username, DisplayName: u.DisplayName, Country: u.Country, Bio: u.Bio, Rating: u.Rating, Wins: u.Wins, Losses: u.Losses, Draws: u.Draws, }) } func (h *Handlers) Leaderboard(w http.ResponseWriter, r *http.Request) { entries, err := h.store.Leaderboard(500) if err != nil { httpx.Error(w, http.StatusInternalServerError, "leaderboard failed") return } httpx.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "players": entries}) } func (h *Handlers) ExportProfile(w http.ResponseWriter, r *http.Request) { if _, ok := UserFromContext(r.Context()); !ok { httpx.Error(w, http.StatusUnauthorized, "not logged in") return } username := r.PathValue("username") u, err := h.store.GetUserByUsername(username) if err != nil { httpx.Error(w, http.StatusNotFound, "no such user") return } httpx.WriteJSON(w, http.StatusOK, ownProfile{ Username: u.Username, DisplayName: u.DisplayName, Country: u.Country, Bio: u.Bio, PrivateNote: u.PrivateNote, }) }