mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
* chore: add /v2 to import module path go mod requires semantic versioning with versions greater than 1.x This was a mechanical update by running: ``` go install github.com/marwan-at-work/mod/cmd/mod@latest mod upgrade ``` Migrate generated files to import /v2 * Fix gen
182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
var DefaultSupportLinks = []codersdk.LinkConfig{
|
|
{
|
|
Name: "Documentation",
|
|
Target: "https://coder.com/docs/coder-oss",
|
|
Icon: "docs",
|
|
},
|
|
{
|
|
Name: "Report a bug",
|
|
Target: "https://github.com/coder/coder/issues/new?labels=needs+grooming&body={CODER_BUILD_INFO}",
|
|
Icon: "bug",
|
|
},
|
|
{
|
|
Name: "Join the Coder Discord",
|
|
Target: "https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer",
|
|
Icon: "chat",
|
|
},
|
|
}
|
|
|
|
// @Summary Get appearance
|
|
// @ID get-appearance
|
|
// @Security CoderSessionToken
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Success 200 {object} codersdk.AppearanceConfig
|
|
// @Router /appearance [get]
|
|
func (api *API) appearance(rw http.ResponseWriter, r *http.Request) {
|
|
cfg, err := api.fetchAppearanceConfig(r.Context())
|
|
if err != nil {
|
|
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: "Failed to fetch appearance config.",
|
|
Detail: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, cfg)
|
|
}
|
|
|
|
func (api *API) fetchAppearanceConfig(ctx context.Context) (codersdk.AppearanceConfig, error) {
|
|
api.entitlementsMu.RLock()
|
|
isEntitled := api.entitlements.Features[codersdk.FeatureAppearance].Entitlement == codersdk.EntitlementEntitled
|
|
api.entitlementsMu.RUnlock()
|
|
|
|
if !isEntitled {
|
|
return codersdk.AppearanceConfig{
|
|
SupportLinks: DefaultSupportLinks,
|
|
}, nil
|
|
}
|
|
|
|
var eg errgroup.Group
|
|
var logoURL string
|
|
var serviceBannerJSON string
|
|
eg.Go(func() (err error) {
|
|
logoURL, err = api.Database.GetLogoURL(ctx)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return xerrors.Errorf("get logo url: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
eg.Go(func() (err error) {
|
|
serviceBannerJSON, err = api.Database.GetServiceBanner(ctx)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return xerrors.Errorf("get service banner: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
err := eg.Wait()
|
|
if err != nil {
|
|
return codersdk.AppearanceConfig{}, err
|
|
}
|
|
|
|
cfg := codersdk.AppearanceConfig{
|
|
LogoURL: logoURL,
|
|
}
|
|
if serviceBannerJSON != "" {
|
|
err = json.Unmarshal([]byte(serviceBannerJSON), &cfg.ServiceBanner)
|
|
if err != nil {
|
|
return codersdk.AppearanceConfig{}, xerrors.Errorf(
|
|
"unmarshal json: %w, raw: %s", err, serviceBannerJSON,
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(api.DeploymentValues.Support.Links.Value) == 0 {
|
|
cfg.SupportLinks = DefaultSupportLinks
|
|
} else {
|
|
cfg.SupportLinks = api.DeploymentValues.Support.Links.Value
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func validateHexColor(color string) error {
|
|
if len(color) != 7 {
|
|
return xerrors.New("expected 7 characters")
|
|
}
|
|
if color[0] != '#' {
|
|
return xerrors.New("no # prefix")
|
|
}
|
|
_, err := hex.DecodeString(color[1:])
|
|
return err
|
|
}
|
|
|
|
// @Summary Update appearance
|
|
// @ID update-appearance
|
|
// @Security CoderSessionToken
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Tags Enterprise
|
|
// @Param request body codersdk.UpdateAppearanceConfig true "Update appearance request"
|
|
// @Success 200 {object} codersdk.UpdateAppearanceConfig
|
|
// @Router /appearance [put]
|
|
func (api *API) putAppearance(rw http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceDeploymentValues) {
|
|
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
|
Message: "Insufficient permissions to update appearance",
|
|
})
|
|
return
|
|
}
|
|
|
|
var appearance codersdk.UpdateAppearanceConfig
|
|
if !httpapi.Read(ctx, rw, r, &appearance) {
|
|
return
|
|
}
|
|
|
|
if appearance.ServiceBanner.Enabled {
|
|
if err := validateHexColor(appearance.ServiceBanner.BackgroundColor); err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: fmt.Sprintf("parse color: %+v", err),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
serviceBannerJSON, err := json.Marshal(appearance.ServiceBanner)
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
Message: fmt.Sprintf("marshal banner: %+v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
err = api.Database.UpsertServiceBanner(ctx, string(serviceBannerJSON))
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: fmt.Sprintf("database error: %+v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
err = api.Database.UpsertLogoURL(ctx, appearance.LogoURL)
|
|
if err != nil {
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
Message: fmt.Sprintf("database error: %+v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
httpapi.Write(r.Context(), rw, http.StatusOK, appearance)
|
|
}
|