Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Upload favicon backend support #6606

Merged
merged 1 commit into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions admin/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (s *Service) InitOrganizationBilling(ctx context.Context, org *database.Org
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: org.QuotaProjects,
QuotaDeployments: org.QuotaDeployments,
Expand Down Expand Up @@ -150,6 +151,7 @@ func (s *Service) RepairOrganizationBilling(ctx context.Context, org *database.O
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: org.QuotaProjects,
QuotaDeployments: org.QuotaDeployments,
Expand Down Expand Up @@ -208,6 +210,7 @@ func (s *Service) RepairOrganizationBilling(ctx context.Context, org *database.O
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: biggerOfInt(sub.Plan.Quotas.NumProjects, org.QuotaProjects),
QuotaDeployments: biggerOfInt(sub.Plan.Quotas.NumDeployments, org.QuotaDeployments),
Expand Down Expand Up @@ -280,6 +283,7 @@ func (s *Service) StartTrial(ctx context.Context, org *database.Organization) (*
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: biggerOfInt(plan.Quotas.NumProjects, org.QuotaProjects),
QuotaDeployments: biggerOfInt(plan.Quotas.NumDeployments, org.QuotaDeployments),
Expand Down
3 changes: 3 additions & 0 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ type Organization struct {
DisplayName string `db:"display_name"`
Description string
LogoAssetID *string `db:"logo_asset_id"`
FaviconAssetID *string `db:"favicon_asset_id"`
CustomDomain string `db:"custom_domain"`
AllUsergroupID *string `db:"all_usergroup_id"`
CreatedOn time.Time `db:"created_on"`
Expand All @@ -340,6 +341,7 @@ type InsertOrganizationOptions struct {
DisplayName string
Description string
LogoAssetID *string
FaviconAssetID *string
CustomDomain string `validate:"omitempty,fqdn"`
QuotaProjects int
QuotaDeployments int
Expand All @@ -359,6 +361,7 @@ type UpdateOrganizationOptions struct {
DisplayName string
Description string
LogoAssetID *string
FaviconAssetID *string
CustomDomain string `validate:"omitempty,fqdn"`
QuotaProjects int
QuotaDeployments int
Expand Down
1 change: 1 addition & 0 deletions admin/database/postgres/migrations/0057.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE orgs ADD COLUMN favicon_asset_id UUID REFERENCES assets(id) ON DELETE SET NULL;
10 changes: 5 additions & 5 deletions admin/database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ func (c *connection) InsertOrganization(ctx context.Context, opts *database.Inse
}

res := &database.Organization{}
err := c.getDB(ctx).QueryRowxContext(ctx, `INSERT INTO orgs(name, display_name, description, logo_asset_id, custom_domain, quota_projects, quota_deployments, quota_slots_total, quota_slots_per_deployment, quota_outstanding_invites, quota_storage_limit_bytes_per_deployment, billing_customer_id, payment_customer_id, billing_email, created_by_user_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *`,
opts.Name, opts.DisplayName, opts.Description, opts.LogoAssetID, opts.CustomDomain, opts.QuotaProjects, opts.QuotaDeployments, opts.QuotaSlotsTotal, opts.QuotaSlotsPerDeployment, opts.QuotaOutstandingInvites, opts.QuotaStorageLimitBytesPerDeployment, opts.BillingCustomerID, opts.PaymentCustomerID, opts.BillingEmail, opts.CreatedByUserID).StructScan(res)
err := c.getDB(ctx).QueryRowxContext(ctx, `INSERT INTO orgs(name, display_name, description, logo_asset_id, favicon_asset_id, custom_domain, quota_projects, quota_deployments, quota_slots_total, quota_slots_per_deployment, quota_outstanding_invites, quota_storage_limit_bytes_per_deployment, billing_customer_id, payment_customer_id, billing_email, created_by_user_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING *`,
opts.Name, opts.DisplayName, opts.Description, opts.LogoAssetID, opts.FaviconAssetID, opts.CustomDomain, opts.QuotaProjects, opts.QuotaDeployments, opts.QuotaSlotsTotal, opts.QuotaSlotsPerDeployment, opts.QuotaOutstandingInvites, opts.QuotaStorageLimitBytesPerDeployment, opts.BillingCustomerID, opts.PaymentCustomerID, opts.BillingEmail, opts.CreatedByUserID).StructScan(res)
if err != nil {
return nil, parseErr("org", err)
}
Expand All @@ -168,8 +168,8 @@ func (c *connection) UpdateOrganization(ctx context.Context, id string, opts *da

res := &database.Organization{}
err := c.getDB(ctx).QueryRowxContext(ctx,
`UPDATE orgs SET name=$1, display_name=$2, description=$3, logo_asset_id=$4, custom_domain=$5, quota_projects=$6, quota_deployments=$7, quota_slots_total=$8, quota_slots_per_deployment=$9, quota_outstanding_invites=$10, quota_storage_limit_bytes_per_deployment=$11, billing_customer_id=$12, payment_customer_id=$13, billing_email=$14, created_by_user_id=$15, updated_on=now() WHERE id=$16 RETURNING *`,
opts.Name, opts.DisplayName, opts.Description, opts.LogoAssetID, opts.CustomDomain, opts.QuotaProjects, opts.QuotaDeployments, opts.QuotaSlotsTotal, opts.QuotaSlotsPerDeployment, opts.QuotaOutstandingInvites, opts.QuotaStorageLimitBytesPerDeployment, opts.BillingCustomerID, opts.PaymentCustomerID, opts.BillingEmail, opts.CreatedByUserID, id).StructScan(res)
`UPDATE orgs SET name=$1, display_name=$2, description=$3, logo_asset_id=$4, favicon_asset_id=$5, custom_domain=$6, quota_projects=$7, quota_deployments=$8, quota_slots_total=$9, quota_slots_per_deployment=$10, quota_outstanding_invites=$11, quota_storage_limit_bytes_per_deployment=$12, billing_customer_id=$13, payment_customer_id=$14, billing_email=$15, created_by_user_id=$16, updated_on=now() WHERE id=$17 RETURNING *`,
opts.Name, opts.DisplayName, opts.Description, opts.LogoAssetID, opts.FaviconAssetID, opts.CustomDomain, opts.QuotaProjects, opts.QuotaDeployments, opts.QuotaSlotsTotal, opts.QuotaSlotsPerDeployment, opts.QuotaOutstandingInvites, opts.QuotaStorageLimitBytesPerDeployment, opts.BillingCustomerID, opts.PaymentCustomerID, opts.BillingEmail, opts.CreatedByUserID, id).StructScan(res)
if err != nil {
return nil, parseErr("org", err)
}
Expand Down
1 change: 1 addition & 0 deletions admin/jobs/river/subscription_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func (w *SubscriptionCancellationCheckWorker) subscriptionCancellationCheck(ctx
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: 0,
QuotaDeployments: 0,
Expand Down
1 change: 1 addition & 0 deletions admin/jobs/river/trial_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ func (w *TrialGracePeriodCheckWorker) trialGracePeriodCheck(ctx context.Context)
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: 0,
QuotaDeployments: 0,
Expand Down
3 changes: 3 additions & 0 deletions admin/server/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ func (s *Server) RenewBillingSubscription(ctx context.Context, req *adminv1.Rene
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: valOrDefault(sub.Plan.Quotas.NumProjects, org.QuotaProjects),
QuotaDeployments: valOrDefault(sub.Plan.Quotas.NumDeployments, org.QuotaDeployments),
Expand Down Expand Up @@ -506,6 +507,7 @@ func (s *Server) SudoUpdateOrganizationBillingCustomer(ctx context.Context, req
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: org.QuotaProjects,
QuotaDeployments: org.QuotaDeployments,
Expand Down Expand Up @@ -879,6 +881,7 @@ func (s *Server) updateQuotasAndHandleBillingIssues(ctx context.Context, org *da
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: valOrDefault(sub.Plan.Quotas.NumProjects, org.QuotaProjects),
QuotaDeployments: valOrDefault(sub.Plan.Quotas.NumDeployments, org.QuotaDeployments),
Expand Down
18 changes: 18 additions & 0 deletions admin/server/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,23 @@ func (s *Server) UpdateOrganization(ctx context.Context, req *adminv1.UpdateOrga
}
}

faviconAssetID := org.FaviconAssetID
if req.FaviconAssetId != nil { // Means it should be updated
if *req.FaviconAssetId == "" { // Means it should be cleared
faviconAssetID = nil
} else {
faviconAssetID = req.FaviconAssetId
}
}

nameChanged := req.NewName != nil && *req.NewName != org.Name
emailChanged := req.BillingEmail != nil && *req.BillingEmail != org.BillingEmail
org, err = s.admin.DB.UpdateOrganization(ctx, org.ID, &database.UpdateOrganizationOptions{
Name: valOrDefault(req.NewName, org.Name),
DisplayName: valOrDefault(req.DisplayName, org.DisplayName),
Description: valOrDefault(req.Description, org.Description),
LogoAssetID: logoAssetID,
FaviconAssetID: faviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: org.QuotaProjects,
QuotaDeployments: org.QuotaDeployments,
Expand Down Expand Up @@ -875,6 +885,7 @@ func (s *Server) SudoUpdateOrganizationQuotas(ctx context.Context, req *adminv1.
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: org.CustomDomain,
QuotaProjects: int(valOrDefault(req.Projects, int32(org.QuotaProjects))),
QuotaDeployments: int(valOrDefault(req.Deployments, int32(org.QuotaDeployments))),
Expand Down Expand Up @@ -919,6 +930,7 @@ func (s *Server) SudoUpdateOrganizationCustomDomain(ctx context.Context, req *ad
DisplayName: org.DisplayName,
Description: org.Description,
LogoAssetID: org.LogoAssetID,
FaviconAssetID: org.FaviconAssetID,
CustomDomain: req.CustomDomain,
QuotaProjects: org.QuotaProjects,
QuotaDeployments: org.QuotaDeployments,
Expand Down Expand Up @@ -946,12 +958,18 @@ func (s *Server) organizationToDTO(o *database.Organization, privileged bool) *a
logoURL = s.admin.URLs.WithCustomDomain(o.CustomDomain).Asset(*o.LogoAssetID)
}

var faviconURL string
if o.FaviconAssetID != nil {
faviconURL = s.admin.URLs.WithCustomDomain(o.CustomDomain).Asset(*o.FaviconAssetID)
}

res := &adminv1.Organization{
Id: o.ID,
Name: o.Name,
DisplayName: o.DisplayName,
Description: o.Description,
LogoUrl: logoURL,
FaviconUrl: faviconURL,
CustomDomain: o.CustomDomain,
Quotas: &adminv1.OrganizationQuotas{
Projects: int32(o.QuotaProjects),
Expand Down
1 change: 1 addition & 0 deletions admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ func (s *Service) CreateOrganizationForUser(ctx context.Context, userID, email,
DisplayName: orgName,
Description: description,
LogoAssetID: nil,
FaviconAssetID: nil,
CustomDomain: "",
QuotaProjects: quotaProjects,
QuotaDeployments: quotaDeployments,
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func OrgCmd(ch *cmdutil.Helper) *cobra.Command {
orgCmd.AddCommand(DeleteCmd(ch))
orgCmd.AddCommand(RenameCmd(ch))
orgCmd.AddCommand(UploadLogoCmd(ch))
orgCmd.AddCommand(UploadFaviconCmd(ch))

return orgCmd
}
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/org/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func ShowCmd(ch *cmdutil.Helper) *cobra.Command {
ch.Printf("Display Name: %s\n", org.DisplayName)
ch.Printf("Description: %s\n", org.Description)
ch.Printf("Custom Logo: %s\n", org.LogoUrl)
ch.Printf("Custom Favicon: %s\n", org.FaviconUrl)
ch.Printf("Custom Domain: %s\n", org.CustomDomain)
ch.Printf("Billing Email: %s\n", org.BillingEmail)
ch.Printf("Created On: %s\n", org.CreatedOn.AsTime().Format(time.RFC3339Nano))
Expand Down
146 changes: 146 additions & 0 deletions cli/cmd/org/upload_favicon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package org

import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/rilldata/rill/cli/pkg/cmdutil"
adminv1 "github.com/rilldata/rill/proto/gen/rill/admin/v1"
"github.com/spf13/cobra"
)

func UploadFaviconCmd(ch *cmdutil.Helper) *cobra.Command {
var path string
var remove bool

cmd := &cobra.Command{
Use: "upload-favicon [<org-name> [<path-to-image>]]",
Args: cobra.MaximumNArgs(2),
Short: "Upload a custom favicon",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := ch.Client()
if err != nil {
return err
}

// Parse positional args into flags
if len(args) > 0 {
ch.Org = args[0]
if len(args) > 1 {
path = args[1]
}
}
if ch.Org == "" {
return fmt.Errorf("an organization name is required")
}

// Handle --remove
if remove {
if path != "" {
return fmt.Errorf("cannot specify both --remove and a path")
}

// Confirmation prompt
if ok, err := cmdutil.ConfirmPrompt(fmt.Sprintf("You are removing the custom favicon for %q. Continue?", ch.Org), "", false); err != nil || !ok {
return err
}

empty := ""
_, err = client.UpdateOrganization(cmd.Context(), &adminv1.UpdateOrganizationRequest{
Name: ch.Org,
FaviconAssetId: &empty,
})
if err != nil {
return err
}

ch.PrintfSuccess("Removed favicon from organization %q\n", ch.Org)
return nil
}

// Check the file is an image
ext := strings.TrimPrefix(filepath.Ext(path), ".")
switch ext {
case "png", "ico":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add gif, svg and jpg also. Might as well just allow every image type at this point

Copy link
Contributor

@begelundmuller begelundmuller Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that's not supported for favicons. But I can add it for normal logo uploads.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default:
return fmt.Errorf("invalid file type %q (expected PNG or JPG)", ext)
}

// Validate and open the path
fi, err := os.Stat(path)
if err != nil {
return fmt.Errorf("failed to read %q: %w", path, err)
}
if fi.IsDir() {
return fmt.Errorf("failed to upload %q: the path is a directory", path)
}
if fi.Size() == 0 {
return fmt.Errorf("failed to upload %q: the file is empty", path)
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open %q: %w", path, err)
}
defer f.Close()

// Confirmation prompt
if ok, err := cmdutil.ConfirmPrompt(fmt.Sprintf("You are changing the custom favicon for %q. Continue?", ch.Org), "", false); err != nil || !ok {
return err
}

// Generate the asset upload URL
asset, err := client.CreateAsset(cmd.Context(), &adminv1.CreateAssetRequest{
OrganizationName: ch.Org,
Type: "image",
Name: "favicon",
Extension: ext,
Cacheable: true,
EstimatedSizeBytes: fi.Size(),
})
if err != nil {
return err
}

// Execute the upload
req, err := http.NewRequestWithContext(cmd.Context(), http.MethodPut, asset.SignedUrl, f)
if err != nil {
return fmt.Errorf("failed to upload: %w", err)
}
for k, v := range asset.SigningHeaders {
req.Header.Set(k, v)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to upload: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to upload: status=%d, error=%s", resp.StatusCode, string(body))
}

// Update the favicon
_, err = client.UpdateOrganization(cmd.Context(), &adminv1.UpdateOrganizationRequest{
Name: ch.Org,
FaviconAssetId: &asset.AssetId,
})
if err != nil {
return fmt.Errorf("failed to update: %w", err)
}

// Print confirmation message
ch.PrintfSuccess("Updated the favicon for %q\n", ch.Org)
return nil
},
}
cmd.Flags().SortFlags = false
cmd.Flags().StringVar(&ch.Org, "org", ch.Org, "Organization name")
cmd.Flags().StringVar(&path, "path", "", "Path to image file (PNG or JPEG)")
cmd.Flags().BoolVar(&remove, "remove", false, "Remove the current favicon")

return cmd
}
4 changes: 4 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ paths:
type: string
logoAssetId:
type: string
faviconAssetId:
type: string
billingEmail:
type: string
tags:
Expand Down Expand Up @@ -4643,6 +4645,8 @@ definitions:
type: string
logoUrl:
type: string
faviconUrl:
type: string
customDomain:
type: string
quotas:
Expand Down
Loading
Loading