mirror of
https://github.com/holos-run/holos.git
synced 2026-03-20 09:15:02 +00:00
This patch refactors the API to be resource-oriented around one service per resource type. PlatformService, OrganizationService, UserService, etc... Validation is improved to use CEL rules provided by [protovalidate][1]. Place holders for FieldMask and other best practices are added, but are unimplemented as per [API Best Practices][2]. The intent is to set us up well for copying and pasting solid existing examples as we add features. With this patch the server and web app client are both updated to use the refactored API, however the following are not working: 1. Update the model. 2. Field Masks. [1]: https://buf.build/bufbuild/protovalidate [2]: https://protobuf.dev/programming-guides/api/
149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/holos-run/holos/internal/ent"
|
|
"github.com/holos-run/holos/internal/ent/user"
|
|
"github.com/holos-run/holos/internal/errors"
|
|
"github.com/holos-run/holos/internal/logger"
|
|
"github.com/holos-run/holos/internal/server/middleware/authn"
|
|
object "github.com/holos-run/holos/service/gen/holos/object/v1alpha1"
|
|
holos "github.com/holos-run/holos/service/gen/holos/organization/v1alpha1"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
// NewOrganizationHandler returns a new OrganizationService implementation.
|
|
func NewOrganizationHandler(db *ent.Client) *OrganizationHandler {
|
|
return &OrganizationHandler{db: db}
|
|
}
|
|
|
|
// OrganizationHandler implements the OrganizationService interface.
|
|
type OrganizationHandler struct {
|
|
db *ent.Client
|
|
}
|
|
|
|
func (h *OrganizationHandler) ListOrganizations(ctx context.Context, req *connect.Request[holos.ListOrganizationsRequest]) (*connect.Response[holos.ListOrganizationsResponse], error) {
|
|
authnID, err := authn.FromContext(ctx)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
|
|
}
|
|
|
|
dbUser, err := h.db.User.Query().
|
|
Where(
|
|
user.Iss(authnID.Issuer()),
|
|
user.Sub(authnID.Subject()),
|
|
).
|
|
WithOrganizations().
|
|
Only(ctx)
|
|
if err != nil {
|
|
if ent.MaskNotFound(err) == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
|
|
} else {
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
|
|
}
|
|
}
|
|
|
|
rpcOrgs := make([]*holos.Organization, 0, len(dbUser.Edges.Organizations))
|
|
for _, dbOrg := range dbUser.Edges.Organizations {
|
|
rpcOrgs = append(rpcOrgs, OrganizationToRPC(dbOrg))
|
|
}
|
|
|
|
// JEFFTODO: FieldMask
|
|
res := connect.NewResponse(&holos.ListOrganizationsResponse{
|
|
User: &object.UserRef{
|
|
User: &object.UserRef_UserId{
|
|
UserId: dbUser.ID.String(),
|
|
},
|
|
},
|
|
Organizations: rpcOrgs,
|
|
})
|
|
return res, nil
|
|
}
|
|
|
|
func (h *OrganizationHandler) CreateOrganization(
|
|
ctx context.Context,
|
|
req *connect.Request[holos.CreateOrganizationRequest],
|
|
) (*connect.Response[holos.CreateOrganizationResponse], error) {
|
|
log := logger.FromContext(ctx)
|
|
authnID, err := authn.FromContext(ctx)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodePermissionDenied, errors.Wrap(err))
|
|
}
|
|
dbUser, err := getUser(ctx, h.db, authnID)
|
|
if err != nil {
|
|
if ent.MaskNotFound(err) == nil {
|
|
return nil, connect.NewError(connect.CodeNotFound, errors.Wrap(err))
|
|
} else {
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, errors.Wrap(err))
|
|
}
|
|
}
|
|
|
|
var dbOrg *ent.Organization
|
|
err = WithTx(ctx, h.db, func(tx *ent.Tx) (err error) {
|
|
dbOrg, err = tx.Organization.Create().
|
|
SetName(cleanAndAppendRandom(authnID.Name())).
|
|
SetDisplayName(authnID.GivenName() + "'s Org").
|
|
SetCreatorID(dbUser.ID).
|
|
SetEditorID(dbUser.ID).
|
|
Save(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tx.Organization.UpdateOne(dbOrg).AddUsers(dbUser).Exec(ctx)
|
|
})
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err))
|
|
}
|
|
log = log.With("organization", dbOrg)
|
|
|
|
log.InfoContext(ctx, "created organization")
|
|
|
|
res := connect.NewResponse(&holos.CreateOrganizationResponse{
|
|
Organization: OrganizationToRPC(dbOrg),
|
|
})
|
|
return res, nil
|
|
}
|
|
|
|
func cleanAndAppendRandom(s string) string {
|
|
mapping := func(r rune) rune {
|
|
if unicode.IsLetter(r) {
|
|
return unicode.ToLower(r)
|
|
}
|
|
return -1
|
|
}
|
|
cleaned := strings.Map(mapping, s)
|
|
randNum := rand.Intn(900_000) + 100_000
|
|
return fmt.Sprintf("%s-%06d", cleaned, randNum)
|
|
}
|
|
|
|
// OrganizationToRPC returns an *holos.Organization adapted from *ent.Organization u.
|
|
func OrganizationToRPC(org *ent.Organization) *holos.Organization {
|
|
orgID := org.ID.String()
|
|
rpcEntity := holos.Organization{
|
|
OrgId: &orgID,
|
|
Name: org.Name,
|
|
DisplayName: &org.DisplayName,
|
|
Detail: &object.Detail{
|
|
CreatedBy: &object.ResourceEditor{
|
|
Editor: &object.ResourceEditor_UserId{
|
|
UserId: org.CreatedByID.String(),
|
|
},
|
|
},
|
|
CreatedAt: timestamppb.New(org.CreatedAt),
|
|
UpdatedBy: &object.ResourceEditor{
|
|
Editor: &object.ResourceEditor_UserId{
|
|
UserId: org.UpdatedByID.String(),
|
|
},
|
|
},
|
|
UpdatedAt: timestamppb.New(org.UpdatedAt),
|
|
},
|
|
}
|
|
return &rpcEntity
|
|
}
|