Files
ad_fun/chessstars/internal/auth/handlers.go
T
2026-07-15 13:29:08 +03:00

217 lines
6.0 KiB
Go

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,
})
}