mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-18 22:34:54 +09:00
hscontrol/api/v1: implement user endpoints
CreateUser, ListUsers (with filters), RenameUser, DeleteUser over the state layer; unknown users now 404. HTTP-parity tests via the generated client.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// This file bridges the existing proto response builders (the Proto() methods
|
||||
// on the state types) to the ogen API types. Reusing Proto() guarantees the
|
||||
// new HTTP API surfaces exactly the same data the gRPC/gateway stack did
|
||||
// (username fallback, masked key prefixes, online computation, the
|
||||
// TaggedDevices substitution, …) without reimplementing it.
|
||||
//
|
||||
// Unlike grpc-gateway (which marshalled with EmitUnpopulated), these converters
|
||||
// omit zero-value and absent fields — empty strings, false booleans, zero
|
||||
// numbers, empty arrays, nil timestamps/objects, and the unspecified register
|
||||
// method. See docs/v1-ogen/CHANGES.md. When the proto stack is removed, these
|
||||
// converters are rewritten to read the state types directly.
|
||||
//
|
||||
// Converters are added here as each resource group is migrated.
|
||||
|
||||
func optString(s string) oas.OptString {
|
||||
if s == "" {
|
||||
return oas.OptString{}
|
||||
}
|
||||
|
||||
return oas.NewOptString(s)
|
||||
}
|
||||
|
||||
func optUint64(v uint64) oas.OptUint64 {
|
||||
if v == 0 {
|
||||
return oas.OptUint64{}
|
||||
}
|
||||
|
||||
return oas.NewOptUint64(v)
|
||||
}
|
||||
|
||||
func optTime(ts *timestamppb.Timestamp) oas.OptDateTime {
|
||||
if ts == nil {
|
||||
return oas.OptDateTime{}
|
||||
}
|
||||
|
||||
return oas.NewOptDateTime(ts.AsTime())
|
||||
}
|
||||
|
||||
func oasUser(u *v1.User) oas.User {
|
||||
return oas.User{
|
||||
ID: optUint64(u.GetId()),
|
||||
Name: optString(u.GetName()),
|
||||
CreatedAt: optTime(u.GetCreatedAt()),
|
||||
DisplayName: optString(u.GetDisplayName()),
|
||||
Email: optString(u.GetEmail()),
|
||||
ProviderId: optString(u.GetProviderId()),
|
||||
Provider: optString(u.GetProvider()),
|
||||
ProfilePicUrl: optString(u.GetProfilePicUrl()),
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/db"
|
||||
"github.com/juanfont/headscale/hscontrol/state"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -54,7 +55,8 @@ func internalError(detail string) *oas.ErrorStatusCode {
|
||||
func mapStateError(err error) *oas.ErrorStatusCode {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound),
|
||||
errors.Is(err, state.ErrNodeNotFound):
|
||||
errors.Is(err, state.ErrNodeNotFound),
|
||||
errors.Is(err, db.ErrUserNotFound):
|
||||
return notFound(err.Error())
|
||||
case errors.Is(err, types.ErrPolicyUpdateIsDisabled):
|
||||
return badRequest(err.Error())
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
oas "github.com/juanfont/headscale/gen/api/v1"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateUser creates a user and distributes the resulting policy change.
|
||||
func (s *Server) CreateUser(
|
||||
_ context.Context,
|
||||
req *oas.CreateUserReq,
|
||||
) (*oas.CreateUserOK, error) {
|
||||
newUser := types.User{
|
||||
Name: req.Name.Or(""),
|
||||
DisplayName: req.DisplayName.Or(""),
|
||||
Email: req.Email.Or(""),
|
||||
ProfilePicURL: req.PictureUrl.Or(""),
|
||||
}
|
||||
|
||||
user, policyChanged, err := s.state.CreateUser(newUser)
|
||||
if err != nil {
|
||||
return nil, internalError("creating user: " + err.Error())
|
||||
}
|
||||
|
||||
s.change(policyChanged)
|
||||
|
||||
return &oas.CreateUserOK{User: oas.NewOptUser(oasUser(user.Proto()))}, nil
|
||||
}
|
||||
|
||||
// ListUsers lists users, optionally filtered by id, name, or email, sorted by id.
|
||||
func (s *Server) ListUsers(
|
||||
_ context.Context,
|
||||
params oas.ListUsersParams,
|
||||
) (*oas.ListUsersOK, error) {
|
||||
var (
|
||||
users []types.User
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case params.Name.Or("") != "":
|
||||
users, err = s.state.ListUsersWithFilter(&types.User{Name: params.Name.Or("")})
|
||||
case params.Email.Or("") != "":
|
||||
users, err = s.state.ListUsersWithFilter(&types.User{Email: params.Email.Or("")})
|
||||
case params.ID.Or(0) != 0:
|
||||
users, err = s.state.ListUsersWithFilter(
|
||||
&types.User{Model: gorm.Model{ID: uint(params.ID.Or(0))}},
|
||||
)
|
||||
default:
|
||||
users, err = s.state.ListAllUsers()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
slices.SortFunc(users, func(a, b types.User) int { return cmp.Compare(a.ID, b.ID) })
|
||||
|
||||
out := make([]oas.User, len(users))
|
||||
for i := range users {
|
||||
out[i] = oasUser(users[i].Proto())
|
||||
}
|
||||
|
||||
return &oas.ListUsersOK{Users: out}, nil
|
||||
}
|
||||
|
||||
// RenameUser renames a user and distributes the resulting policy change.
|
||||
func (s *Server) RenameUser(
|
||||
_ context.Context,
|
||||
params oas.RenameUserParams,
|
||||
) (*oas.RenameUserOK, error) {
|
||||
oldUser, err := s.state.GetUserByID(types.UserID(params.OldID))
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
_, c, err := s.state.RenameUser(types.UserID(oldUser.ID), params.NewName)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(c)
|
||||
|
||||
newUser, err := s.state.GetUserByName(params.NewName)
|
||||
if err != nil {
|
||||
return nil, mapStateError(err)
|
||||
}
|
||||
|
||||
return &oas.RenameUserOK{User: oas.NewOptUser(oasUser(newUser.Proto()))}, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user and distributes the resulting policy change.
|
||||
func (s *Server) DeleteUser(_ context.Context, params oas.DeleteUserParams) error {
|
||||
user, err := s.state.GetUserByID(types.UserID(params.ID))
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
policyChanged, err := s.state.DeleteUser(types.UserID(user.ID))
|
||||
if err != nil {
|
||||
return mapStateError(err)
|
||||
}
|
||||
|
||||
s.change(policyChanged)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user