@@ -4,6 +4,7 @@ package admin
44import (
55 "context"
66 "errors"
7+ "log/slog"
78 "net/http"
89 "net/url"
910 "slices"
@@ -15,6 +16,7 @@ import (
1516
1617 "gomodel/internal/aliases"
1718 "gomodel/internal/auditlog"
19+ "gomodel/internal/authkeys"
1820 "gomodel/internal/core"
1921 "gomodel/internal/executionplans"
2022 "gomodel/internal/guardrails"
@@ -27,6 +29,7 @@ type Handler struct {
2729 usageReader usage.UsageReader
2830 auditReader auditlog.Reader
2931 registry * providers.ModelRegistry
32+ authKeys * authkeys.Service
3033 aliases * aliases.Service
3134 plans * executionplans.Service
3235 guardrails * guardrails.Registry
@@ -69,6 +72,13 @@ func WithAliases(service *aliases.Service) Option {
6972 }
7073}
7174
75+ // WithAuthKeys enables managed auth key administration endpoints.
76+ func WithAuthKeys (service * authkeys.Service ) Option {
77+ return func (h * Handler ) {
78+ h .authKeys = service
79+ }
80+ }
81+
7282// WithExecutionPlans enables execution-plan administration endpoints.
7383func WithExecutionPlans (service * executionplans.Service ) Option {
7484 return func (h * Handler ) {
@@ -613,6 +623,12 @@ type createExecutionPlanRequest struct {
613623 Payload executionplans.Payload `json:"plan_payload"`
614624}
615625
626+ type createAuthKeyRequest struct {
627+ Name string `json:"name"`
628+ Description string `json:"description,omitempty"`
629+ ExpiresAt * time.Time `json:"expires_at,omitempty"`
630+ }
631+
616632func featureUnavailableError (message string ) error {
617633 return core .NewInvalidRequestErrorWithStatus (http .StatusServiceUnavailable , message , nil ).
618634 WithCode ("feature_unavailable" )
@@ -622,6 +638,10 @@ func (h *Handler) aliasesUnavailableError() error {
622638 return featureUnavailableError ("aliases feature is unavailable" )
623639}
624640
641+ func (h * Handler ) authKeysUnavailableError () error {
642+ return featureUnavailableError ("auth keys feature is unavailable" )
643+ }
644+
625645func (h * Handler ) executionPlansUnavailableError () error {
626646 return featureUnavailableError ("execution plans feature is unavailable" )
627647}
@@ -646,6 +666,98 @@ func executionPlanWriteError(err error) error {
646666 return err
647667}
648668
669+ func authKeyWriteError (err error ) error {
670+ if err == nil {
671+ return nil
672+ }
673+ if authkeys .IsValidationError (err ) {
674+ return core .NewInvalidRequestError (err .Error (), err )
675+ }
676+ return err
677+ }
678+
679+ func deactivateByID (
680+ c * echo.Context ,
681+ unavailableErr error ,
682+ idLabel string ,
683+ notFoundErr error ,
684+ notFoundMessage string ,
685+ deactivate func (context.Context , string ) error ,
686+ writeError func (error ) error ,
687+ ) error {
688+ if unavailableErr != nil {
689+ return handleError (c , unavailableErr )
690+ }
691+
692+ id := strings .TrimSpace (c .Param ("id" ))
693+ if id == "" {
694+ return handleError (c , core .NewInvalidRequestError (idLabel + " id is required" , nil ))
695+ }
696+
697+ if err := deactivate (c .Request ().Context (), id ); err != nil {
698+ if errors .Is (err , notFoundErr ) {
699+ return handleError (c , core .NewNotFoundError (notFoundMessage + id ))
700+ }
701+ return handleError (c , writeError (err ))
702+ }
703+ return c .NoContent (http .StatusNoContent )
704+ }
705+
706+ // ListAuthKeys handles GET /admin/api/v1/auth-keys
707+ func (h * Handler ) ListAuthKeys (c * echo.Context ) error {
708+ if h .authKeys == nil {
709+ return handleError (c , h .authKeysUnavailableError ())
710+ }
711+ views := h .authKeys .ListViews ()
712+ if views == nil {
713+ views = []authkeys.View {}
714+ }
715+ return c .JSON (http .StatusOK , views )
716+ }
717+
718+ // CreateAuthKey handles POST /admin/api/v1/auth-keys
719+ func (h * Handler ) CreateAuthKey (c * echo.Context ) error {
720+ if h .authKeys == nil {
721+ return handleError (c , h .authKeysUnavailableError ())
722+ }
723+
724+ var req createAuthKeyRequest
725+ if err := c .Bind (& req ); err != nil {
726+ return handleError (c , core .NewInvalidRequestError ("invalid request body: " + err .Error (), err ))
727+ }
728+
729+ issued , err := h .authKeys .Create (c .Request ().Context (), authkeys.CreateInput {
730+ Name : req .Name ,
731+ Description : req .Description ,
732+ ExpiresAt : req .ExpiresAt ,
733+ })
734+ if err != nil {
735+ return handleError (c , authKeyWriteError (err ))
736+ }
737+ if issued == nil {
738+ requestID := strings .TrimSpace (core .GetRequestID (c .Request ().Context ()))
739+ slog .Error ("auth key service returned nil issued key" , "request_id" , requestID , "path" , c .Request ().URL .Path )
740+ return c .JSON (http .StatusInternalServerError , (& core.GatewayError {
741+ Type : core .ErrorType ("internal_error" ),
742+ Message : "auth key creation failed unexpectedly" ,
743+ StatusCode : http .StatusInternalServerError ,
744+ }).WithCode ("auth_key_issue_failed" ).ToJSON ())
745+ }
746+ return c .JSON (http .StatusCreated , issued )
747+ }
748+
749+ // DeactivateAuthKey handles POST /admin/api/v1/auth-keys/:id/deactivate
750+ func (h * Handler ) DeactivateAuthKey (c * echo.Context ) error {
751+ var unavailableErr error
752+ var deactivate func (context.Context , string ) error
753+ if h .authKeys == nil {
754+ unavailableErr = h .authKeysUnavailableError ()
755+ } else {
756+ deactivate = h .authKeys .Deactivate
757+ }
758+ return deactivateByID (c , unavailableErr , "auth key" , authkeys .ErrNotFound , "auth key not found: " , deactivate , authKeyWriteError )
759+ }
760+
649761// ListAliases handles GET /admin/api/v1/aliases
650762func (h * Handler ) ListAliases (c * echo.Context ) error {
651763 if h .aliases == nil {
@@ -806,22 +918,14 @@ func (h *Handler) CreateExecutionPlan(c *echo.Context) error {
806918
807919// DeactivateExecutionPlan handles POST /admin/api/v1/execution-plans/:id/deactivate
808920func (h * Handler ) DeactivateExecutionPlan (c * echo.Context ) error {
921+ var unavailableErr error
922+ var deactivate func (context.Context , string ) error
809923 if h .plans == nil {
810- return handleError (c , h .executionPlansUnavailableError ())
811- }
812-
813- id := strings .TrimSpace (c .Param ("id" ))
814- if id == "" {
815- return handleError (c , core .NewInvalidRequestError ("execution plan id is required" , nil ))
816- }
817-
818- if err := h .plans .Deactivate (c .Request ().Context (), id ); err != nil {
819- if errors .Is (err , executionplans .ErrNotFound ) {
820- return handleError (c , core .NewNotFoundError ("workflow not found: " + id ))
821- }
822- return handleError (c , executionPlanWriteError (err ))
924+ unavailableErr = h .executionPlansUnavailableError ()
925+ } else {
926+ deactivate = h .plans .Deactivate
823927 }
824- return c . NoContent ( http . StatusNoContent )
928+ return deactivateByID ( c , unavailableErr , "execution plan" , executionplans . ErrNotFound , "workflow not found: " , deactivate , executionPlanWriteError )
825929}
826930
827931func (h * Handler ) validateExecutionPlanGuardrails (payload executionplans.Payload ) error {
0 commit comments