Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
}

httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AuthMethods{
TermsOfServiceLink: api.DeploymentValues.TermsOfServiceLink.Value(),
Password: codersdk.AuthMethod{
Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(),
},
Expand All @@ -486,7 +487,7 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {

// @Summary OAuth 2.0 GitHub Callback
// @ID oauth-20-github-callback
// @Security CoderSessionToken
// @Security CoderSessionTokens
// @Tags Users
// @Success 307
// @Router /users/oauth2/github/callback [get]
Expand Down
9 changes: 9 additions & 0 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ type DeploymentValues struct {
AllowWorkspaceRenames serpent.Bool `json:"allow_workspace_renames,omitempty" typescript:",notnull"`
Healthcheck HealthcheckConfig `json:"healthcheck,omitempty" typescript:",notnull"`
CLIUpgradeMessage serpent.String `json:"cli_upgrade_message,omitempty" typescript:",notnull"`
TermsOfServiceLink serpent.String `json:"terms_of_service_link,omitempty" typescript:",notnull"`

Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
Expand Down Expand Up @@ -1683,6 +1684,14 @@ when required by your organization's security policy.`,
YAML: "secureAuthCookie",
Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"),
},
{
Name: "Terms of Service Link",
Description: "A link to an external Terms of Service that must be accepted by users when logging in.",
Flag: "terms-of-service-link",
Env: "CODER_TERMS_OF_SERVICE_LINK",
YAML: "termsOfServiceLink",
Value: &c.TermsOfServiceLink,
},
{
Name: "Strict-Transport-Security",
Description: "Controls if the 'Strict-Transport-Security' header is set on all static file responses. " +
Expand Down
7 changes: 4 additions & 3 deletions codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,10 @@ type CreateOrganizationRequest struct {

// AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc.
type AuthMethods struct {
Password AuthMethod `json:"password"`
Github AuthMethod `json:"github"`
OIDC OIDCAuthMethod `json:"oidc"`
TermsOfServiceLink string `json:"terms_of_service_link,omitempty"`
Password AuthMethod `json:"password"`
Github AuthMethod `json:"github"`
OIDC OIDCAuthMethod `json:"oidc"`
}

type AuthMethod struct {
Expand Down
2 changes: 2 additions & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions site/src/pages/LoginPage/LoginPageView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
MockAuthMethodsAll,
MockAuthMethodsExternal,
MockAuthMethodsPasswordOnly,
MockAuthMethodsPasswordTermsOfService,
mockApiError,
} from "testHelpers/entities";
import { LoginPageView } from "./LoginPageView";
Expand Down Expand Up @@ -33,6 +34,12 @@ export const WithAllAuthMethods: Story = {
},
};

export const WithTermsOfService: Story = {
args: {
authMethods: MockAuthMethodsPasswordTermsOfService,
},
};

export const AuthError: Story = {
args: {
error: mockApiError({
Expand All @@ -53,6 +60,7 @@ export const ExternalAuthError: Story = {

export const LoadingAuthMethods: Story = {
args: {
isLoading: true,
authMethods: undefined,
},
};
Expand Down
71 changes: 51 additions & 20 deletions site/src/pages/LoginPage/SignInForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { Interpolation, Theme } from "@emotion/react";
import type { FC, ReactNode } from "react";
import Checkbox from "@mui/material/Checkbox";
import { type FC, type ReactNode, useState } from "react";
import type { AuthMethods } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { getApplicationName } from "utils/appearance";
import { OAuthSignInForm } from "./OAuthSignInForm";
import { PasswordSignInForm } from "./PasswordSignInForm";
import { Link } from "@mui/material";

export const Language = {
emailLabel: "Email",
Expand Down Expand Up @@ -83,6 +85,10 @@ export const SignInForm: FC<SignInFormProps> = ({
const passwordEnabled = authMethods?.password.enabled ?? true;
const applicationName = getApplicationName();

const [tosAccepted, setTosAccepted] = useState(false);
const termsOfServiceAcceptanceRequired =
authMethods?.terms_of_service_link && !tosAccepted;

return (
<div css={styles.root}>
<h1 css={styles.title}>{applicationName}</h1>
Expand All @@ -99,28 +105,53 @@ export const SignInForm: FC<SignInFormProps> = ({
</div>
)}

{oAuthEnabled && (
<OAuthSignInForm
isSigningIn={isSigningIn}
redirectTo={redirectTo}
authMethods={authMethods}
/>
)}
{!termsOfServiceAcceptanceRequired && (
<>
{oAuthEnabled && (
<OAuthSignInForm
isSigningIn={isSigningIn}
redirectTo={redirectTo}
authMethods={authMethods}
/>
)}

{passwordEnabled && oAuthEnabled && (
<div css={styles.divider}>
<div css={styles.dividerLine} />
<div css={styles.dividerLabel}>Or</div>
<div css={styles.dividerLine} />
</div>
{passwordEnabled && oAuthEnabled && (
<div css={styles.divider}>
<div css={styles.dividerLine} />
<div css={styles.dividerLabel}>or</div>
<div css={styles.dividerLine} />
</div>
)}

{passwordEnabled && (
<PasswordSignInForm
onSubmit={onSubmit}
autoFocus={!oAuthEnabled}
isSigningIn={isSigningIn}
/>
)}
</>
)}

{passwordEnabled && (
<PasswordSignInForm
onSubmit={onSubmit}
autoFocus={!oAuthEnabled}
isSigningIn={isSigningIn}
/>
{authMethods?.terms_of_service_link && (
<div css={{ paddingTop: 8, fontSize: 14 }}>
<label>
<Checkbox
size="small"
checked={tosAccepted}
onChange={(event) => setTosAccepted(event.target.checked)}
/>
I agree to the{" "}
<Link
href={authMethods.terms_of_service_link}
target="_blank"
rel="noreferrer"
>
Terms of Service
</Link>
.
</label>
</div>
)}

{!passwordEnabled && !oAuthEnabled && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,7 @@ export const SingleSignOnSection: FC<SingleSignOnSectionProps> = ({
}) => {
const theme = useTheme();

const authList = Object.values(
authMethods,
) as (typeof authMethods)[keyof typeof authMethods][];
const noSsoEnabled = !authList.some((method) => method.enabled);
const noSsoEnabled = !authMethods.github.enabled && !authMethods.oidc.enabled;

return (
<>
Expand Down
7 changes: 7 additions & 0 deletions site/src/testHelpers/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,13 @@ export const MockAuthMethodsPasswordOnly: TypesGen.AuthMethods = {
oidc: { enabled: false, signInText: "", iconUrl: "" },
};

export const MockAuthMethodsPasswordTermsOfService: TypesGen.AuthMethods = {
terms_of_service_link: "https://www.youtube.com/watch?v=C2f37Vb2NAE",
password: { enabled: true },
github: { enabled: false },
oidc: { enabled: false, signInText: "", iconUrl: "" },
};

export const MockAuthMethodsExternal: TypesGen.AuthMethods = {
password: { enabled: false },
github: { enabled: true },
Expand Down