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