Fix role and organization logo

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-10-30 22:42:17 +01:00
parent aabad5087c
commit d1c31978da
14 changed files with 177 additions and 74 deletions

View File

@@ -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" />

View File

@@ -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 = () => {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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())

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"`

View File

@@ -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))
}

View File

@@ -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{