mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-24 15:26:49 +09:00
api/v2: add Tailscale-compatible keys API
Add the v2 foundation — Basic/Bearer auth, scope scaffolding, the tailnet check, and the Tailscale error shape — plus the auth-key endpoints mapped onto Headscale pre-auth keys.
This commit is contained in:
1 parent
2ff342764f
commit
fe08d43978
5 files changed
+862
No files matched your search
@@ -0,0 +1,83 @@
|
||||
package apiv2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// apiError is the Tailscale API error body. The official Tailscale Go client —
|
||||
// and therefore the Terraform provider and tscli built on it — decode 4xx/5xx
|
||||
// responses into this shape. Huma's default RFC 9457 problem+json would reach
|
||||
// them with an empty message, so every v2 error is rewritten into this shape by
|
||||
// tailscaleErrorTransformer. The HTTP status is read from the response code,
|
||||
// not from the body's status field.
|
||||
type apiError struct {
|
||||
Message string `json:"message"`
|
||||
Data []apiErrorData `json:"data,omitempty"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type apiErrorData struct {
|
||||
User string `json:"user,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// tailscaleErrorTransformer rewrites Huma's RFC 9457 error model into the
|
||||
// Tailscale error shape. It is registered on the v2 API config only, so the
|
||||
// headscale-native v1 API keeps emitting problem+json. Non-error bodies pass
|
||||
// through untouched.
|
||||
func tailscaleErrorTransformer(_ huma.Context, _ string, v any) (any, error) {
|
||||
em, ok := v.(*huma.ErrorModel)
|
||||
if !ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
message := em.Detail
|
||||
if message == "" {
|
||||
message = em.Title
|
||||
}
|
||||
|
||||
out := apiError{Message: message, Status: em.Status}
|
||||
|
||||
if len(em.Errors) > 0 {
|
||||
details := make([]string, 0, len(em.Errors))
|
||||
for _, d := range em.Errors {
|
||||
details = append(details, d.Error())
|
||||
}
|
||||
|
||||
out.Data = []apiErrorData{{Errors: details}}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// mapError translates a state/db-layer error into a Huma HTTP error
|
||||
// (not-found→404, invalid input→400, everything else→500). The transformer
|
||||
// then reshapes it into the Tailscale body.
|
||||
func mapError(msg string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound),
|
||||
errors.Is(err, db.ErrPreAuthKeyNotFound),
|
||||
errors.Is(err, state.ErrNodeNotFound):
|
||||
return huma.Error404NotFound(msg, err)
|
||||
|
||||
case errors.Is(err, db.ErrPreAuthKeyNotTaggedOrOwned),
|
||||
errors.Is(err, db.ErrPreAuthKeyACLTagInvalid),
|
||||
errors.Is(err, state.ErrGivenNameInvalid),
|
||||
errors.Is(err, state.ErrGivenNameTaken),
|
||||
errors.Is(err, state.ErrNodeNameNotUnique),
|
||||
errors.Is(err, state.ErrRequestedTagsInvalidOrNotPermitted):
|
||||
return huma.Error400BadRequest(msg, err)
|
||||
|
||||
default:
|
||||
return huma.Error500InternalServerError(msg, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user