api-admin: domain controller delegates to AdminDomainUsecase

Add an AdminDomainUsecase interface for the cross-user operations the
admin API needs (list every domain, fetch any by ID or by FQDN+owner, raw
create/update, clear all) and implement it on domain.Service. Refactor
the admin DomainController to depend on DomainUsecase + AdminDomainUsecase
only, dropping its direct domain.DomainStorage handle. Per-user clears now
go through DomainUsecase.DeleteDomain, which already encapsulates the
deletion side effects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-05-03 21:57:15 +08:00
commit 55c4237f6e
7 changed files with 112 additions and 30 deletions

View file

@ -31,28 +31,27 @@ import (
"git.happydns.org/happyDomain/internal/api/controller"
"git.happydns.org/happyDomain/internal/api/middleware"
"git.happydns.org/happyDomain/internal/usecase/domain"
"git.happydns.org/happyDomain/model"
happydns "git.happydns.org/happyDomain/model"
)
type DomainController struct {
domainService happydns.DomainUsecase
adminService happydns.AdminDomainUsecase
remoteZoneImporter happydns.RemoteZoneImporterUsecase
zoneImporter happydns.ZoneImporterUsecase
store domain.DomainStorage
}
func NewDomainController(
duService happydns.DomainUsecase,
adminService happydns.AdminDomainUsecase,
remoteZoneImporter happydns.RemoteZoneImporterUsecase,
zoneImporter happydns.ZoneImporterUsecase,
store domain.DomainStorage,
) *DomainController {
return &DomainController{
duService,
remoteZoneImporter,
zoneImporter,
store,
domainService: duService,
adminService: adminService,
remoteZoneImporter: remoteZoneImporter,
zoneImporter: zoneImporter,
}
}
@ -79,17 +78,11 @@ func (dc *DomainController) ListDomains(c *gin.Context) {
return
}
iter, err := dc.store.ListAllDomains()
domains, err := dc.adminService.ListAllDomains()
if err != nil {
middleware.ErrorResponse(c, http.StatusInternalServerError, fmt.Errorf("unable to retrieve domains list: %w", err))
return
}
defer iter.Close()
var domains []*happydns.Domain
for iter.Next() {
domains = append(domains, iter.Item())
}
happydns.ApiResponse(c, domains, nil)
}
@ -123,7 +116,7 @@ func (dc *DomainController) NewDomain(c *gin.Context) {
ud.Id = nil
ud.Owner = user.Id
happydns.ApiResponse(c, ud, dc.store.CreateDomain(ud))
happydns.ApiResponse(c, ud, dc.adminService.AdminCreateDomain(ud))
}
// DeleteDomain removes a domain from the system by identifier or domain name.
@ -156,7 +149,7 @@ func (dc *DomainController) DeleteDomain(c *gin.Context) {
})
}
domains, err := dc.store.GetDomainByDN(user, c.Param("domain"))
domains, err := dc.adminService.GetDomainsByFQDN(user, c.Param("domain"))
if err != nil {
middleware.ErrorResponse(c, http.StatusNotFound, err)
return
@ -170,19 +163,17 @@ func (dc *DomainController) DeleteDomain(c *gin.Context) {
domainid = domains[0].Id
}
happydns.ApiResponse(c, true, dc.store.DeleteDomain(domainid))
happydns.ApiResponse(c, true, dc.domainService.DeleteDomain(domainid))
}
func (dc *DomainController) searchUserDomain(filter func(*happydns.Domain) bool) *happydns.User {
iter, err := dc.store.ListAllDomains()
domains, err := dc.adminService.ListAllDomains()
if err != nil {
log.Println("Unable to retrieve domains list:", err.Error())
return nil
}
defer iter.Close()
for iter.Next() {
domain := iter.Item()
for _, domain := range domains {
if filter(domain) {
// Create a fake minimal user, as only the Id is required to perform further actions on database
return &happydns.User{Id: domain.Owner}
@ -223,7 +214,7 @@ func (dc *DomainController) GetDomain(c *gin.Context) {
})
}
domain, err := dc.store.GetDomainByDN(user, c.Param("domain"))
domain, err := dc.adminService.GetDomainsByFQDN(user, c.Param("domain"))
happydns.ApiResponse(c, domain, err)
} else {
var user *happydns.User
@ -236,7 +227,7 @@ func (dc *DomainController) GetDomain(c *gin.Context) {
})
}
domain, err := dc.store.GetDomain(domainid)
domain, err := dc.adminService.GetDomainByID(domainid)
if err != nil {
happydns.ApiResponse(c, nil, err)
return
@ -281,7 +272,7 @@ func (dc *DomainController) UpdateDomain(c *gin.Context) {
}
ud.Id = domain.Id
happydns.ApiResponse(c, ud, dc.store.UpdateDomain(ud))
happydns.ApiResponse(c, ud, dc.adminService.AdminUpdateDomain(ud))
}
// ClearDomains removes all domains from the system or all domains belonging to a specific user.
@ -309,8 +300,7 @@ func (dc *DomainController) ClearDomains(c *gin.Context) {
}
for _, dn := range domains {
e := dc.store.DeleteDomain(dn.Id)
if e != nil {
if e := dc.domainService.DeleteDomain(dn.Id); e != nil {
err = errors.Join(err, e)
}
}
@ -324,7 +314,7 @@ func (dc *DomainController) ClearDomains(c *gin.Context) {
return
}
happydns.ApiResponse(c, true, dc.store.ClearDomains())
happydns.ApiResponse(c, true, dc.adminService.ClearDomains())
}
// UpdateZones updates the zone history for a specific domain.
@ -355,5 +345,5 @@ func (dc *DomainController) UpdateZones(c *gin.Context) {
return
}
happydns.ApiResponse(c, domain, dc.store.UpdateDomain(domain))
happydns.ApiResponse(c, domain, dc.adminService.AdminUpdateDomain(domain))
}

View file

@ -32,9 +32,9 @@ import (
func declareDomainRoutes(router *gin.RouterGroup, dep Dependencies, store storage.Storage) {
dc := controller.NewDomainController(
dep.Domain,
dep.AdminDomain,
dep.RemoteZoneImporter,
dep.ZoneImporter,
store,
)
router.GET("/domains", dc.ListDomains)

View file

@ -33,6 +33,7 @@ import (
// Dependencies holds all use cases required to register the admin API routes.
type Dependencies struct {
AdminAuthUser happydns.AdminAuthUserUsecase
AdminDomain happydns.AdminDomainUsecase
AdminProvider happydns.AdminProviderUsecase
AdminSession happydns.AdminSessionUsecase
AdminUser happydns.AdminUserUsecase

View file

@ -72,6 +72,7 @@ func NewAdmin(app *App) *Admin {
app.store,
admin.Dependencies{
AdminAuthUser: app.usecases.authUserAdmin,
AdminDomain: app.usecases.domainAdmin,
AdminProvider: providerAdminService,
AdminSession: sessionUC.NewService(app.store),
AdminUser: app.usecases.userAdmin,

View file

@ -63,6 +63,7 @@ type Usecases struct {
authUser happydns.AuthUserUsecase
authUserAdmin happydns.AdminAuthUserUsecase
domain happydns.DomainUsecase
domainAdmin happydns.AdminDomainUsecase
domainInfo happydns.DomainInfoUsecase
domainLog happydns.DomainLogUsecase
emailAutoconfig happydns.EmailAutoconfigUsecase
@ -261,6 +262,7 @@ func (app *App) initUsecases() {
domainLogService,
)
app.usecases.domain = domainService
app.usecases.domainAdmin = domainService
app.usecases.zoneService = zoneServiceUC.NewZoneServiceUsecases(
domainService,
zoneService.CreateZoneUC,

View file

@ -0,0 +1,75 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package domain
import (
happydns "git.happydns.org/happyDomain/model"
)
// ListAllDomains returns every domain in the system. Intended for
// administrative callers; iterator drainage is hidden from the caller.
func (s *Service) ListAllDomains() ([]*happydns.Domain, error) {
iter, err := s.store.ListAllDomains()
if err != nil {
return nil, err
}
defer iter.Close()
var domains []*happydns.Domain
for iter.Next() {
domains = append(domains, iter.Item())
}
return domains, iter.Err()
}
// GetDomainByID retrieves a domain by its identifier without ownership
// verification. Intended for administrative callers.
func (s *Service) GetDomainByID(domainID happydns.Identifier) (*happydns.Domain, error) {
return s.store.GetDomain(domainID)
}
// GetDomainsByFQDN looks up domains owned by user that match the given
// fully-qualified domain name. Intended for administrative callers that
// have already resolved the owner.
func (s *Service) GetDomainsByFQDN(user *happydns.User, fqdn string) ([]*happydns.Domain, error) {
return s.store.GetDomainByDN(user, fqdn)
}
// AdminCreateDomain persists domain as-is, bypassing the registration-time
// validations performed by CreateDomain.
func (s *Service) AdminCreateDomain(domain *happydns.Domain) error {
return s.store.CreateDomain(domain)
}
// AdminUpdateDomain persists changes to domain. It is the write-side
// counterpart used by admin endpoints that mutate a Domain in memory and
// need to commit the result without going through the user-scoped
// fetch-mutate-save cycle of UpdateDomain.
func (s *Service) AdminUpdateDomain(domain *happydns.Domain) error {
return s.store.UpdateDomain(domain)
}
// ClearDomains removes every domain from the database. Intended for
// administrative callers performing a full reset.
func (s *Service) ClearDomains() error {
return s.store.ClearDomains()
}

View file

@ -130,3 +130,16 @@ type DomainUsecase interface {
ListUserDomains(*User) ([]*Domain, error)
UpdateDomain(Identifier, *User, func(*Domain)) error
}
// AdminDomainUsecase exposes administrative domain operations that bypass
// the caller-scoped checks of DomainUsecase. Admin callers can list every
// domain, fetch any domain by ID or by FQDN+owner, create or replace one
// from a raw Domain, and wipe the table.
type AdminDomainUsecase interface {
ListAllDomains() ([]*Domain, error)
GetDomainByID(Identifier) (*Domain, error)
GetDomainsByFQDN(*User, string) ([]*Domain, error)
AdminCreateDomain(*Domain) error
AdminUpdateDomain(*Domain) error
ClearDomains() error
}