server/internal/handlers/account.go
Rama main · réplica de lectura del repositorio original.
package handlers
import (
"errors"
"net/http"
"net/mail"
"strings"
"blackbox/server/internal/auth"
"blackbox/server/internal/models"
"gorm.io/gorm"
)
// UpdateAccount handles PATCH /api/auth/me
// Allows a password-based user to update their email address.
// OIDC-linked accounts cannot change their email this way.
func UpdateAccount(database *gorm.DB, jwtSecret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.ClaimsFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
// Check the JWT claim first to short-circuit obvious SSO-managed accounts.
// We re-check persisted OIDC linkage after loading the user below to guard
// against stale or tampered tokens and enforce current server-side state.
if claims.OIDCLinked {
writeError(w, http.StatusForbidden, "email is managed by your SSO provider")
return
}
var req struct {
Email string `json:"email"`
}
if !decodeJSONBody(w, r, maxCredentialBodyBytes, &req) {
return
}
req.Email = strings.TrimSpace(req.Email)
parsedEmail, err := mail.ParseAddress(req.Email)
if req.Email == "" || err != nil || parsedEmail.Address != req.Email {
writeError(w, http.StatusBadRequest, "valid email required")
return
}
var user models.User
if err := database.First(&user, "id = ?", claims.UserID).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to load user")
return
}
// Re-check server-side OIDC state even after the JWT gate above.
if user.OIDCIssuer != "" {
writeError(w, http.StatusForbidden, "email is managed by your SSO provider")
return
}
if req.Email != "" {
var existing models.User
err := database.First(&existing, "email = ? AND id != ?", req.Email, user.ID).Error
if err == nil {
writeError(w, http.StatusConflict, "email already in use")
return
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
writeError(w, http.StatusInternalServerError, "failed to check email")
return
}
}
if err := database.Model(&user).Update("email", req.Email).Error; err != nil {
writeError(w, http.StatusInternalServerError, "failed to update email")
return
}
user.Email = req.Email
token, err := auth.IssueJWT(
user.ID,
user.Username,
user.Email,
false,
user.IsAdmin,
user.TokenVersion,
jwtSecret,
jwtTTL(),
)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to issue token")
return
}
setSessionCookie(w, r, token, jwtTTL())
writeSessionResponse(w, &auth.Claims{
UserID: user.ID,
Username: user.Username,
Email: user.Email,
OIDCLinked: false,
IsAdmin: user.IsAdmin,
TokenVersion: user.TokenVersion,
}, http.StatusOK)
}
}