Fix role and organization logo
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -384,14 +384,14 @@ function OrganizationSelector({
|
||||
|
||||
const isSAMLUrl = targetUrl.includes("/connect/saml/");
|
||||
|
||||
// Use organization endpoint for all logos for consistency
|
||||
const logoUrl = organization.logoUrl;
|
||||
|
||||
return (
|
||||
<DropdownItem asChild key={organization.id}>
|
||||
{isSAMLUrl ? (
|
||||
<a href={targetUrl} className="flex items-center gap-2">
|
||||
<Avatar
|
||||
name={organization.name}
|
||||
src={organization.logoUrl}
|
||||
/>
|
||||
<Avatar name={organization.name} src={logoUrl} />
|
||||
<span className="flex-1">{organization.name}</span>
|
||||
{isAuthenticated && (
|
||||
<IconCheckmark1 size={16} className="text-green-600" />
|
||||
@@ -405,10 +405,7 @@ function OrganizationSelector({
|
||||
</a>
|
||||
) : (
|
||||
<Link to={targetUrl} className="flex items-center gap-2">
|
||||
<Avatar
|
||||
name={organization.name}
|
||||
src={organization.logoUrl}
|
||||
/>
|
||||
<Avatar name={organization.name} src={logoUrl} />
|
||||
<span className="flex-1">{organization.name}</span>
|
||||
{isAuthenticated && (
|
||||
<IconCheckmark1 size={16} className="text-green-600" />
|
||||
|
||||
@@ -175,19 +175,23 @@ export default function GeneralSettingsTab() {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setLogoPreview(reader.result as string);
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
logo: file,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
setLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
logoFile: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.logoFile": file,
|
||||
},
|
||||
onCompleted: () => {
|
||||
setLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleHorizontalLogoChange: ChangeEventHandler<HTMLInputElement> = (
|
||||
@@ -199,19 +203,23 @@ export default function GeneralSettingsTab() {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setHorizontalLogoPreview(reader.result as string);
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
horizontalLogo: file,
|
||||
},
|
||||
},
|
||||
onCompleted: () => {
|
||||
setHorizontalLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
updateOrganization({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organization.id,
|
||||
horizontalLogoFile: null,
|
||||
},
|
||||
},
|
||||
uploadables: {
|
||||
"input.horizontalLogoFile": file,
|
||||
},
|
||||
onCompleted: () => {
|
||||
setHorizontalLogoPreview(null);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteHorizontalLogo = () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func ExtractAttributeValue(assertion *saml.Assertion, attributeName string) (string, error) {
|
||||
@@ -76,12 +77,12 @@ func ExtractEmailDomain(email string) (string, error) {
|
||||
return domain, nil
|
||||
}
|
||||
|
||||
func MapSAMLRoleToSystemRole(samlRole string) string {
|
||||
func MapSAMLRoleToSystemRole(samlRole string) coredata.Role {
|
||||
if samlRole != "" && isValidRole(samlRole) {
|
||||
return samlRole
|
||||
return coredata.Role(samlRole)
|
||||
}
|
||||
|
||||
return "MEMBER"
|
||||
return coredata.RoleMember
|
||||
}
|
||||
|
||||
func isValidRole(role string) bool {
|
||||
|
||||
@@ -407,7 +407,7 @@ func (s *SAMLService) InitiateSAMLLogin(
|
||||
type SAMLUserInfo struct {
|
||||
Email string
|
||||
FullName string
|
||||
Role string
|
||||
Role coredata.Role
|
||||
SAMLSubject string
|
||||
OrganizationID gid.GID
|
||||
SAMLConfigID gid.GID
|
||||
|
||||
@@ -1055,15 +1055,9 @@ func (s Service) GetOrganizationLogoFile(
|
||||
organizationID gid.GID,
|
||||
session *coredata.Session,
|
||||
) (*coredata.File, error) {
|
||||
// Check authentication requirements before allowing access to logo
|
||||
err := s.CheckSingleOrganizationAccess(ctx, user, organizationID, session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access denied: %w", err)
|
||||
}
|
||||
|
||||
var logoFile *coredata.File
|
||||
|
||||
err = s.pg.WithConn(
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(conn pg.Conn) error {
|
||||
scope := coredata.NewScope(organizationID.TenantID())
|
||||
|
||||
@@ -44,15 +44,6 @@ type (
|
||||
invitationTokenValidity time.Duration
|
||||
scope coredata.Scoper
|
||||
}
|
||||
|
||||
Role string
|
||||
)
|
||||
|
||||
const (
|
||||
RoleOwner Role = "OWNER"
|
||||
RoleAdmin Role = "ADMIN"
|
||||
RoleMember Role = "MEMBER"
|
||||
RoleViewer Role = "VIEWER"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -284,7 +275,7 @@ type UserInvitation struct {
|
||||
ID gid.GID
|
||||
Email string
|
||||
FullName string
|
||||
Role string
|
||||
Role coredata.Role
|
||||
ExpiresAt time.Time
|
||||
AcceptedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
@@ -398,7 +389,7 @@ func (s *TenantAuthzService) AddUserToOrganization(
|
||||
ctx context.Context,
|
||||
userID gid.GID,
|
||||
orgID gid.GID,
|
||||
role string,
|
||||
role coredata.Role,
|
||||
) error {
|
||||
now := time.Now()
|
||||
membershipID := gid.New(s.scope.GetTenantID(), coredata.MembershipEntityType)
|
||||
@@ -624,7 +615,7 @@ func (s *TenantAuthzService) GetUserRoleInOrganization(
|
||||
ctx context.Context,
|
||||
userID gid.GID,
|
||||
orgID gid.GID,
|
||||
) (string, error) {
|
||||
) (coredata.Role, error) {
|
||||
membership := &coredata.Membership{}
|
||||
|
||||
err := s.pg.WithConn(
|
||||
@@ -675,7 +666,7 @@ func (s *TenantAuthzService) UpdateUserRole(
|
||||
ctx context.Context,
|
||||
userID gid.GID,
|
||||
orgID gid.GID,
|
||||
newRole string,
|
||||
newRole coredata.Role,
|
||||
) error {
|
||||
return s.pg.WithTx(
|
||||
ctx,
|
||||
@@ -702,7 +693,7 @@ func (s *TenantAuthzService) InviteUserToOrganization(
|
||||
organizationID gid.GID,
|
||||
emailAddress string,
|
||||
fullName string,
|
||||
role string,
|
||||
role coredata.Role,
|
||||
) (*coredata.Invitation, error) {
|
||||
var invitation *coredata.Invitation
|
||||
|
||||
@@ -808,7 +799,7 @@ func (s *TenantAuthzService) EnsureSAMLMembership(
|
||||
ctx context.Context,
|
||||
userID gid.GID,
|
||||
organizationID gid.GID,
|
||||
role string,
|
||||
role coredata.Role,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ type (
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Email string `db:"email"`
|
||||
FullName string `db:"full_name"`
|
||||
Role string `db:"role"`
|
||||
Role Role `db:"role"`
|
||||
Status InvitationStatus `db:"status"`
|
||||
ExpiresAt time.Time `db:"expires_at"`
|
||||
AcceptedAt *time.Time `db:"accepted_at"`
|
||||
@@ -47,7 +47,7 @@ type (
|
||||
OrganizationID gid.GID `json:"organization_id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
Role string `json:"role"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
ErrInvitationNotFound struct {
|
||||
|
||||
@@ -33,7 +33,7 @@ type (
|
||||
ID gid.GID `db:"id"`
|
||||
UserID gid.GID `db:"user_id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Role string `db:"role"`
|
||||
Role Role `db:"role"`
|
||||
FullName string `db:"full_name"`
|
||||
EmailAddress string `db:"email_address"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
|
||||
63
pkg/coredata/role.go
Normal file
63
pkg/coredata/role.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 coredata
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleOwner Role = "OWNER"
|
||||
RoleAdmin Role = "ADMIN"
|
||||
RoleMember Role = "MEMBER"
|
||||
RoleViewer Role = "VIEWER"
|
||||
)
|
||||
|
||||
func (r Role) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func (r *Role) Scan(value any) error {
|
||||
var s string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
s = v
|
||||
case []byte:
|
||||
s = string(v)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type for Role: %T", value)
|
||||
}
|
||||
|
||||
switch s {
|
||||
case "OWNER":
|
||||
*r = RoleOwner
|
||||
case "ADMIN":
|
||||
*r = RoleAdmin
|
||||
case "MEMBER":
|
||||
*r = RoleMember
|
||||
case "VIEWER":
|
||||
*r = RoleViewer
|
||||
default:
|
||||
return fmt.Errorf("invalid Role value: %q", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Role) Value() (driver.Value, error) {
|
||||
return r.String(), nil
|
||||
}
|
||||
@@ -108,6 +108,13 @@ enum InvitationStatus
|
||||
)
|
||||
}
|
||||
|
||||
enum Role @goModel(model: "github.com/getprobo/probo/pkg/coredata.Role") {
|
||||
OWNER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleOwner")
|
||||
ADMIN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleAdmin")
|
||||
MEMBER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleMember")
|
||||
VIEWER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleViewer")
|
||||
}
|
||||
|
||||
enum DocumentStatus
|
||||
@goModel(model: "github.com/getprobo/probo/pkg/coredata.DocumentStatus") {
|
||||
DRAFT
|
||||
@@ -1837,7 +1844,7 @@ type Membership implements Node {
|
||||
id: ID!
|
||||
userID: ID!
|
||||
organizationID: ID!
|
||||
role: String!
|
||||
role: Role!
|
||||
fullName: String!
|
||||
emailAddress: String!
|
||||
authMethod: UserAuthMethod! @goField(forceResolver: true)
|
||||
@@ -1849,7 +1856,7 @@ type Invitation implements Node {
|
||||
id: ID!
|
||||
email: String!
|
||||
fullName: String!
|
||||
role: String!
|
||||
role: Role!
|
||||
status: InvitationStatus!
|
||||
expiresAt: Datetime!
|
||||
acceptedAt: Datetime
|
||||
|
||||
@@ -9834,6 +9834,13 @@ enum InvitationStatus
|
||||
)
|
||||
}
|
||||
|
||||
enum Role @goModel(model: "github.com/getprobo/probo/pkg/coredata.Role") {
|
||||
OWNER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleOwner")
|
||||
ADMIN @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleAdmin")
|
||||
MEMBER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleMember")
|
||||
VIEWER @goEnum(value: "github.com/getprobo/probo/pkg/coredata.RoleViewer")
|
||||
}
|
||||
|
||||
enum DocumentStatus
|
||||
@goModel(model: "github.com/getprobo/probo/pkg/coredata.DocumentStatus") {
|
||||
DRAFT
|
||||
@@ -11563,7 +11570,7 @@ type Membership implements Node {
|
||||
id: ID!
|
||||
userID: ID!
|
||||
organizationID: ID!
|
||||
role: String!
|
||||
role: Role!
|
||||
fullName: String!
|
||||
emailAddress: String!
|
||||
authMethod: UserAuthMethod! @goField(forceResolver: true)
|
||||
@@ -11575,7 +11582,7 @@ type Invitation implements Node {
|
||||
id: ID!
|
||||
email: String!
|
||||
fullName: String!
|
||||
role: String!
|
||||
role: Role!
|
||||
status: InvitationStatus!
|
||||
expiresAt: Datetime!
|
||||
acceptedAt: Datetime
|
||||
@@ -38001,9 +38008,9 @@ func (ec *executionContext) _Invitation_role(ctx context.Context, field graphql.
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(string)
|
||||
res := resTmp.(coredata.Role)
|
||||
fc.Result = res
|
||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||
return ec.marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Invitation_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
@@ -38013,7 +38020,7 @@ func (ec *executionContext) fieldContext_Invitation_role(_ context.Context, fiel
|
||||
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 nil, errors.New("field of type Role does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
@@ -39588,9 +39595,9 @@ func (ec *executionContext) _Membership_role(ctx context.Context, field graphql.
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(string)
|
||||
res := resTmp.(coredata.Role)
|
||||
fc.Result = res
|
||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||
return ec.marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_Membership_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
@@ -39600,7 +39607,7 @@ func (ec *executionContext) fieldContext_Membership_role(_ context.Context, fiel
|
||||
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 nil, errors.New("field of type Role does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
@@ -56563,6 +56570,8 @@ func (ec *executionContext) fieldContext_SAMLConfiguration_organization(_ contex
|
||||
return ec.fieldContext_Organization_processingActivities(ctx, field)
|
||||
case "snapshots":
|
||||
return ec.fieldContext_Organization_snapshots(ctx, field)
|
||||
case "trustCenterFiles":
|
||||
return ec.fieldContext_Organization_trustCenterFiles(ctx, field)
|
||||
case "trustCenter":
|
||||
return ec.fieldContext_Organization_trustCenter(ctx, field)
|
||||
case "customDomain":
|
||||
@@ -61954,6 +61963,8 @@ func (ec *executionContext) fieldContext_TrustCenterFile_organization(_ context.
|
||||
return ec.fieldContext_Organization_trustCenter(ctx, field)
|
||||
case "customDomain":
|
||||
return ec.fieldContext_Organization_customDomain(ctx, field)
|
||||
case "samlConfigurations":
|
||||
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
|
||||
case "createdAt":
|
||||
return ec.fieldContext_Organization_createdAt(ctx, field)
|
||||
case "updatedAt":
|
||||
@@ -104604,6 +104615,38 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (ec *executionContext) unmarshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx context.Context, v any) (coredata.Role, error) {
|
||||
tmp, err := graphql.UnmarshalString(v)
|
||||
res := unmarshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole[tmp]
|
||||
return res, graphql.ErrorOnPath(ctx, err)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole(ctx context.Context, sel ast.SelectionSet, v coredata.Role) graphql.Marshaler {
|
||||
_ = sel
|
||||
res := graphql.MarshalString(marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole[v])
|
||||
if res == graphql.Null {
|
||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var (
|
||||
unmarshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole = map[string]coredata.Role{
|
||||
"OWNER": coredata.RoleOwner,
|
||||
"ADMIN": coredata.RoleAdmin,
|
||||
"MEMBER": coredata.RoleMember,
|
||||
"VIEWER": coredata.RoleViewer,
|
||||
}
|
||||
marshalNRole2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐRole = map[coredata.Role]string{
|
||||
coredata.RoleOwner: "OWNER",
|
||||
coredata.RoleAdmin: "ADMIN",
|
||||
coredata.RoleMember: "MEMBER",
|
||||
coredata.RoleViewer: "VIEWER",
|
||||
}
|
||||
)
|
||||
|
||||
func (ec *executionContext) marshalNSAMLConfiguration2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfigurationᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.SAMLConfiguration) graphql.Marshaler {
|
||||
ret := make(graphql.Array, len(v))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -1276,7 +1276,7 @@ type Invitation struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Role string `json:"role"`
|
||||
Role coredata.Role `json:"role"`
|
||||
Status coredata.InvitationStatus `json:"status"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
|
||||
@@ -1343,7 +1343,7 @@ type Membership struct {
|
||||
ID gid.GID `json:"id"`
|
||||
UserID gid.GID `json:"userID"`
|
||||
OrganizationID gid.GID `json:"organizationID"`
|
||||
Role string `json:"role"`
|
||||
Role coredata.Role `json:"role"`
|
||||
FullName string `json:"fullName"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
AuthMethod coredata.UserAuthMethod `json:"authMethod"`
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"github.com/getprobo/probo/pkg/authz"
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/page"
|
||||
@@ -1489,7 +1488,7 @@ func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.Confirm
|
||||
// InviteUser is the resolver for the inviteUser field.
|
||||
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
|
||||
authzSvc := r.AuthzService(ctx, input.OrganizationID.TenantID())
|
||||
invitation, err := authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember))
|
||||
invitation, err := authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, coredata.RoleMember)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot invite user to organization: %w", err))
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func ListInvitationsHandler(authzSvc *authz.Service) http.HandlerFunc {
|
||||
ID: invitation.ID,
|
||||
Email: invitation.Email,
|
||||
FullName: invitation.FullName,
|
||||
Role: invitation.Role,
|
||||
Role: invitation.Role.String(),
|
||||
ExpiresAt: invitation.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
CreatedAt: invitation.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
Organization: OrganizationResponseSummary{
|
||||
|
||||
Reference in New Issue
Block a user