Files
Kristoffer Dalby c9bbb5bdec api/v1: add code-first Huma implementation of the v1 API
Reimplement the v1 API as a code-first Huma service in hscontrol/api/v1,
a thin adapter over the state layer. Huma emits the OpenAPI 3.1 document
(openapi/v1/headscale.yaml) from the Go operation and type definitions;
cmd/gen-openapi writes it and the 3.0.3 downgrade the client is generated
from. Responses reproduce the protojson wire contract (string-encoded
64-bit IDs, all fields emitted, RFC3339/null timestamps); errors map to
the correct HTTP status (404/400/409, 500 for server faults) via mapError.

Serve it on the chi router at /api/v1 behind the existing API-key
middleware, and point /swagger at the emitted spec.
2026-06-19 15:21:00 +02:00

164 lines
4.0 KiB
Go

package apiv1
import (
"context"
"errors"
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
)
func init() {
registrations = append(registrations, registerAuth)
}
// errAuthRejected is the verdict handed to the waiting registration flow when
// an auth session is rejected.
var errAuthRejected = errors.New("auth request rejected")
// AuthRegisterRequestBody is the v1.AuthRegisterRequest body.
type AuthRegisterRequestBody struct {
User string `json:"user,omitempty"`
AuthID string `json:"authId,omitempty"`
}
// AuthApproveRequestBody is the v1.AuthApproveRequest body.
type AuthApproveRequestBody struct {
AuthID string `json:"authId,omitempty"`
}
// AuthRejectRequestBody is the v1.AuthRejectRequest body.
type AuthRejectRequestBody struct {
AuthID string `json:"authId,omitempty"`
}
type (
authRegisterInput struct {
Body AuthRegisterRequestBody
}
authRegisterOutput struct {
Body struct {
Node Node `json:"node"`
}
}
)
type (
authApproveInput struct {
Body AuthApproveRequestBody
}
authApproveOutput struct {
Body struct{}
}
)
type (
authRejectInput struct {
Body AuthRejectRequestBody
}
authRejectOutput struct {
Body struct{}
}
)
func registerAuth(api huma.API, b Backend) {
huma.Register(api, huma.Operation{
OperationID: "authRegister",
Method: http.MethodPost,
Path: "/api/v1/auth/register",
Summary: "Register node via auth flow",
Tags: []string{"Auth"},
Security: bearerAuth,
}, func(ctx context.Context, in *authRegisterInput) (*authRegisterOutput, error) {
// Malformed auth_id is 400; unknown user and missing pending session are
// 404 via mapError, matching the Approve/Reject handlers.
registrationID, err := types.AuthIDFromString(in.Body.AuthID)
if err != nil {
return nil, huma.Error400BadRequest("registering node", err)
}
user, err := b.State.GetUserByName(in.Body.User)
if err != nil {
return nil, mapError("looking up user", err)
}
node, nodeChange, err := b.State.HandleNodeFromAuthPath(
registrationID,
types.UserID(user.ID),
nil,
util.RegisterMethodCLI,
)
if err != nil {
return nil, mapError("registering node", err)
}
routeChange, err := b.State.AutoApproveRoutes(node)
if err != nil {
return nil, huma.Error500InternalServerError("auto approving routes", err)
}
b.Change(nodeChange, routeChange)
out := &authRegisterOutput{}
out.Body.Node = nodeFromView(node)
return out, nil
})
huma.Register(api, huma.Operation{
OperationID: "authApprove",
Method: http.MethodPost,
Path: "/api/v1/auth/approve",
Summary: "Approve a pending auth session",
Tags: []string{"Auth"},
Security: bearerAuth,
}, func(ctx context.Context, in *authApproveInput) (*authApproveOutput, error) {
authReq, err := pendingAuthRequest(b, in.Body.AuthID)
if err != nil {
return nil, err
}
authReq.FinishAuth(types.AuthVerdict{})
return &authApproveOutput{}, nil
})
huma.Register(api, huma.Operation{
OperationID: "authReject",
Method: http.MethodPost,
Path: "/api/v1/auth/reject",
Summary: "Reject a pending auth session",
Tags: []string{"Auth"},
Security: bearerAuth,
}, func(ctx context.Context, in *authRejectInput) (*authRejectOutput, error) {
authReq, err := pendingAuthRequest(b, in.Body.AuthID)
if err != nil {
return nil, err
}
authReq.FinishAuth(types.AuthVerdict{
Err: errAuthRejected,
})
return &authRejectOutput{}, nil
})
}
// pendingAuthRequest looks up the pending session for auth_id. Malformed
// auth_id is 400, unknown is 404.
func pendingAuthRequest(b Backend, rawID string) (*types.AuthRequest, error) {
authID, err := types.AuthIDFromString(rawID)
if err != nil {
return nil, huma.Error400BadRequest("invalid auth_id", err)
}
authReq, ok := b.State.GetAuthCacheEntry(authID)
if !ok {
return nil, huma.Error404NotFound("no pending auth session for auth_id " + authID.String())
}
return authReq, nil
}