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