diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index ea97bd7..79030e1 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -274,6 +274,7 @@ func main() { admin.HandleFunc("/feature-flags", adminHandler.UpdateFeatureFlags).Methods("PUT") // manage identity providers — admin-only admin.HandleFunc("/auth/providers", authHandler.CreateProvider).Methods("POST") + admin.HandleFunc("/auth/providers/{providerId}", authHandler.UpdateProvider).Methods("PUT") admin.HandleFunc("/auth/providers/{providerId}", adminHandler.DeleteProvider).Methods("DELETE") // Serve static files (frontend) for all other routes diff --git a/backend/internal/application/dto/dto.go b/backend/internal/application/dto/dto.go index 481456d..e35877d 100644 --- a/backend/internal/application/dto/dto.go +++ b/backend/internal/application/dto/dto.go @@ -57,6 +57,21 @@ type CreateAuthProviderRequest struct { IsActive bool `json:"is_active"` } +// UpdateAuthProviderRequest represents an OAuth/OIDC provider update request. +// ClientSecret may be empty to keep the existing secret. +type UpdateAuthProviderRequest struct { + Name string `json:"name"` + Type string `json:"type"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + AuthorizationURL string `json:"authorization_url"` + TokenURL string `json:"token_url"` + UserInfoURL string `json:"userinfo_url"` + Scopes []string `json:"scopes"` + IDTokenClaim string `json:"id_token_claim,omitempty"` + IsActive bool `json:"is_active"` +} + // FeatureFlagsDTO represents app-wide feature flags in API responses. type FeatureFlagsDTO struct { RegistrationEnabled bool `json:"registration_enabled"` diff --git a/backend/internal/application/services/auth_service.go b/backend/internal/application/services/auth_service.go index 3695837..59227a2 100644 --- a/backend/internal/application/services/auth_service.go +++ b/backend/internal/application/services/auth_service.go @@ -319,6 +319,57 @@ func (s *AuthService) CreateProvider(ctx context.Context, req *dto.CreateAuthPro return dto.NewAuthProviderDTO(provider), nil } +// UpdateProvider updates an existing OAuth/OIDC provider. +// If ClientSecret is empty, the existing encrypted secret is preserved. +func (s *AuthService) UpdateProvider(ctx context.Context, providerID bson.ObjectID, req *dto.UpdateAuthProviderRequest) (*dto.AuthProviderDTO, error) { + if s.providerRepo == nil || s.encryptor == nil { + return nil, errors.New("provider configuration unavailable") + } + + existing, err := s.providerRepo.GetProviderByID(ctx, providerID) + if err != nil { + return nil, err + } + + providerType := strings.ToLower(strings.TrimSpace(req.Type)) + if providerType != "oidc" && providerType != "oauth2" { + return nil, errors.New("provider type must be oidc or oauth2") + } + + name := strings.TrimSpace(req.Name) + clientID := strings.TrimSpace(req.ClientID) + authorizationURL := strings.TrimSpace(req.AuthorizationURL) + tokenURL := strings.TrimSpace(req.TokenURL) + if name == "" || clientID == "" || authorizationURL == "" || tokenURL == "" { + return nil, errors.New("missing required provider fields") + } + + existing.Name = name + existing.Type = providerType + existing.ClientID = clientID + existing.AuthorizationURL = authorizationURL + existing.TokenURL = tokenURL + existing.UserInfoURL = strings.TrimSpace(req.UserInfoURL) + existing.Scopes = normalizeScopes(req.Scopes, providerType) + existing.IDTokenClaim = strings.TrimSpace(req.IDTokenClaim) + existing.IsActive = req.IsActive + + clientSecret := strings.TrimSpace(req.ClientSecret) + if clientSecret != "" { + encrypted, err := s.encryptor.Encrypt(clientSecret) + if err != nil { + return nil, err + } + existing.ClientSecret = encrypted + } + + if err := s.providerRepo.UpdateProvider(ctx, existing); err != nil { + return nil, err + } + + return dto.NewAuthProviderDTO(existing), nil +} + // BuildProviderAuthorizationURL constructs a provider authorization URL. func (s *AuthService) BuildProviderAuthorizationURL(ctx context.Context, providerID bson.ObjectID, redirectURI, state string) (string, error) { flags, err := s.GetFeatureFlags(ctx) diff --git a/backend/internal/interfaces/handlers/auth_handler.go b/backend/internal/interfaces/handlers/auth_handler.go index 2a20696..ee10922 100644 --- a/backend/internal/interfaces/handlers/auth_handler.go +++ b/backend/internal/interfaces/handlers/auth_handler.go @@ -141,6 +141,30 @@ func (h *AuthHandler) CreateProvider(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(provider) } +// UpdateProvider updates an existing OAuth/OIDC provider configuration. +func (h *AuthHandler) UpdateProvider(w http.ResponseWriter, r *http.Request) { + providerID, err := bson.ObjectIDFromHex(mux.Vars(r)["providerId"]) + if err != nil { + http.Error(w, "Invalid provider ID", http.StatusBadRequest) + return + } + + var req dto.UpdateAuthProviderRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + provider, err := h.authService.UpdateProvider(r.Context(), providerID, &req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(provider) +} + // StartProviderLogin redirects the browser to the selected provider. func (h *AuthHandler) StartProviderLogin(w http.ResponseWriter, r *http.Request) { providerID, err := bson.ObjectIDFromHex(mux.Vars(r)["providerId"]) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a57f819..0fdff60 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -205,7 +205,7 @@ -
+
@@ -965,6 +965,16 @@ const logout = () => { display: block; } +.admin-route-view { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; + width: 100%; + padding: 0; +} + @media (max-width: 768px) { .app-navbar { display: grid; diff --git a/frontend/src/components/AdminProviderModal.vue b/frontend/src/components/AdminProviderModal.vue new file mode 100644 index 0000000..bb57a37 --- /dev/null +++ b/frontend/src/components/AdminProviderModal.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/frontend/src/pages/Admin.vue b/frontend/src/pages/Admin.vue index a53f5ab..1b7c281 100644 --- a/frontend/src/pages/Admin.vue +++ b/frontend/src/pages/Admin.vue @@ -1,9 +1,14 @@