- Updated .env.example to include new configuration options for the UltiAI branding and API endpoints. - Enhanced Nginx configuration to support new API routes for the MCP and WebSocket connections. - Introduced sub-filters for branding adjustments in Nginx responses. - Added new JavaScript patch for API endpoint adjustments. - Implemented tests for new API functionalities and improved error handling in the AI gateway.
165 lines
5.0 KiB
Go
165 lines
5.0 KiB
Go
package users
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
|
|
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
|
|
"github.com/ultisuite/ulti-backend/internal/api/middleware"
|
|
"github.com/ultisuite/ulti-backend/internal/config"
|
|
"github.com/ultisuite/ulti-backend/internal/permission"
|
|
platformusers "github.com/ultisuite/ulti-backend/internal/users"
|
|
"github.com/ultisuite/ulti-backend/internal/orgpolicy"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *pgxpool.Pool
|
|
cfg *config.Config
|
|
logger *slog.Logger
|
|
orgPolicy *orgpolicy.Loader
|
|
}
|
|
|
|
func NewHandler(db *pgxpool.Pool, cfg *config.Config) *Handler {
|
|
return &Handler{
|
|
db: db,
|
|
cfg: cfg,
|
|
orgPolicy: orgpolicy.NewLoader(db, nil),
|
|
logger: slog.Default().With("component", "users-api"),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/me", h.Me)
|
|
r.Put("/me/avatar", h.PutAvatar)
|
|
r.Delete("/me/avatar", h.DeleteAvatar)
|
|
return r
|
|
}
|
|
|
|
func (h *Handler) Me(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
state, err := platformusers.GetAccountState(r.Context(), h.db, claims.Sub)
|
|
if err != nil {
|
|
h.logger.Error("read account state", "error", err)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
role := permission.DeriveAccountRole(state.PlatformAdmin, state.Status)
|
|
|
|
orgAgenda, err := h.orgPolicy.PublicAgendaPolicy(r.Context())
|
|
if err != nil {
|
|
h.logger.Warn("read org agenda policy", "error", err)
|
|
orgAgenda = orgpolicy.PublicAgendaPolicy{
|
|
DefaultThemeMode: "system",
|
|
DefaultVideoProvider: "ultimeet",
|
|
ConfiguredVideoProviders: []string{"ultimeet"},
|
|
}
|
|
}
|
|
|
|
orgDrive, err := h.orgPolicy.PublicDrivePolicy(r.Context())
|
|
if err != nil {
|
|
h.logger.Warn("read org drive policy", "error", err)
|
|
orgDrive = orgpolicy.PublicDrivePolicy{}
|
|
}
|
|
|
|
avatarURL, err := platformusers.GetAvatarURL(r.Context(), h.db, claims.Sub)
|
|
if err != nil {
|
|
h.logger.Warn("read user avatar", "error", err)
|
|
}
|
|
if avatarURL == "" {
|
|
if imported, importErr := platformusers.ImportAvatarFromAuthentik(r.Context(), h.db, h.cfg, claims.Sub); importErr != nil {
|
|
h.logger.Warn("import avatar from authentik", "error", importErr)
|
|
} else if imported != "" {
|
|
avatarURL = imported
|
|
}
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"sub": claims.Sub,
|
|
"email": claims.Email,
|
|
"name": claims.Name,
|
|
"status": state.Status,
|
|
"platform_admin": state.PlatformAdmin,
|
|
"role": role,
|
|
"groups": claims.Groups,
|
|
"org_agenda": orgAgenda,
|
|
"org_drive": orgDrive,
|
|
}
|
|
if avatarURL != "" {
|
|
payload["avatar_url"] = avatarURL
|
|
}
|
|
|
|
apiresponse.WriteJSON(w, http.StatusOK, payload)
|
|
}
|
|
|
|
func (h *Handler) PutAvatar(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
var body struct {
|
|
AvatarURL string `json:"avatar_url"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid json body", nil)
|
|
return
|
|
}
|
|
|
|
if err := platformusers.SetAvatarURL(r.Context(), h.db, claims.Sub, body.AvatarURL); err != nil {
|
|
switch {
|
|
case errors.Is(err, platformusers.ErrAvatarTooLarge):
|
|
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "avatar too large (max 512 KiB)", nil)
|
|
case errors.Is(err, platformusers.ErrAvatarInvalid):
|
|
apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "invalid avatar image", nil)
|
|
case errors.Is(err, pgx.ErrNoRows):
|
|
apivalidate.WriteNotFound(w, r, "user not found")
|
|
default:
|
|
h.logger.Error("set user avatar", "error", err)
|
|
apivalidate.WriteInternal(w, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
platformusers.PropagateAvatarOutbound(r.Context(), h.db, h.cfg, claims, body.AvatarURL)
|
|
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
|
|
"avatar_url": body.AvatarURL,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) DeleteAvatar(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
if err := platformusers.ClearAvatarURL(r.Context(), h.db, claims.Sub); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apivalidate.WriteNotFound(w, r, "user not found")
|
|
return
|
|
}
|
|
h.logger.Error("clear user avatar", "error", err)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
|
|
platformusers.PropagateAvatarOutbound(r.Context(), h.db, h.cfg, claims, "")
|
|
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"ok": true})
|
|
}
|