5f72264b06
- Introduced a new section for managing organizations, including creating organizations and adding users to them. - Added a password reset modal and functionality to request a password reset link. - Updated the settings page to include personal settings for changing passwords and admin settings for configuring registration options. - Enhanced the app detail view with tabs for releases, insights, collaborators, tracks, and settings. - Improved user management with admin capabilities to verify user emails and change user roles. - Updated navigation and UI elements to accommodate new features and improve user experience.
235 lines
6.6 KiB
Go
235 lines
6.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/shorebird-server/internal/api/middleware"
|
|
"github.com/shorebird-server/internal/db"
|
|
)
|
|
|
|
// AdminHandler handles admin/management API endpoints.
|
|
type AdminHandler struct {
|
|
DB db.Store
|
|
}
|
|
|
|
// AddTargetDevice handles POST /api/v1/admin/patches/{patchId}/target-devices
|
|
// Restricts a patch to only be delivered to specific devices (by client_id).
|
|
func (h *AdminHandler) AddTargetDevice(w http.ResponseWriter, r *http.Request) {
|
|
patchID, err := strconv.Atoi(chi.URLParam(r, "patchId"))
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid patch ID", nil)
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
ClientID string `json:"client_id"`
|
|
}
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
if err := h.DB.AddPatchTargetDevice(r.Context(), patchID, req.ClientID); err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to add target device", strPtr(err.Error()))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// RemoveTargetDevice handles DELETE /api/v1/admin/patches/{patchId}/target-devices/{clientId}
|
|
func (h *AdminHandler) RemoveTargetDevice(w http.ResponseWriter, r *http.Request) {
|
|
// For simplicity, target device removal is handled by the database cascade
|
|
// In a full implementation, add a RemovePatchTargetDevice DB method
|
|
respondError(w, http.StatusNotImplemented, "Not yet implemented", nil)
|
|
}
|
|
|
|
// GetTargetDevices handles GET /api/v1/admin/patches/{patchId}/target-devices
|
|
func (h *AdminHandler) GetTargetDevices(w http.ResponseWriter, r *http.Request) {
|
|
patchID, err := strconv.Atoi(chi.URLParam(r, "patchId"))
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid patch ID", nil)
|
|
return
|
|
}
|
|
|
|
devices, err := h.DB.GetPatchTargetDevices(r.Context(), patchID)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to fetch target devices", nil)
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
|
"patch_id": patchID,
|
|
"client_ids": devices,
|
|
})
|
|
}
|
|
|
|
// GetPatchEvents handles GET /api/v1/admin/apps/{appId}/events
|
|
// Returns patch events for analytics.
|
|
func (h *AdminHandler) GetPatchEvents(w http.ResponseWriter, r *http.Request) {
|
|
// This would query patch_events table for the app
|
|
// For now, return a stub
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
|
"events": []map[string]interface{}{},
|
|
})
|
|
}
|
|
|
|
// ListUsers handles GET /api/v1/admin/users (admin convenience endpoint)
|
|
func (h *AdminHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|
if !h.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
|
|
users, err := h.DB.ListAllUsers(r.Context())
|
|
if err != nil {
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
|
"message": "Admin endpoints available",
|
|
"users": []interface{}{},
|
|
})
|
|
return
|
|
}
|
|
|
|
type userEntry struct {
|
|
ID int `json:"id"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
CreatedAt string `json:"created_at"`
|
|
EmailVerified bool `json:"email_verified"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
|
|
result := make([]userEntry, 0, len(users))
|
|
for _, u := range users {
|
|
role := u.Role
|
|
if role == "" {
|
|
role = "member"
|
|
}
|
|
result = append(result, userEntry{
|
|
ID: u.ID,
|
|
Email: u.Email,
|
|
Name: u.Name,
|
|
Role: role,
|
|
CreatedAt: u.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
|
EmailVerified: u.EmailVerified,
|
|
IsAdmin: u.IsAdmin,
|
|
})
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
|
"users": result,
|
|
})
|
|
}
|
|
|
|
func (h *AdminHandler) VerifyUserEmail(w http.ResponseWriter, r *http.Request) {
|
|
if !h.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid user ID", nil)
|
|
return
|
|
}
|
|
if err := h.DB.MarkUserEmailVerified(r.Context(), userID); err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to verify user email", nil)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *AdminHandler) SetUserAdmin(w http.ResponseWriter, r *http.Request) {
|
|
if !h.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
userID, err := strconv.Atoi(chi.URLParam(r, "userId"))
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid user ID", nil)
|
|
return
|
|
}
|
|
var req struct {
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid request body", nil)
|
|
return
|
|
}
|
|
if err := h.DB.SetUserAdmin(r.Context(), userID, req.IsAdmin); err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to update admin status", nil)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *AdminHandler) GetSettings(w http.ResponseWriter, r *http.Request) {
|
|
if !h.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
settings, err := h.DB.ListSettings(r.Context())
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to load settings", nil)
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{"settings": redactedSettings(settings)})
|
|
}
|
|
|
|
func (h *AdminHandler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
|
|
if !h.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
var req map[string]string
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid request body", nil)
|
|
return
|
|
}
|
|
allowed := map[string]bool{
|
|
"registration_enabled": true,
|
|
"sso_registration_enabled": true,
|
|
"sso_only_registration": true,
|
|
"casdoor_endpoint": true,
|
|
"casdoor_client_id": true,
|
|
"casdoor_client_secret": true,
|
|
"casdoor_organization": true,
|
|
}
|
|
for key, value := range req {
|
|
if !allowed[key] {
|
|
continue
|
|
}
|
|
if key == "casdoor_client_secret" && value == "" {
|
|
continue
|
|
}
|
|
if err := h.DB.SetSetting(r.Context(), key, value); err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to update settings", nil)
|
|
return
|
|
}
|
|
}
|
|
h.GetSettings(w, r)
|
|
}
|
|
|
|
func (h *AdminHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
|
claims := middleware.GetClaims(r)
|
|
if claims == nil {
|
|
respondError(w, http.StatusUnauthorized, "Unauthorized", nil)
|
|
return false
|
|
}
|
|
user, err := h.DB.GetUserByID(r.Context(), claims.UserID)
|
|
if err != nil || !user.IsAdmin {
|
|
respondError(w, http.StatusForbidden, "Admin access required", nil)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func redactedSettings(settings map[string]string) map[string]string {
|
|
out := map[string]string{}
|
|
for k, v := range settings {
|
|
if k == "casdoor_client_secret" && v != "" {
|
|
out[k] = "********"
|
|
continue
|
|
}
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|