diff --git a/apps/console/src/components/NavUser.tsx b/apps/console/src/components/NavUser.tsx index e222048ef..a4b8e7d6e 100644 --- a/apps/console/src/components/NavUser.tsx +++ b/apps/console/src/components/NavUser.tsx @@ -1,21 +1,13 @@ "use client"; -import { - BadgeCheck, - Bell, - ChevronsUpDown, - CreditCard, - LogOut, - Sparkles, -} from "lucide-react"; +import { ChevronsUpDown, LogOut } from "lucide-react"; import { graphql, useFragment } from "react-relay"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, diff --git a/apps/console/src/components/__generated__/OrganizationSwitcher_organizations.graphql.ts b/apps/console/src/components/__generated__/OrganizationSwitcher_organizations.graphql.ts index 0952497a2..1b8777f38 100644 --- a/apps/console/src/components/__generated__/OrganizationSwitcher_organizations.graphql.ts +++ b/apps/console/src/components/__generated__/OrganizationSwitcher_organizations.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<075aa1caf8416e0cce7536d0da9b5ca9>> * @lightSyntaxTransform * @nogrep */ @@ -16,7 +16,7 @@ export type OrganizationSwitcher_organizations$data = { readonly edges: ReadonlyArray<{ readonly node: { readonly id: string; - readonly logoUrl: string; + readonly logoUrl: string | null | undefined; readonly name: string; }; }>; diff --git a/apps/console/src/components/ui/command.tsx b/apps/console/src/components/ui/command.tsx index a67a90493..b5360cf18 100644 --- a/apps/console/src/components/ui/command.tsx +++ b/apps/console/src/components/ui/command.tsx @@ -6,6 +6,11 @@ import { Search } from "lucide-react"; import { cn } from "@/lib/utils"; import { Dialog, DialogContent } from "@/components/ui/dialog"; +// Add the missing type definition +type CommandDialogProps = DialogProps & { + children?: React.ReactNode; +}; + const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -21,8 +26,6 @@ const Command = React.forwardRef< )); Command.displayName = CommandPrimitive.displayName; -interface CommandDialogProps extends DialogProps {} - const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( @@ -39,7 +42,7 @@ const CommandInput = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( -
+
(null); + + const [isEditNameOpen, setIsEditNameOpen] = useState(false); + const [organizationName, setOrganizationName] = useState( + organization.name || "" + ); + const [isUploading, setIsUploading] = useState(false); + + const [updateOrganization] = + useMutation( + updateOrganizationMutation + ); + + const handleUpdateName = () => { + updateOrganization({ + variables: { + input: { + organizationId: organization.id, + name: organizationName, + }, + }, + onCompleted: () => { + toast({ + title: "Organization updated", + description: "Organization name has been updated successfully.", + variant: "default", + }); + setIsEditNameOpen(false); + }, + onError: (error) => { + toast({ + title: "Error updating organization", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + + const handleLogoUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Create a FileReader to read the file as a data URL + const reader = new FileReader(); + reader.onloadend = () => { + setIsUploading(true); + + updateOrganization({ + variables: { + input: { + organizationId: organization.id, + logo: null, + }, + }, + uploadables: { + "input.logo": file, + }, + onCompleted: () => { + setIsUploading(false); + toast({ + title: "Logo updated", + description: "Organization logo has been updated successfully.", + variant: "default", + }); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }, + onError: (error) => { + setIsUploading(false); + toast({ + title: "Error updating logo", + description: error.message, + variant: "destructive", + }); + }, + }); + }; + reader.readAsDataURL(file); + }; return ( <> @@ -65,32 +174,10 @@ function SettingsPageContent({ - User & Organisation information - - Publish your trust page to the web - + Organization information + Manage your organization details -
- -
-
- - - This is your email to connect to Probo - -
-
- - john.doe@example.com - - -
-
-
-
@@ -110,7 +197,23 @@ function SettingsPageContent({ Upload a logo to be displayed at the top of your trust page
- +
+ + +
@@ -125,16 +228,19 @@ function SettingsPageContent({
{organization.name} -
- - @@ -193,6 +299,34 @@ function SettingsPageContent({ + + + + + Edit Organization Name + + Update the name of your organization. + + +
+
+ + setOrganizationName(e.target.value)} + placeholder="Enter organization name" + /> +
+
+ + + + +
+
); } diff --git a/apps/console/src/pages/__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql.ts b/apps/console/src/pages/__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql.ts index ecefbadac..e684e8a29 100644 --- a/apps/console/src/pages/__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql.ts +++ b/apps/console/src/pages/__generated__/CreateOrganizationPageCreateOrganizationMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<75687d4b60d4ae79658fffb99ea6f52b>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -21,7 +21,7 @@ export type CreateOrganizationPageCreateOrganizationMutation$data = { readonly organizationEdge: { readonly node: { readonly id: string; - readonly logoUrl: string; + readonly logoUrl: string | null | undefined; readonly name: string; }; }; diff --git a/apps/console/src/pages/__generated__/OrganizationSelectionPageQuery.graphql.ts b/apps/console/src/pages/__generated__/OrganizationSelectionPageQuery.graphql.ts index 0ac0afdab..66ef66a16 100644 --- a/apps/console/src/pages/__generated__/OrganizationSelectionPageQuery.graphql.ts +++ b/apps/console/src/pages/__generated__/OrganizationSelectionPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<59c4e8be77fdb5ebcc76c2a611f38052>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -17,7 +17,7 @@ export type OrganizationSelectionPageQuery$data = { readonly edges: ReadonlyArray<{ readonly node: { readonly id: string; - readonly logoUrl: string; + readonly logoUrl: string | null | undefined; readonly name: string; }; }>; diff --git a/apps/console/src/pages/__generated__/SettingsPageQuery.graphql.ts b/apps/console/src/pages/__generated__/SettingsPageQuery.graphql.ts index d83ed5638..767ea647a 100644 --- a/apps/console/src/pages/__generated__/SettingsPageQuery.graphql.ts +++ b/apps/console/src/pages/__generated__/SettingsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<9a5ce377f093c4bbbb2375aacc373fb0>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -15,7 +15,7 @@ export type SettingsPageQuery$variables = { export type SettingsPageQuery$data = { readonly organization: { readonly id: string; - readonly logoUrl?: string; + readonly logoUrl?: string | null | undefined; readonly name?: string; }; }; diff --git a/apps/console/src/pages/__generated__/SettingsPageUpdateOrganizationMutation.graphql.ts b/apps/console/src/pages/__generated__/SettingsPageUpdateOrganizationMutation.graphql.ts new file mode 100644 index 000000000..f3c60b462 --- /dev/null +++ b/apps/console/src/pages/__generated__/SettingsPageUpdateOrganizationMutation.graphql.ts @@ -0,0 +1,123 @@ +/** + * @generated SignedSource<<76018198697d82d6f630558618c6aaff>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type UpdateOrganizationInput = { + logo?: any | null | undefined; + name?: string | null | undefined; + organizationId: string; +}; +export type SettingsPageUpdateOrganizationMutation$variables = { + input: UpdateOrganizationInput; +}; +export type SettingsPageUpdateOrganizationMutation$data = { + readonly updateOrganization: { + readonly organization: { + readonly id: string; + readonly logoUrl: string | null | undefined; + readonly name: string; + }; + }; +}; +export type SettingsPageUpdateOrganizationMutation = { + response: SettingsPageUpdateOrganizationMutation$data; + variables: SettingsPageUpdateOrganizationMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateOrganizationPayload", + "kind": "LinkedField", + "name": "updateOrganization", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SettingsPageUpdateOrganizationMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SettingsPageUpdateOrganizationMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "2bf2a6e054aed9bd337372682342a30f", + "id": null, + "metadata": {}, + "name": "SettingsPageUpdateOrganizationMutation", + "operationKind": "mutation", + "text": "mutation SettingsPageUpdateOrganizationMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "737ec44a08ac05e9f78827811cc0ca0e"; + +export default node; diff --git a/pkg/coredata/migrations/20250313T110700Z.sql b/pkg/coredata/migrations/20250313T110700Z.sql new file mode 100644 index 000000000..df524b696 --- /dev/null +++ b/pkg/coredata/migrations/20250313T110700Z.sql @@ -0,0 +1 @@ +ALTER TABLE organizations RENAME COLUMN logo_url TO logo_object_key; \ No newline at end of file diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index bec39eed9..613725afe 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -27,12 +27,12 @@ import ( type ( Organization struct { - ID gid.GID `db:"id"` - TenantID gid.TenantID `db:"tenant_id"` - Name string `db:"name"` - LogoURL string `db:"logo_url"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + TenantID gid.TenantID `db:"tenant_id"` + Name string `db:"name"` + LogoObjectKey string `db:"logo_object_key"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } Organizations []*Organization @@ -49,7 +49,7 @@ SELECT tenant_id, id, name, - logo_url, + logo_object_key, created_at, updated_at FROM @@ -89,19 +89,19 @@ INSERT INTO organizations ( tenant_id, id, name, - logo_url, + logo_object_key, created_at, updated_at -) VALUES (@tenant_id, @id, @name, @logo_url, @created_at, @updated_at) +) VALUES (@tenant_id, @id, @name, @logo_object_key, @created_at, @updated_at) ` args := pgx.StrictNamedArgs{ - "tenant_id": o.TenantID, - "id": o.ID, - "name": o.Name, - "logo_url": o.LogoURL, - "created_at": o.CreatedAt, - "updated_at": o.UpdatedAt, + "tenant_id": o.TenantID, + "id": o.ID, + "name": o.Name, + "logo_object_key": o.LogoObjectKey, + "created_at": o.CreatedAt, + "updated_at": o.UpdatedAt, } _, err := conn.Exec(ctx, q, args) @@ -111,3 +111,38 @@ INSERT INTO organizations ( return nil } + +func (o *Organization) Update( + ctx context.Context, + scope Scoper, + conn pg.Conn, +) error { + q := ` +UPDATE organizations +SET + name = @name, + logo_object_key = @logo_object_key, + updated_at = @updated_at +WHERE + %s + AND id = @id +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "id": o.ID, + "name": o.Name, + "logo_object_key": o.LogoObjectKey, + "updated_at": o.UpdatedAt, + } + + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update organization: %w", err) + } + + return nil +} diff --git a/pkg/probo/organization_service.go b/pkg/probo/organization_service.go index 38e426aee..5e66e7724 100644 --- a/pkg/probo/organization_service.go +++ b/pkg/probo/organization_service.go @@ -17,10 +17,14 @@ package probo import ( "context" "fmt" + "io" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" + "go.gearno.de/crypto/uuid" "go.gearno.de/kit/pg" ) @@ -32,6 +36,12 @@ type ( CreateOrganizationRequest struct { Name string } + + UpdateOrganizationRequest struct { + ID gid.GID + Name *string + File io.Reader + } ) func (s OrganizationService) Create( @@ -94,3 +104,85 @@ func (s OrganizationService) Get( return organization, nil } + +func (s OrganizationService) Update( + ctx context.Context, + req UpdateOrganizationRequest, +) (*coredata.Organization, error) { + organization := &coredata.Organization{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := organization.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + organization.UpdatedAt = time.Now() + + if req.Name != nil { + organization.Name = *req.Name + } + + if req.File != nil { + objectKey, err := uuid.NewV7() + if err != nil { + return fmt.Errorf("cannot generate object key: %w", err) + } + + _, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(objectKey.String()), + Body: req.File, + }) + + if err != nil { + return fmt.Errorf("cannot upload file to S3: %w", err) + } + + organization.LogoObjectKey = objectKey.String() + } + + if err := organization.Update(ctx, s.svc.scope, conn); err != nil { + return fmt.Errorf("cannot update organization: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return organization, nil +} + +func (s OrganizationService) GenerateLogoURL( + ctx context.Context, + organizationID gid.GID, + expiresIn time.Duration, +) (*string, error) { + organization, err := s.Get(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("cannot get organization: %w", err) + } + + if organization.LogoObjectKey == "" { + return nil, nil + } + + presignClient := s3.NewPresignClient(s.svc.s3) + + presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.svc.bucket), + Key: aws.String(organization.LogoObjectKey), + }, func(opts *s3.PresignOptions) { + opts.Expires = expiresIn + }) + if err != nil { + return nil, fmt.Errorf("cannot presign GetObject request: %w", err) + } + + return &presignedReq.URL, nil +} diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 3c6a2f71b..a15620f48 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -107,7 +107,7 @@ type OrganizationEdge { type Organization implements Node { id: ID! name: String! - logoUrl: String! + logoUrl: String @goField(forceResolver: true) frameworks( first: Int @@ -467,7 +467,7 @@ input CreateOrganizationInput { input UpdateOrganizationInput { organizationId: ID! name: String - logoUrl: String + logo: Upload } input DeleteOrganizationInput { diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index e86e2e2cb..65b2ea48f 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -427,6 +427,7 @@ type MutationResolver interface { ConfirmEmail(ctx context.Context, input types.ConfirmEmailInput) (*types.ConfirmEmailPayload, error) } type OrganizationResolver interface { + LogoURL(ctx context.Context, obj *types.Organization) (*string, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PeopleConnection, error) @@ -2065,7 +2066,7 @@ type OrganizationEdge { type Organization implements Node { id: ID! name: String! - logoUrl: String! + logoUrl: String @goField(forceResolver: true) frameworks( first: Int @@ -2425,7 +2426,7 @@ input CreateOrganizationInput { input UpdateOrganizationInput { organizationId: ID! name: String - logoUrl: String + logo: Upload } input DeleteOrganizationInput { @@ -7462,29 +7463,26 @@ func (ec *executionContext) _Organization_logoUrl(ctx context.Context, field gra 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.LogoURL, nil + return ec.resolvers.Organization().LogoURL(rctx, obj) }) 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) + res := resTmp.(*string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Organization_logoUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Organization", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -13938,7 +13936,7 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "name", "logoUrl"} + fieldsInOrder := [...]string{"organizationId", "name", "logo"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -13959,13 +13957,13 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co return it, err } it.Name = data - case "logoUrl": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("logoUrl")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) + case "logo": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("logo")) + data, err := ec.unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v) if err != nil { return it, err } - it.LogoURL = data + it.Logo = data } } @@ -15815,10 +15813,33 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection atomic.AddUint32(&out.Invalids, 1) } case "logoUrl": - out.Values[i] = ec._Organization_logoUrl(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + res = ec._Organization_logoUrl(ctx, field, obj) + return res } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "frameworks": field := field @@ -19835,6 +19856,22 @@ var ( } ) +func (ec *executionContext) unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v any) (*graphql.Upload, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalUpload(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, sel ast.SelectionSet, v *graphql.Upload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalUpload(*v) + return res +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/console/v1/types/organization.go b/pkg/server/api/console/v1/types/organization.go index 134536119..20e99cdc4 100644 --- a/pkg/server/api/console/v1/types/organization.go +++ b/pkg/server/api/console/v1/types/organization.go @@ -22,7 +22,6 @@ func NewOrganization(o *coredata.Organization) *Organization { return &Organization{ ID: o.ID, Name: o.Name, - LogoURL: o.LogoURL, CreatedAt: o.CreatedAt, UpdatedAt: o.UpdatedAt, } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 02603c0e3..920dc0b46 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -253,7 +253,7 @@ type Mutation struct { type Organization struct { ID gid.GID `json:"id"` Name string `json:"name"` - LogoURL string `json:"logoUrl"` + LogoURL *string `json:"logoUrl,omitempty"` Frameworks *FrameworkConnection `json:"frameworks"` Vendors *VendorConnection `json:"vendors"` Peoples *PeopleConnection `json:"peoples"` @@ -399,9 +399,9 @@ type UpdateFrameworkPayload struct { } type UpdateOrganizationInput struct { - OrganizationID gid.GID `json:"organizationId"` - Name *string `json:"name,omitempty"` - LogoURL *string `json:"logoUrl,omitempty"` + OrganizationID gid.GID `json:"organizationId"` + Name *string `json:"name,omitempty"` + Logo *graphql.Upload `json:"logo,omitempty"` } type UpdateOrganizationPayload struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index e424e10af..72014a1d1 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -197,6 +197,29 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C }, nil } +// UpdateOrganization is the resolver for the updateOrganization field. +func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) { + svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID()) + + req := probo.UpdateOrganizationRequest{ + ID: input.OrganizationID, + Name: input.Name, + } + + if input.Logo != nil { + req.File = input.Logo.File + } + + organization, err := svc.Organizations.Update(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot update organization: %w", err) + } + + return &types.UpdateOrganizationPayload{ + Organization: types.NewOrganization(organization), + }, nil +} + // DeleteOrganization is the resolver for the deleteOrganization field. func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) { panic(fmt.Errorf("not implemented: DeleteOrganization - deleteOrganization")) @@ -320,6 +343,11 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda }, nil } +// ImportFramework is the resolver for the importFramework field. +func (r *mutationResolver) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) { + panic(fmt.Errorf("not implemented: ImportFramework - importFramework")) +} + // CreateControl is the resolver for the createControl field. func (r *mutationResolver) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) { svc := r.GetTenantServiceIfAuthorized(ctx, input.FrameworkID.TenantID()) @@ -464,6 +492,13 @@ func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.Confirm return &types.ConfirmEmailPayload{Success: true}, nil } +// LogoURL is the resolver for the logoUrl field. +func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) { + svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID()) + + return svc.Organizations.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) +} + // Frameworks is the resolver for the frameworks field. func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) { svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())