diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 93d9d69517ec9..621d57812b242 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -586,6 +586,9 @@ OIDC OPTIONS: --oidc-username-field string, $CODER_OIDC_USERNAME_FIELD (default: preferred_username) OIDC claim field to use as the username. + --oidc-logout-redirect-uri string, $CODER_OIDC_LOGOUT_URI + OIDC redirect URI after logout. + --oidc-sign-in-text string, $CODER_OIDC_SIGN_IN_TEXT (default: OpenID Connect) The text to show on the OpenID Connect sign in button. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 96a03c5b1f05e..38dd2085c0a80 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -389,6 +389,9 @@ oidc: # an insecure OIDC configuration. It is not recommended to use this flag. # (default: , type: bool) dangerousSkipIssuerChecks: false + # OIDC redirect URI after logout. + # (default: , type: string) + logoutRedirectURI: "" # Telemetry is critical to our ability to improve Coder. We strip all personal # information before sending data to our servers. Please only disable telemetry # when required by your organization's security policy. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 329951003007b..4befa051f6f20 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6057,6 +6057,31 @@ const docTemplate = `{ } } }, + "/users/oidc-logout": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get user OIDC logout URL", + "operationId": "get-user-oidc-logout-url", + "responses": { + "200": { + "description": "Returns a map containing the OIDC logout URL", + "schema": { + "$ref": "#/definitions/codersdk.OIDCLogoutResponse" + } + } + } + } + }, "/users/oidc/callback": { "get": { "security": [ @@ -12480,6 +12505,12 @@ const docTemplate = `{ "issuer_url": { "type": "string" }, + "logout_endpoint": { + "type": "string" + }, + "logout_redirect_uri": { + "type": "string" + }, "name_field": { "type": "string" }, @@ -12524,6 +12555,14 @@ const docTemplate = `{ } } }, + "codersdk.OIDCLogoutResponse": { + "type": "object", + "properties": { + "oidc_logout_url": { + "type": "string" + } + } + }, "codersdk.Organization": { "type": "object", "required": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 63b7146365d9f..2569abb935395 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5345,6 +5345,27 @@ } } }, + "/users/oidc-logout": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Users"], + "summary": "Get user OIDC logout URL", + "operationId": "get-user-oidc-logout-url", + "responses": { + "200": { + "description": "Returns a map containing the OIDC logout URL", + "schema": { + "$ref": "#/definitions/codersdk.OIDCLogoutResponse" + } + } + } + } + }, "/users/oidc/callback": { "get": { "security": [ @@ -11230,6 +11251,12 @@ "issuer_url": { "type": "string" }, + "logout_endpoint": { + "type": "string" + }, + "logout_redirect_uri": { + "type": "string" + }, "name_field": { "type": "string" }, @@ -11274,6 +11301,14 @@ } } }, + "codersdk.OIDCLogoutResponse": { + "type": "object", + "properties": { + "oidc_logout_url": { + "type": "string" + } + } + }, "codersdk.Organization": { "type": "object", "required": ["created_at", "id", "is_default", "updated_at"], diff --git a/coderd/coderd.go b/coderd/coderd.go index be558797389b9..4b18669ba3c22 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1106,6 +1106,7 @@ func New(options *Options) *API { r.Post("/", api.postUser) r.Get("/", api.users) r.Post("/logout", api.postLogout) + r.Get("/oidc-logout", api.userOIDCLogoutURL) // These routes query information about site wide roles. r.Route("/roles", func(r chi.Router) { r.Get("/", api.AssignableSiteRoles) diff --git a/coderd/userauth.go b/coderd/userauth.go index c5e95e44998b2..fe81170ba1703 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -3,10 +3,13 @@ package coderd import ( "context" "database/sql" + "encoding/base64" "errors" "fmt" + "io" "net/http" "net/mail" + "net/url" "sort" "strconv" "strings" @@ -734,6 +737,197 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) { }) } +// getDiscoveryEndpoints will return endpoints for end session and revocation +func (api *API) getDiscoveryEndpoints() (endSessionEndpoint string, revocationEndpoint string, err error) { + oidcProvider := api.OIDCConfig.Provider + + var discoveryConfig struct { + EndSessionEndpoint string `json:"end_session_endpoint"` + RevocationEndpoint string `json:"revocation_endpoint"` + } + + // Extract endpoints + if err := oidcProvider.Claims(&discoveryConfig); err != nil { + return "", "", xerrors.Errorf("failed to extract endpoints from OIDC provider discovery claims: %w", err) + } + + return discoveryConfig.EndSessionEndpoint, discoveryConfig.RevocationEndpoint, nil +} + +// revokeOAuthToken will revoke a particular token +func (api *API) revokeOAuthToken(ctx context.Context, token string, revocationEndpoint string) error { + logger := api.Logger.Named(userAuthLoggerName) + + if token == "" || revocationEndpoint == "" { + logger.Warn(ctx, "skip OAuth token revocation") + return nil + } + + dvOIDC := api.DeploymentValues.OIDC + oidcClientID := dvOIDC.ClientID.Value() + oidcClientSecret := dvOIDC.ClientSecret.Value() + + if oidcClientID == "" || oidcClientSecret == "" { + return xerrors.New("missing required configs for revocation (endpoint, client ID, or secret)") + } + + data := url.Values{} + data.Set("token", token) + + revokeReq, err := http.NewRequestWithContext(ctx, http.MethodPost, revocationEndpoint, strings.NewReader(data.Encode())) + if err != nil { + return xerrors.Errorf("failed to create revoke request object: %w", err) + } + + revokeReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + auth := base64.StdEncoding.EncodeToString([]byte(oidcClientID + ":" + oidcClientSecret)) + revokeReq.Header.Set("Authorization", "Basic "+auth) + + httpClient := &http.Client{} + resp, err := httpClient.Do(revokeReq) + if err != nil { + return xerrors.Errorf("failed to send revoke request to %s: %w", revocationEndpoint, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBodyBytes, _ := io.ReadAll(resp.Body) + respBodyStr := string(respBodyBytes) + + logger.Warn(ctx, "failed to request OAuth token revocation", + slog.F("status_code", resp.StatusCode), + slog.F("response_body", respBodyStr), + slog.F("endpoint", revocationEndpoint), + slog.F("client_id", oidcClientID), + ) + + return xerrors.Errorf("failed to revoke with status %d: %s", resp.StatusCode, respBodyStr) + } + + logger.Info(ctx, "success to revoke OAuth token", slog.F("status_code", resp.StatusCode)) + return nil // Success +} + +// Returns URL for the OIDC logout after token revocation. +// +// @Summary Get user OIDC logout URL +// @ID get-user-oidc-logout-url +// @Security CoderSessionToken +// @Produce json +// @Tags Users +// @Success 200 {object} codersdk.OIDCLogoutResponse "Returns a map containing the OIDC logout URL" +// @Router /users/oidc-logout [get] +func (api *API) userOIDCLogoutURL(rw http.ResponseWriter, r *http.Request) { + logger := api.Logger.Named(userAuthLoggerName) + ctx := r.Context() + + // Check if OIDC is configured + if api.OIDCConfig == nil || api.OIDCConfig.Provider == nil { + logger.Warn(ctx, "unable to support OIDC logout with current configuration") + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to retrieve OIDC configuration.", + }) + return + } + + // Get logged-in user + apiKey := httpmw.APIKey(r) + user, err := api.Database.GetUserByID(ctx, apiKey.UserID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ + Message: "Failed to retrieve user information.", + }) + return + } + + // Default response: empty URL if OIDC logout is not supported + response := codersdk.OIDCLogoutResponse{URL: ""} + + // Retrieve the user's OAuthAccessToken for logout + // nolint:gocritic // We only can get user link by user ID and login type with the system auth. + link, err := api.Database.GetUserLinkByUserIDLoginType(dbauthz.AsSystemRestricted(ctx), + database.GetUserLinkByUserIDLoginTypeParams{ + UserID: user.ID, + LoginType: user.LoginType, + }) + if err != nil { + if xerrors.Is(err, sql.ErrNoRows) { + logger.Warn(ctx, "no OIDC link found for this user") + httpapi.Write(ctx, rw, http.StatusOK, response) + return + } + + logger.Error(ctx, "failed to retrieve OIDC user link", "error", err) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to retrieve user authentication data.", + Detail: err.Error(), + }) + return + } + + accessToken := link.OAuthAccessToken + refreshToken := link.OAuthRefreshToken + + // Retrieve OIDC environment variables + dvOIDC := api.DeploymentValues.OIDC + oidcClientID := dvOIDC.ClientID.Value() + logoutURI := dvOIDC.LogoutRedirectURI.Value() + + endSessionEndpoint, revocationEndpoint, err := api.getDiscoveryEndpoints() + if err != nil { + logger.Error(ctx, "failed to get OIDC discovery endpoints", slog.Error(err)) + + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to process OIDC configuration.", + }) + return + } + + // Perform token revocation first + err = api.revokeOAuthToken(ctx, refreshToken, revocationEndpoint) + if err != nil { + // Do not return since this step is optional + logger.Warn(ctx, "failed to revoke OAuth token during logout", slog.Error(err)) + } + + if endSessionEndpoint == "" { + logger.Warn(ctx, "missing OIDC logout endpoint") + httpapi.Write(ctx, rw, http.StatusOK, response) + return + } + + // Construct OIDC Logout URL + logoutURL, err := url.Parse(endSessionEndpoint) + if err != nil { + logger.Error(ctx, "failed to parse OIDC endpoint", "error", err) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Invalid OIDC endpoint.", + Detail: err.Error(), + }) + return + } + + // Build parameters + q := url.Values{} + + if accessToken != "" { + q.Set("id_token_hint", accessToken) + } + if oidcClientID != "" { + q.Set("client_id", oidcClientID) + } + // post_logout_redirect_uri? + if logoutURI != "" { + q.Set("logout_uri", logoutURI) + } + + logoutURL.RawQuery = q.Encode() + + // Return full logout URL + response.URL = logoutURL.String() + httpapi.Write(ctx, rw, http.StatusOK, response) +} + // GithubOAuth2Team represents a team scoped to an organization. type GithubOAuth2Team struct { Organization string diff --git a/codersdk/deployment.go b/codersdk/deployment.go index e1c0b977c00d2..f507c8d477f09 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -543,6 +543,8 @@ type OIDCConfig struct { IconURL serpent.URL `json:"icon_url" typescript:",notnull"` SignupsDisabledText serpent.String `json:"signups_disabled_text" typescript:",notnull"` SkipIssuerChecks serpent.Bool `json:"skip_issuer_checks" typescript:",notnull"` + LogoutEndpoint serpent.String `json:"logout_endpoint" typescript:",notnull"` + LogoutRedirectURI serpent.String `json:"logout_redirect_uri" typescript:",notnull"` } type TelemetryConfig struct { @@ -1917,6 +1919,16 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Group: &deploymentGroupOIDC, YAML: "dangerousSkipIssuerChecks", }, + { + Name: "OIDC logout redirect URI", + Description: "OIDC redirect URI after logout.", + Flag: "oidc-logout-redirect-uri", + Env: "CODER_OIDC_LOGOUT_URI", + Default: "", + Value: &c.OIDC.LogoutRedirectURI, + Group: &deploymentGroupOIDC, + YAML: "logoutRedirectURI", + }, // Telemetry settings telemetryEnable, { diff --git a/codersdk/users.go b/codersdk/users.go index 4dbdc0d4e4f91..c68b49b61dfcd 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -300,6 +300,11 @@ type UserParameter struct { Value string `json:"value"` } +// OIDCLogoutResponse represents the response for an OIDC logout request +type OIDCLogoutResponse struct { + URL string `json:"oidc_logout_url"` +} + // UserAutofillParameters returns all recently used parameters for the given user. func (c *Client) UserAutofillParameters(ctx context.Context, user string, templateID uuid.UUID) ([]UserParameter, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/autofill-parameters?template_id=%s", user, templateID), nil) diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 66e85f3f6978a..368a7ac50325d 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -365,6 +365,8 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "ignore_email_verified": true, "ignore_user_info": true, "issuer_url": "string", + "logout_endpoint": "string", + "logout_redirect_uri": "string", "name_field": "string", "organization_assign_default": true, "organization_field": "string", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 20ed37f81f7f7..52f854e58fbab 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2008,6 +2008,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "ignore_email_verified": true, "ignore_user_info": true, "issuer_url": "string", + "logout_endpoint": "string", + "logout_redirect_uri": "string", "name_field": "string", "organization_assign_default": true, "organization_field": "string", @@ -2478,6 +2480,8 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "ignore_email_verified": true, "ignore_user_info": true, "issuer_url": "string", + "logout_endpoint": "string", + "logout_redirect_uri": "string", "name_field": "string", "organization_assign_default": true, "organization_field": "string", @@ -3973,6 +3977,8 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "ignore_email_verified": true, "ignore_user_info": true, "issuer_url": "string", + "logout_endpoint": "string", + "logout_redirect_uri": "string", "name_field": "string", "organization_assign_default": true, "organization_field": "string", @@ -3994,37 +4000,54 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------------|----------------------------------|----------|--------------|----------------------------------------------------------------------------------| -| `allow_signups` | boolean | false | | | -| `auth_url_params` | object | false | | | -| `client_cert_file` | string | false | | | -| `client_id` | string | false | | | -| `client_key_file` | string | false | | Client key file & ClientCertFile are used in place of ClientSecret for PKI auth. | -| `client_secret` | string | false | | | -| `email_domain` | array of string | false | | | -| `email_field` | string | false | | | -| `group_allow_list` | array of string | false | | | -| `group_auto_create` | boolean | false | | | -| `group_mapping` | object | false | | | -| `group_regex_filter` | [serpent.Regexp](#serpentregexp) | false | | | -| `groups_field` | string | false | | | -| `icon_url` | [serpent.URL](#serpenturl) | false | | | -| `ignore_email_verified` | boolean | false | | | -| `ignore_user_info` | boolean | false | | | -| `issuer_url` | string | false | | | -| `name_field` | string | false | | | -| `organization_assign_default` | boolean | false | | | -| `organization_field` | string | false | | | -| `organization_mapping` | object | false | | | -| `scopes` | array of string | false | | | -| `sign_in_text` | string | false | | | -| `signups_disabled_text` | string | false | | | -| `skip_issuer_checks` | boolean | false | | | -| `user_role_field` | string | false | | | -| `user_role_mapping` | object | false | | | -| `user_roles_default` | array of string | false | | | -| `username_field` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------------------------|----------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `allow_signups` | boolean | false | | | +| `auth_url_params` | object | false | | | +| `client_cert_file` | string | false | | | +| `client_id` | string | false | | | +| `client_key_file` | string | false | | Client key file & ClientCertFile are used in place of ClientSecret for PKI auth. | +| `client_secret` | string | false | | | +| `email_domain` | array of string | false | | | +| `email_field` | string | false | | | +| `group_allow_list` | array of string | false | | | +| `group_auto_create` | boolean | false | | | +| `group_mapping` | object | false | | | +| `group_regex_filter` | [serpent.Regexp](#serpentregexp) | false | | | +| `groups_field` | string | false | | | +| `icon_url` | [serpent.URL](#serpenturl) | false | | | +| `ignore_email_verified` | boolean | false | | | +| `ignore_user_info` | boolean | false | | Ignore user info & UserInfoFromAccessToken are mutually exclusive. Only 1 can be set to true. Ideally this would be an enum with 3 states, ['none', 'userinfo', 'access_token']. However, for backward compatibility, `ignore_user_info` must remain. And `access_token` is a niche, non-spec compliant edge case. So it's use is rare, and should not be advised. | +| `issuer_url` | string | false | | | +| `logout_endpoint` | string | false | | | +| `logout_redirect_uri` | string | false | | | +| `name_field` | string | false | | | +| `organization_assign_default` | boolean | false | | | +| `organization_field` | string | false | | | +| `organization_mapping` | object | false | | | +| `scopes` | array of string | false | | | +| `sign_in_text` | string | false | | | +| `signups_disabled_text` | string | false | | | +| `skip_issuer_checks` | boolean | false | | | +| `source_user_info_from_access_token` | boolean | false | | Source user info from access token as mentioned above is an edge case. This allows sourcing the user_info from the access token itself instead of a user_info endpoint. This assumes the access token is a valid JWT with a set of claims to be merged with the id_token. | +| `user_role_field` | string | false | | | +| `user_role_mapping` | object | false | | | +| `user_roles_default` | array of string | false | | | +| `username_field` | string | false | | | + +## codersdk.OIDCLogoutResponse + +```json +{ + "oidc_logout_url": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-------------------|--------|----------|--------------|-------------| +| `oidc_logout_url` | string | false | | | ## codersdk.Organization diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index d8aac77cfa83b..96a260d36d659 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -337,6 +337,38 @@ curl -X GET http://coder-server:8080/api/v2/users/oauth2/github/callback \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Returns URL for the OIDC logout + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/users/oidc-logout \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /users/oidc-logout` + +### Example responses + +> 200 Response + +```json +{ + "oidc_logout_url": "string" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|----------------------------------------------|----------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | Returns a map containing the OIDC logout URL | [codersdk.OIDCLogoutResponse](schemas.md#codersdkoidclogoutresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +>>>>>>> c0bbf7105 (chore: add auto generated files) ## OpenID Connect Callback ### Code samples diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 98cb2a90c20da..1eb27d03e6e27 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -683,6 +683,16 @@ The custom text to show on the error page informing about disabled OIDC signups. OIDC issuer urls must match in the request, the id_token 'iss' claim, and in the well-known configuration. This flag disables that requirement, and can lead to an insecure OIDC configuration. It is not recommended to use this flag. +### --oidc-logout-redirect-uri + +| | | +|-------------|-------------------------------------| +| Type | string | +| Environment | $CODER_OIDC_LOGOUT_URI | +| YAML | oidc.logoutRedirectURI | + +OIDC redirect URI after logout. + ### --telemetry | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index ebaf1a5ac0bbd..4d78d09278b3a 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -587,6 +587,9 @@ OIDC OPTIONS: --oidc-username-field string, $CODER_OIDC_USERNAME_FIELD (default: preferred_username) OIDC claim field to use as the username. + --oidc-logout-redirect-uri string, $CODER_OIDC_LOGOUT_URI + OIDC redirect URI after logout. + --oidc-sign-in-text string, $CODER_OIDC_SIGN_IN_TEXT (default: OpenID Connect) The text to show on the OpenID Connect sign in button. diff --git a/site/index.html b/site/index.html index fff26338b21aa..e7e9ca49b7daa 100644 --- a/site/index.html +++ b/site/index.html @@ -25,31 +25,31 @@ - + />--> - + />--> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 26491efb10565..3fef19ebefdd9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -430,7 +430,28 @@ class ApiMethods { }; logout = async (): Promise => { - return this.axios.post("/api/v2/users/logout"); + try { + // Fetch the stored ID token from the backend + const response = await this.axios.get("/api/v2/users/oidc-logout"); + + // Redirect to OIDC logout after Coder logout + if (response.data.oidc_logout_url) { + // Coder session logout + await this.axios.post("/api/v2/users/logout"); + + // OIDC logout + window.location.href = response.data.oidc_logout_url; + } else { + // Redirect normally if no token is available + console.warn( + "No ID token found, continuing logout without OIDC logout.", + ); + return this.axios.post("/api/v2/users/logout"); + } + } catch (error) { + console.error("Logout failed", error); + return this.axios.post("/api/v2/users/logout"); + } }; getAuthenticatedUser = async () => { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index de879ee23daa5..edb4b78eb6914 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1407,6 +1407,13 @@ export interface OIDCConfig { readonly icon_url: string; readonly signups_disabled_text: string; readonly skip_issuer_checks: boolean; + readonly logout_endpoint: string; + readonly logout_redirect_uri: string; +} + +// From codersdk/users.go +export interface OIDCLogoutResponse { + readonly oidc_logout_url: string; } // From codersdk/organizations.go diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index d5ee661025f47..e93c7e7218b0f 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -9,6 +9,7 @@ import { DeploymentDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; import { UserDropdown } from "./UserDropdown/UserDropdown"; +import { useTheme } from "@emotion/react"; export interface NavbarViewProps { logo_url?: string; @@ -41,14 +42,25 @@ export const NavbarView: FC = ({ canViewAuditLog, proxyContextValue, }) => { + const theme = useTheme(); + + const colorTheme = theme.palette.mode; + const LogoHEaaNDark = "/favicons/heaan-dark.svg"; + const LogoHEaaNLight = "/favicons/heaan-light.svg"; + return (
- {logo_url ? ( + HEaaN-logo + {/*{logo_url ? ( ) : ( - )} + )}*/} diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index 90ea1dab74a67..df9893811caf5 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -95,7 +95,7 @@ export const UserDropdownContent: FC = ({ {Language.signOutLabel} - {supportLinks && ( + {/*{supportLinks && ( <> {supportLinks.map((link) => ( @@ -113,7 +113,7 @@ export const UserDropdownContent: FC = ({ ))} - )} + )}*/} diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index 9a367c1c13801..f490086ee9bde 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -69,6 +69,9 @@ export const LoginPage: FC = () => { /> ); } + } else { + window.location.replace(`https://heaan.io`); + return null; } if (isConfiguringTheFirstUser) { diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx index ea3b150d9844e..9734167e259df 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx @@ -70,7 +70,7 @@ export const AccountForm: FC = ({ onChange={onChangeTrimmed(form)} aria-disabled={!editable} autoComplete="username" - disabled={!editable} + disabled fullWidth label={Language.usernameLabel} /> @@ -83,13 +83,14 @@ export const AccountForm: FC = ({ }} label={Language.nameLabel} helperText='The human-readable name is optional and can be accessed in a template via the "data.coder_workspace_owner.me.full_name" property.' + disabled /> -
+ {/*
{Language.updateSettings} -
+
*/} ); diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index b3f4a76cd4b3d..87bde9634657f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -408,6 +408,7 @@ const WarningDialog: FC< // You can see the favicon designs here: https://www.figma.com/file/YIGBkXUcnRGz2ZKNmLaJQf/Coder-v2-Design?node-id=560%3A620 type FaviconType = + "heaan" | "favicon" | "favicon-success" | "favicon-error" @@ -417,26 +418,26 @@ type FaviconType = const getFaviconByStatus = (build: TypesGen.WorkspaceBuild): FaviconType => { switch (build.status) { case undefined: - return "favicon"; + return "heaan"; case "running": - return "favicon-success"; + return "heaan"; case "starting": - return "favicon-running"; + return "heaan"; case "stopping": - return "favicon-running"; + return "heaan"; case "stopped": - return "favicon"; + return "heaan"; case "deleting": - return "favicon"; + return "heaan"; case "deleted": - return "favicon"; + return "heaan"; case "canceling": - return "favicon-warning"; + return "heaan"; case "canceled": - return "favicon"; + return "heaan"; case "failed": - return "favicon-error"; + return "heaan"; case "pending": - return "favicon"; + return "heaan"; } }; diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index 3d63b9ac81b5a..54de2f4bf911e 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -21,6 +21,7 @@ "confluence.svg", "container.svg", "cpp.svg", + "cryptolab_symbol.svg", "cursor.svg", "database.svg", "datagrip.svg", @@ -49,6 +50,8 @@ "go.svg", "goland.svg", "google.svg", + "heaan-dark.svg", + "heaan-light.svg", "image.svg", "intellij.svg", "java.svg", diff --git a/site/static/favicons/heaan-dark.svg b/site/static/favicons/heaan-dark.svg new file mode 100644 index 0000000000000..c05fbc7dc0dc3 --- /dev/null +++ b/site/static/favicons/heaan-dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/site/static/favicons/heaan-light.svg b/site/static/favicons/heaan-light.svg new file mode 100644 index 0000000000000..b2df44b83504f --- /dev/null +++ b/site/static/favicons/heaan-light.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/site/static/icon/heaan-dark.svg b/site/static/icon/heaan-dark.svg new file mode 100644 index 0000000000000..c05fbc7dc0dc3 --- /dev/null +++ b/site/static/icon/heaan-dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/site/static/icon/heaan-light.svg b/site/static/icon/heaan-light.svg new file mode 100644 index 0000000000000..b2df44b83504f --- /dev/null +++ b/site/static/icon/heaan-light.svg @@ -0,0 +1,8 @@ + + + + + + + +