217 lines
6.0 KiB
Go
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,
|
|
})
|
|
}
|