Files

170 lines
4.8 KiB
Go
Raw Permalink Normal View History

package api
import (
"errors"
"net/http"
"regexp"
"time"
"messenger/internal/auth"
"messenger/internal/store"
)
var usernameRe = regexp.MustCompile(`^[a-z0-9_]{3,32}$`)
type userJSON struct {
ID int64 `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Avatar string `json:"avatar"`
CreatedAt time.Time `json:"created_at"`
}
func toUserJSON(u *store.User) userJSON {
return userJSON{
ID: u.ID,
Username: u.Username,
DisplayName: u.DisplayName,
Avatar: u.Avatar,
CreatedAt: u.CreatedAt,
}
}
type credentialsRequest struct {
Username string `json:"username"`
DisplayName string `json:"display_name"`
Password string `json:"password"`
}
func (h *apiHandler) register(w http.ResponseWriter, r *http.Request) {
var req credentialsRequest
if !decodeJSON(w, r, &req) {
return
}
if !usernameRe.MatchString(req.Username) {
writeError(w, http.StatusBadRequest, "username must be 3-32 chars of [a-z0-9_]")
return
}
if req.DisplayName == "" || len(req.DisplayName) > 64 {
writeError(w, http.StatusBadRequest, "display_name must be 1-64 chars")
return
}
2026-09-28 13:11:34 +03:00
if len(req.Password) == 0 || len(req.Password) > 128 {
writeError(w, http.StatusBadRequest, "password must be 1-128 chars")
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
user, err := h.store.CreateUser(r.Context(), req.Username, req.DisplayName, hash)
if err != nil {
if errors.Is(err, store.ErrDuplicate) {
writeError(w, http.StatusConflict, "username already taken")
return
}
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
h.issueSession(w, r, user.ID)
writeJSON(w, http.StatusCreated, toUserJSON(user))
}
func (h *apiHandler) login(w http.ResponseWriter, r *http.Request) {
var req credentialsRequest
if !decodeJSON(w, r, &req) {
return
}
user, err := h.store.GetUserByUsername(r.Context(), req.Username)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
writeError(w, http.StatusUnauthorized, "invalid credentials")
return
}
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
ok, err := auth.VerifyPassword(user.PasswordHash, req.Password)
if err != nil || !ok {
writeError(w, http.StatusUnauthorized, "invalid credentials")
return
}
h.issueSession(w, r, user.ID)
writeJSON(w, http.StatusOK, toUserJSON(user))
}
func (h *apiHandler) refresh(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(refreshCookieName)
if err != nil {
writeError(w, http.StatusUnauthorized, "missing refresh token")
return
}
session, err := h.store.GetSessionByRefreshHash(r.Context(), auth.HashRefreshToken(cookie.Value))
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid refresh token")
return
}
if session.RevokedAt != nil {
writeError(w, http.StatusUnauthorized, "session revoked")
return
}
if time.Now().After(session.ExpiresAt) {
writeError(w, http.StatusUnauthorized, "session expired")
return
}
// Rotation: revoke the used session and issue a fresh pair.
if err := h.store.RevokeSession(r.Context(), session.ID); err != nil {
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
h.issueSession(w, r, session.UserID)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *apiHandler) logout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(refreshCookieName); err == nil {
session, err := h.store.GetSessionByRefreshHash(r.Context(), auth.HashRefreshToken(cookie.Value))
if err == nil {
_ = h.store.RevokeSession(r.Context(), session.ID)
}
}
clearAuthCookies(w)
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *apiHandler) me(w http.ResponseWriter, r *http.Request) {
userID, ok := userIDFrom(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthenticated")
return
}
user, err := h.store.GetUserByID(r.Context(), userID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
writeJSON(w, http.StatusOK, toUserJSON(user))
}
func (h *apiHandler) issueSession(w http.ResponseWriter, r *http.Request, userID int64) {
access, err := h.tokens.IssueAccessToken(userID, accessTTL)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
rawRefresh, refreshHash, err := auth.GenerateRefreshToken()
if err != nil {
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
_, err = h.store.CreateSession(r.Context(), userID, refreshHash, r.UserAgent(), r.RemoteAddr, time.Now().Add(refreshTTL))
if err != nil {
writeError(w, http.StatusInternalServerError, "internal server error")
return
}
setAuthCookies(w, access, rawRefresh, h.secure)
}