matrixcloud / api /handlers_auth.go
github-actions[bot]
Deploy from 4dfd54f2
857a91b
Raw
History Blame Contribute Delete
6.45 kB
package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"regexp"
"strings"
"time"
"github.com/agent-matrix/matrix-runtime/internal/store"
)
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
// userJSON is the public shape of a user returned to the console.
func userJSON(u *store.User) map[string]any {
return map[string]any{
"id": u.ID,
"name": u.Name,
"email": u.Email,
"role": u.Role,
"workspace": u.WorkspaceName,
"workspace_slug": u.WorkspaceSlug,
"workspace_id": u.WorkspaceID,
}
}
func (s *Server) requireStore(w http.ResponseWriter) bool {
if s.store == nil {
writeError(w, http.StatusServiceUnavailable, "user store is not available")
return false
}
return true
}
func (s *Server) handleSignup(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
var req struct{ Name, Email, Password, Workspace string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
req.Email = strings.TrimSpace(req.Email)
if !emailRe.MatchString(req.Email) {
writeError(w, http.StatusBadRequest, "enter a valid email address")
return
}
if len(req.Password) < 6 {
writeError(w, http.StatusBadRequest, "password must be at least 6 characters")
return
}
if strings.TrimSpace(req.Name) == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
u, err := s.store.Signup(req.Name, req.Email, req.Password, req.Workspace)
if err != nil {
if errors.Is(err, store.ErrEmailTaken) {
writeError(w, http.StatusConflict, err.Error())
return
}
writeError(w, http.StatusInternalServerError, "could not create account")
return
}
token, err := s.store.CreateSession(u.ID)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not start session")
return
}
s.audit(r, u.WorkspaceID, u.ID, "user.signup", u.Email, "success", nil)
// Best-effort welcome + verification email (never blocks signup success).
if verifyTok, verr := s.store.CreateEmailVerification(u.ID, 24*time.Hour); verr == nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
_ = s.email.SendWelcome(ctx, u.Email, u.Name, verifyTok)
}()
}
writeJSON(w, http.StatusCreated, map[string]any{"token": token, "user": userJSON(u)})
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
var req struct{ Email, Password string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
u, err := s.store.Login(req.Email, req.Password)
if err != nil {
s.audit(r, "", strings.ToLower(strings.TrimSpace(req.Email)), "user.login", req.Email, "failure", nil)
writeError(w, http.StatusUnauthorized, "invalid email or password")
return
}
token, err := s.store.CreateSession(u.ID)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not start session")
return
}
s.audit(r, u.WorkspaceID, u.ID, "user.login", u.Email, "success", nil)
writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": userJSON(u)})
}
func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
u, err := s.store.UserBySession(bearer(r))
if err != nil {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
writeJSON(w, http.StatusOK, map[string]any{"user": userJSON(u)})
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
token := bearer(r)
all := r.URL.Query().Get("all") == "true"
if all {
if u, err := s.store.UserBySession(token); err == nil {
_ = s.store.DeleteUserSessions(u.ID)
}
}
_ = s.store.DeleteSession(token)
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func bearer(r *http.Request) string {
return strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
}
// handleForgotPassword always responds 200 (to avoid leaking which emails are
// registered). When the email matches a user, a reset link is emailed via
// Resend with a single-use, 1-hour token.
func (s *Server) handleForgotPassword(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
var req struct{ Email string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
const ok = "If an account exists for that email, a reset link is on its way."
u, err := s.store.UserByEmail(req.Email)
if err != nil {
// Don't reveal absence; respond identically.
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "message": ok})
return
}
token, err := s.store.CreatePasswordReset(u.ID, time.Hour)
if err != nil {
writeError(w, http.StatusInternalServerError, "could not start password reset")
return
}
if err := s.email.SendPasswordReset(r.Context(), u.Email, token); err != nil {
writeError(w, http.StatusBadGateway, "could not send reset email")
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "message": ok})
}
// handleResetPassword consumes a reset token and sets a new password.
func (s *Server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
var req struct{ Token, Password string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if err := s.store.ResetPassword(strings.TrimSpace(req.Token), req.Password); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "message": "Your password has been updated. Please sign in."})
}
// handleVerifyEmail consumes an email-verification token.
func (s *Server) handleVerifyEmail(w http.ResponseWriter, r *http.Request) {
if !s.requireStore(w) {
return
}
var req struct{ Token string }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if _, err := s.store.VerifyEmail(strings.TrimSpace(req.Token)); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "message": "Email verified."})
}