diff --git a/coderd/audit.go b/coderd/audit.go
index e0f000c495a3a..5a2c41b3e5ad2 100644
--- a/coderd/audit.go
+++ b/coderd/audit.go
@@ -17,6 +17,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
@@ -193,7 +194,7 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
for _, roleName := range dblog.UserRoles {
rbacRole, _ := rbac.RoleByName(roleName)
- user.Roles = append(user.Roles, convertRole(rbacRole))
+ user.Roles = append(user.Roles, db2sdk.Role(rbacRole))
}
}
diff --git a/coderd/coderd.go b/coderd/coderd.go
index fe59fd8726467..af03f69f8a89f 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -293,11 +293,13 @@ func New(options *Options) *API {
},
)
- staticHandler := site.Handler(site.FS(), binFS, binHashes)
- // Static file handler must be wrapped with HSTS handler if the
- // StrictTransportSecurityAge is set. We only need to set this header on
- // static files since it only affects browsers.
- staticHandler = httpmw.HSTS(staticHandler, options.StrictTransportSecurityCfg)
+ staticHandler := site.New(&site.Options{
+ BinFS: binFS,
+ BinHashes: binHashes,
+ Database: options.Database,
+ SiteFS: site.FS(),
+ })
+ staticHandler.Experiments.Store(&experiments)
oauthConfigs := &httpmw.OAuth2Configs{
Github: options.GithubOAuth2Config,
@@ -313,7 +315,7 @@ func New(options *Options) *API {
ID: uuid.New(),
Options: options,
RootHandler: r,
- siteHandler: staticHandler,
+ SiteHandler: staticHandler,
HTTPAuth: &HTTPAuthorizer{
Authorizer: options.Authorizer,
Logger: options.Logger,
@@ -813,7 +815,11 @@ func New(options *Options) *API {
// By default we do not add extra websocket connections to the CSP
return []string{}
})
- r.NotFound(cspMW(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP))).ServeHTTP)
+
+ // Static file handler must be wrapped with HSTS handler if the
+ // StrictTransportSecurityAge is set. We only need to set this header on
+ // static files since it only affects browsers.
+ r.NotFound(cspMW(compressHandler(httpmw.HSTS(api.SiteHandler, options.StrictTransportSecurityCfg))).ServeHTTP)
// This must be before all middleware to improve the response time.
// So make a new router, and mount the old one as the root.
@@ -858,7 +864,8 @@ type API struct {
// RootHandler serves "/"
RootHandler chi.Router
- siteHandler http.Handler
+ // SiteHandler serves static files for the dashboard.
+ SiteHandler *site.Handler
WebsocketWaitMutex sync.Mutex
WebsocketWaitGroup sync.WaitGroup
diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go
index 465462781557e..7b3b8310eaf3c 100644
--- a/coderd/database/db2sdk/db2sdk.go
+++ b/coderd/database/db2sdk/db2sdk.go
@@ -5,8 +5,11 @@ import (
"encoding/json"
"time"
+ "github.com/google/uuid"
+
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/parameter"
+ "github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionersdk/proto"
)
@@ -100,3 +103,31 @@ func ProvisionerJobStatus(provisionerJob database.ProvisionerJob) codersdk.Provi
return codersdk.ProvisionerJobRunning
}
}
+
+func User(user database.User, organizationIDs []uuid.UUID) codersdk.User {
+ convertedUser := codersdk.User{
+ ID: user.ID,
+ Email: user.Email,
+ CreatedAt: user.CreatedAt,
+ LastSeenAt: user.LastSeenAt,
+ Username: user.Username,
+ Status: codersdk.UserStatus(user.Status),
+ OrganizationIDs: organizationIDs,
+ Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
+ AvatarURL: user.AvatarURL.String,
+ }
+
+ for _, roleName := range user.RBACRoles {
+ rbacRole, _ := rbac.RoleByName(roleName)
+ convertedUser.Roles = append(convertedUser.Roles, Role(rbacRole))
+ }
+
+ return convertedUser
+}
+
+func Role(role rbac.Role) codersdk.Role {
+ return codersdk.Role{
+ DisplayName: role.DisplayName,
+ Name: role.Name,
+ }
+}
diff --git a/coderd/members.go b/coderd/members.go
index 293f007dd400d..67ab19cf45687 100644
--- a/coderd/members.go
+++ b/coderd/members.go
@@ -8,6 +8,7 @@ import (
"golang.org/x/xerrors"
+ "github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/database"
@@ -104,7 +105,7 @@ func convertOrganizationMember(mem database.OrganizationMember) codersdk.Organiz
for _, roleName := range mem.Roles {
rbacRole, _ := rbac.RoleByName(roleName)
- convertedMember.Roles = append(convertedMember.Roles, convertRole(rbacRole))
+ convertedMember.Roles = append(convertedMember.Roles, db2sdk.Role(rbacRole))
}
return convertedMember
}
diff --git a/coderd/roles.go b/coderd/roles.go
index a067173300e43..177a5301eae00 100644
--- a/coderd/roles.go
+++ b/coderd/roles.go
@@ -55,13 +55,6 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, assignableRoles(actorRoles.Actor.Roles, roles))
}
-func convertRole(role rbac.Role) codersdk.Role {
- return codersdk.Role{
- DisplayName: role.DisplayName,
- Name: role.Name,
- }
-}
-
func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role) []codersdk.AssignableRoles {
assignable := make([]codersdk.AssignableRoles, 0)
for _, role := range roles {
diff --git a/coderd/users.go b/coderd/users.go
index 7cb05dacacd60..04dbab057ffe2 100644
--- a/coderd/users.go
+++ b/coderd/users.go
@@ -14,6 +14,7 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/coderd/database/db2sdk"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
@@ -401,7 +402,7 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
Users: []telemetry.User{telemetry.ConvertUser(user)},
})
- httpapi.Write(ctx, rw, http.StatusCreated, convertUser(user, []uuid.UUID{req.OrganizationID}))
+ httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.User(user, []uuid.UUID{req.OrganizationID}))
}
// @Summary Delete user
@@ -495,7 +496,7 @@ func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
return
}
- httpapi.Write(ctx, rw, http.StatusOK, convertUser(user, organizationIDs))
+ httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(user, organizationIDs))
}
// @Summary Update user profile
@@ -580,7 +581,7 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
return
}
- httpapi.Write(ctx, rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs))
+ httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUserProfile, organizationIDs))
}
// @Summary Suspend user account
@@ -667,7 +668,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
return
}
- httpapi.Write(ctx, rw, http.StatusOK, convertUser(suspendedUser, organizations))
+ httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(suspendedUser, organizations))
}
}
@@ -892,7 +893,7 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
return
}
- httpapi.Write(ctx, rw, http.StatusOK, convertUser(updatedUser, organizationIDs))
+ httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizationIDs))
}
// updateSiteUserRoles will ensure only site wide roles are passed in as arguments.
@@ -1087,32 +1088,11 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
}, nil)
}
-func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
- convertedUser := codersdk.User{
- ID: user.ID,
- Email: user.Email,
- CreatedAt: user.CreatedAt,
- LastSeenAt: user.LastSeenAt,
- Username: user.Username,
- Status: codersdk.UserStatus(user.Status),
- OrganizationIDs: organizationIDs,
- Roles: make([]codersdk.Role, 0, len(user.RBACRoles)),
- AvatarURL: user.AvatarURL.String,
- }
-
- for _, roleName := range user.RBACRoles {
- rbacRole, _ := rbac.RoleByName(roleName)
- convertedUser.Roles = append(convertedUser.Roles, convertRole(rbacRole))
- }
-
- return convertedUser
-}
-
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
converted := make([]codersdk.User, 0, len(users))
for _, u := range users {
userOrganizationIDs := organizationIDsByUserID[u.ID]
- converted = append(converted, convertUser(u, userOrganizationIDs))
+ converted = append(converted, db2sdk.User(u, userOrganizationIDs))
}
return converted
}
diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go
index 6f8ef8500e21f..c4dc8b34e1941 100644
--- a/enterprise/coderd/appearance.go
+++ b/enterprise/coderd/appearance.go
@@ -1,6 +1,7 @@
package coderd
import (
+ "context"
"database/sql"
"encoding/hex"
"encoding/json"
@@ -8,6 +9,7 @@ import (
"fmt"
"net/http"
+ "golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/httpapi"
@@ -41,35 +43,49 @@ var DefaultSupportLinks = []codersdk.LinkConfig{
// @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()
- ctx := r.Context()
-
if !isEntitled {
- httpapi.Write(ctx, rw, http.StatusOK, codersdk.AppearanceConfig{
+ return codersdk.AppearanceConfig{
SupportLinks: DefaultSupportLinks,
- })
- return
+ }, nil
}
- logoURL, err := api.Database.GetLogoURL(ctx)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Failed to fetch logo URL.",
- Detail: err.Error(),
- })
- return
- }
-
- serviceBannerJSON, err := api.Database.GetServiceBanner(r.Context())
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: "Failed to fetch service banner.",
- Detail: err.Error(),
- })
- return
+ 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{
@@ -78,12 +94,9 @@ func (api *API) appearance(rw http.ResponseWriter, r *http.Request) {
if serviceBannerJSON != "" {
err = json.Unmarshal([]byte(serviceBannerJSON), &cfg.ServiceBanner)
if err != nil {
- httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
- Message: fmt.Sprintf(
- "unmarshal json: %+v, raw: %s", err, serviceBannerJSON,
- ),
- })
- return
+ return codersdk.AppearanceConfig{}, xerrors.Errorf(
+ "unmarshal json: %w, raw: %s", err, serviceBannerJSON,
+ )
}
}
@@ -93,7 +106,7 @@ func (api *API) appearance(rw http.ResponseWriter, r *http.Request) {
cfg.SupportLinks = api.DeploymentValues.Support.Links.Value
}
- httpapi.Write(r.Context(), rw, http.StatusOK, cfg)
+ return cfg, nil
}
func validateHexColor(color string) error {
diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go
index 5da6f392fc431..0021fd4df63c4 100644
--- a/enterprise/coderd/coderd.go
+++ b/enterprise/coderd/coderd.go
@@ -66,6 +66,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
}()
api.AGPL.Options.SetUserGroups = api.setUserGroups
+ api.AGPL.SiteHandler.AppearanceFetcher = api.fetchAppearanceConfig
oauthConfigs := &httpmw.OAuth2Configs{
Github: options.GithubOAuth2Config,
@@ -451,6 +452,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
}
api.entitlements = entitlements
+ api.AGPL.SiteHandler.Entitlements.Store(&entitlements)
return nil
}
diff --git a/site/index.html b/site/index.html
index ac18266dcda35..61cb49be7b129 100644
--- a/site/index.html
+++ b/site/index.html
@@ -12,11 +12,14 @@
-
+
-
+
+
+
+
{
+ // Appearance is injected by the Coder server into the HTML document.
+ const appearance = document.querySelector("meta[property=appearance]")
+ if (appearance) {
+ const rawContent = appearance.getAttribute("content")
+ try {
+ return JSON.parse(rawContent as string)
+ } catch (ex) {
+ // Ignore this and fetch as normal!
+ }
+ }
+
+ return API.getAppearance()
+ },
setAppearance: (_, event) => API.updateAppearance(event.appearance),
},
},
diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts
index 132b01d496e84..8c5ce5dd8edfc 100644
--- a/site/src/xServices/auth/authXService.ts
+++ b/site/src/xServices/auth/authXService.ts
@@ -99,7 +99,22 @@ export const isAuthenticated = (data?: AuthData): data is AuthenticatedData =>
data !== undefined && "user" in data
const loadInitialAuthData = async (): Promise => {
- const authenticatedUser = await API.getAuthenticatedUser()
+ let authenticatedUser: TypesGen.User | undefined
+ // User is injected by the Coder server into the HTML document.
+ const userMeta = document.querySelector("meta[property=user]")
+ if (userMeta) {
+ const rawContent = userMeta.getAttribute("content")
+ try {
+ authenticatedUser = JSON.parse(rawContent as string) as TypesGen.User
+ } catch (ex) {
+ // Ignore this and fetch as normal!
+ }
+ }
+
+ // If we have the user from the meta tag, we can skip this!
+ if (!authenticatedUser) {
+ authenticatedUser = await API.getAuthenticatedUser()
+ }
if (authenticatedUser) {
const permissions = (await API.checkAuthorization({
diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts
index 11fb7f2a3008b..b1257cb4d18e4 100644
--- a/site/src/xServices/entitlements/entitlementsXService.ts
+++ b/site/src/xServices/entitlements/entitlementsXService.ts
@@ -58,7 +58,22 @@ export const entitlementsMachine = createMachine(
}),
},
services: {
- getEntitlements: () => API.getEntitlements(),
+ getEntitlements: async () => {
+ // Entitlements is injected by the Coder server into the HTML document.
+ const entitlements = document.querySelector(
+ "meta[property=entitlements]",
+ )
+ if (entitlements) {
+ const rawContent = entitlements.getAttribute("content")
+ try {
+ return JSON.parse(rawContent as string)
+ } catch (ex) {
+ // Ignore this and fetch as normal!
+ }
+ }
+
+ return API.getEntitlements()
+ },
},
},
)
diff --git a/site/src/xServices/experiments/experimentsMachine.ts b/site/src/xServices/experiments/experimentsMachine.ts
index 399eaf6c2650b..0c8eef6cf4a55 100644
--- a/site/src/xServices/experiments/experimentsMachine.ts
+++ b/site/src/xServices/experiments/experimentsMachine.ts
@@ -50,7 +50,20 @@ export const experimentsMachine = createMachine(
},
{
services: {
- getExperiments: getExperiments,
+ getExperiments: async () => {
+ // Experiments is injected by the Coder server into the HTML document.
+ const experiments = document.querySelector("meta[property=experiments]")
+ if (experiments) {
+ const rawContent = experiments.getAttribute("content")
+ try {
+ return JSON.parse(rawContent as string)
+ } catch (ex) {
+ // Ignore this and fetch as normal!
+ }
+ }
+
+ return getExperiments()
+ },
},
actions: {
assignExperiments: assign({