Add multi-tenant system

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-25 22:52:06 +01:00
parent 29ce55e89c
commit 0c176674a0
35 changed files with 1254 additions and 686 deletions

View File

@@ -345,9 +345,10 @@ type EvidenceStateTransition {
# Authentication types
type User {
id: ID!
fullName: String!
email: String!
organization: Organization! @goField(forceResolver: true)
organizations: [Organization!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!

View File

@@ -271,11 +271,12 @@ type ComplexityRoot struct {
}
User struct {
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
ID func(childComplexity int) int
Organization func(childComplexity int) int
UpdatedAt func(childComplexity int) int
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
FullName func(childComplexity int) int
ID func(childComplexity int) int
Organizations func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
Vendor struct {
@@ -337,7 +338,7 @@ type TaskResolver interface {
Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error)
}
type UserResolver interface {
Organization(ctx context.Context, obj *types.User) (*types.Organization, error)
Organizations(ctx context.Context, obj *types.User) ([]*types.Organization, error)
}
type executableSchema struct {
@@ -1258,6 +1259,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.Email(childComplexity), true
case "User.fullName":
if e.complexity.User.FullName == nil {
break
}
return e.complexity.User.FullName(childComplexity), true
case "User.id":
if e.complexity.User.ID == nil {
break
@@ -1265,12 +1273,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.ID(childComplexity), true
case "User.organization":
if e.complexity.User.Organization == nil {
case "User.organizations":
if e.complexity.User.Organizations == nil {
break
}
return e.complexity.User.Organization(childComplexity), true
return e.complexity.User.Organizations(childComplexity), true
case "User.updatedAt":
if e.complexity.User.UpdatedAt == nil {
@@ -1856,9 +1864,10 @@ type EvidenceStateTransition {
# Authentication types
type User {
id: ID!
fullName: String!
email: String!
organization: Organization! @goField(forceResolver: true)
organizations: [Organization!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
@@ -6740,10 +6749,12 @@ func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field g
switch field.Name {
case "id":
return ec.fieldContext_User_id(ctx, field)
case "fullName":
return ec.fieldContext_User_fullName(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "organization":
return ec.fieldContext_User_organization(ctx, field)
case "organizations":
return ec.fieldContext_User_organizations(ctx, field)
case "createdAt":
return ec.fieldContext_User_createdAt(ctx, field)
case "updatedAt":
@@ -7896,6 +7907,44 @@ func (ec *executionContext) fieldContext_User_id(_ context.Context, field graphq
return fc, nil
}
func (ec *executionContext) _User_fullName(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_fullName(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.FullName, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_User_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "User",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_email(ctx, field)
if err != nil {
@@ -7934,15 +7983,15 @@ func (ec *executionContext) fieldContext_User_email(_ context.Context, field gra
return fc, nil
}
func (ec *executionContext) _User_organization(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_organization(ctx, field)
func (ec *executionContext) _User_organizations(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_organizations(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.User().Organization(rctx, obj)
return ec.resolvers.User().Organizations(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@@ -7954,12 +8003,12 @@ func (ec *executionContext) _User_organization(ctx context.Context, field graphq
}
return graphql.Null
}
res := resTmp.(*types.Organization)
res := resTmp.([]*types.Organization)
fc.Result = res
return ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, field.Selections, res)
return ec.marshalNOrganization2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganizationᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_User_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
func (ec *executionContext) fieldContext_User_organizations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "User",
Field: field,
@@ -12702,16 +12751,21 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "fullName":
out.Values[i] = ec._User_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "email":
out.Values[i] = ec._User_email(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "organization":
case "organizations":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
res = ec._User_organization(ctx, field, obj)
res = ec._User_organizations(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
@@ -13853,8 +13907,42 @@ func (ec *executionContext) marshalNNode2githubᚗcomᚋgetproboᚋproboᚋpkg
return ec._Node(ctx, sel, v)
}
func (ec *executionContext) marshalNOrganization2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx context.Context, sel ast.SelectionSet, v types.Organization) graphql.Marshaler {
return ec._Organization(ctx, sel, &v)
func (ec *executionContext) marshalNOrganization2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganizationᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.Organization) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
for _, e := range ret {
if e == graphql.Null {
return graphql.Null
}
}
return ret
}
func (ec *executionContext) marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx context.Context, sel ast.SelectionSet, v *types.Organization) graphql.Marshaler {

View File

@@ -292,11 +292,12 @@ type UpdateVendorInput struct {
}
type User struct {
ID gid.GID `json:"id"`
Email string `json:"email"`
Organization *Organization `json:"organization"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
Email string `json:"email"`
Organizations []*Organization `json:"organizations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Vendor struct {

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"time"
"github.com/getprobo/probo/pkg/usrmgr/coredata"
)
type UserType struct {
ID string
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUser creates a new UserType from a coredata.User
func NewUser(u *coredata.User) *UserType {
return &UserType{
ID: u.ID.String(),
Email: u.EmailAddress,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// AddUserToOrganizationInput is the input for adding a user to an organization
type AddUserToOrganizationInput struct {
UserID string
OrganizationID string
}
// AddUserToOrganizationPayload is the payload for adding a user to an organization
type AddUserToOrganizationPayload struct {
User *UserType
}

View File

@@ -269,6 +269,7 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.User, error) {
return &types.User{
ID: user.ID,
Email: user.EmailAddress,
FullName: user.FullName,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}, nil
@@ -298,26 +299,30 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in
return types.NewEvidenceConnection(page), nil
}
// Organization is the resolver for the organization field.
func (r *userResolver) Organization(ctx context.Context, obj *types.User) (*types.Organization, error) {
// Get the user's organization ID
organizationID, err := r.usrmgrSvc.GetUserOrganization(ctx, obj.ID)
// Organizations is the resolver for the organizations field.
func (r *userResolver) Organizations(ctx context.Context, obj *types.User) ([]*types.Organization, error) {
// Get the user's organization IDs
organizationIDs, err := r.usrmgrSvc.GetUserOrganizations(ctx, obj.ID)
if err != nil {
return nil, fmt.Errorf("failed to get user organization: %w", err)
return nil, fmt.Errorf("failed to get user organizations: %w", err)
}
// If the user doesn't have an organization, return nil
if organizationID == gid.Nil {
return nil, nil
// If the user doesn't have any organizations, return an empty slice
if len(organizationIDs) == 0 {
return []*types.Organization{}, nil
}
// Get the organization details
organization, err := r.proboSvc.GetOrganization(ctx, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get organization details: %w", err)
// Get the organization details for each organization ID
var organizations []*types.Organization
for _, organizationID := range organizationIDs {
organization, err := r.proboSvc.GetOrganization(ctx, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get organization details: %w", err)
}
organizations = append(organizations, types.NewOrganization(organization))
}
return types.NewOrganization(organization), nil
return organizations, nil
}
// Control returns schema.ControlResolver implementation.