Skip to content

Commit

Permalink
Upload favicon backend support (#6606)
Browse files Browse the repository at this point in the history
  • Loading branch information
AdityaHegde authored Feb 10, 2025
1 parent edef8a1 commit fd0878b
Show file tree
Hide file tree
Showing 18 changed files with 3,928 additions and 3,700 deletions.
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":
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

1 comment on commit fd0878b

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Please sign in to comment.