From cd99956995ce4caca72e3a3e55c053ec8ae8fe77 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Thu, 12 Jun 2025 19:17:53 +0200 Subject: [PATCH 1/4] feat: add comprehensive login page customization options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CLI arguments and environment variables to customize all login page elements: - Login title, subtitle, and welcome text - Password field placeholder and submit button text - Password instruction messages (config file, env var, hashed) - Error messages (rate limit, missing/incorrect password) New CLI options: --login-title, --login-below, --password-placeholder, --submit-text --login-password-msg, --login-env-password-msg, --login-hashed-password-msg --login-rate-limit-msg, --missing-password-msg, --incorrect-password-msg New environment variables: CS_LOGIN_TITLE, CS_LOGIN_BELOW, CS_PASSWORD_PLACEHOLDER, CS_SUBMIT_TEXT CS_LOGIN_PASSWORD_MSG, CS_LOGIN_ENV_PASSWORD_MSG, CS_LOGIN_HASHED_PASSWORD_MSG CS_LOGIN_RATE_LIMIT_MSG, CS_MISSING_PASSWORD_MSG, CS_INCORRECT_PASSWORD_MSG Features: - Full backwards compatibility with existing --app-name/--welcome-text - HTML escaping for security (prevents XSS) - Config file support (YAML) - Priority: CLI args > env vars > config file > defaults - Internationalization preserved for non-customized messages Perfect for Docker deployments and corporate branding. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/FAQ.md | 34 +++++++ docs/README.md | 2 + docs/customization.md | 147 ++++++++++++++++++++++++++++ docs/install.md | 24 +++++ src/node/cli.ts | 91 +++++++++++++++++ src/node/routes/login.ts | 31 +++--- test/unit/node/cli.test.ts | 30 ++++++ test/unit/node/routes/login.test.ts | 65 ++++++++++++ 8 files changed, 413 insertions(+), 11 deletions(-) create mode 100644 docs/customization.md diff --git a/docs/FAQ.md b/docs/FAQ.md index c46c003b8800..0f1e78f58bb6 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -81,6 +81,40 @@ You can change the config file's location using the `--config` flag or The default location respects `$XDG_CONFIG_HOME`. +### Login page customization + +You can customize the login page appearance using CLI flags or environment variables: + +**CLI flags:** +```bash +code-server --login-title "My Code Server" \ + --login-env-password-msg "Password set via environment" \ + --password-placeholder "Enter password" \ + --submit-text "LOGIN" +``` + +**Environment variables:** +```bash +export CS_LOGIN_TITLE="My Code Server" +export CS_LOGIN_ENV_PASSWORD_MSG="Password set via environment" +export CS_PASSWORD_PLACEHOLDER="Enter password" +export CS_SUBMIT_TEXT="LOGIN" +code-server +``` + +**Config file:** +```yaml +bind-addr: 127.0.0.1:8080 +auth: password +password: your-password +login-title: "My Code Server" +login-env-password-msg: "Password set via environment" +password-placeholder: "Enter password" +submit-text: "LOGIN" +``` + +CLI flags take priority over environment variables, which take priority over config file settings. + ## How do I make my keyboard shortcuts work? Many shortcuts will not work by default, since they'll be "caught" by the browser. diff --git a/docs/README.md b/docs/README.md index 5724c804c087..e80b7bfb23be 100644 --- a/docs/README.md +++ b/docs/README.md @@ -61,6 +61,8 @@ code-server. We also have an in-depth [setup and configuration](https://coder.com/docs/code-server/latest/guide) guide. +You can also customize the login page appearance - see our [customization guide](./customization.md). + ## Questions? See answers to [frequently asked diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 000000000000..0dd8d9d4e1c0 --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,147 @@ +# Login Page Customization + +code-server allows you to customize the login page appearance and messages through CLI arguments, environment variables, or configuration files. + +## Available Customization Options + +### Branding and Appearance +- **Login Title**: Customize the main title on the login page +- **Welcome Text**: Set custom welcome message +- **App Name**: Change the application branding throughout the interface + +### Login Messages +- **Password Instructions**: Customize the message explaining where to find the password +- **Environment Password Message**: Custom message when password is set via `$PASSWORD` +- **Hashed Password Message**: Custom message when password is set via `$HASHED_PASSWORD` + +### Form Elements +- **Password Placeholder**: Custom placeholder text for the password field +- **Submit Button**: Custom text for the login button + +### Error Messages +- **Rate Limit Message**: Custom message when login attempts are rate limited +- **Missing Password**: Custom message for empty password submissions +- **Incorrect Password**: Custom message for wrong password attempts + +## Configuration Methods + +### CLI Arguments + +```bash +code-server \ + --app-name "My Development Server" \ + --welcome-text "Welcome to the development environment" \ + --login-title "Secure Access Portal" \ + --login-below "Please authenticate to continue" \ + --password-placeholder "Enter your access code" \ + --submit-text "AUTHENTICATE" \ + --login-env-password-msg "Access code provided via environment variable" \ + --login-rate-limit-msg "Too many attempts. Please wait before trying again." \ + --missing-password-msg "Access code is required" \ + --incorrect-password-msg "Invalid access code" +``` + +### Environment Variables + +Perfect for Docker deployments and containerized environments: + +```bash +# Basic branding +export CS_APP_NAME="My Development Server" +export CS_WELCOME_TEXT="Welcome to the development environment" + +# Login page customization +export CS_LOGIN_TITLE="Secure Access Portal" +export CS_LOGIN_BELOW="Please authenticate to continue" +export CS_PASSWORD_PLACEHOLDER="Enter your access code" +export CS_SUBMIT_TEXT="AUTHENTICATE" + +# Message customization +export CS_LOGIN_ENV_PASSWORD_MSG="Access code provided via environment variable" +export CS_LOGIN_RATE_LIMIT_MSG="Too many attempts. Please wait before trying again." +export CS_MISSING_PASSWORD_MSG="Access code is required" +export CS_INCORRECT_PASSWORD_MSG="Invalid access code" + +code-server +``` + +### Configuration File + +Add to your `~/.config/code-server/config.yaml`: + +```yaml +bind-addr: 127.0.0.1:8080 +auth: password +password: your-password + +# Branding +app-name: "My Development Server" +welcome-text: "Welcome to the development environment" + +# Login page +login-title: "Secure Access Portal" +login-below: "Please authenticate to continue" +password-placeholder: "Enter your access code" +submit-text: "AUTHENTICATE" + +# Messages +login-env-password-msg: "Access code provided via environment variable" +login-rate-limit-msg: "Too many attempts. Please wait before trying again." +missing-password-msg: "Access code is required" +incorrect-password-msg: "Invalid access code" +``` + +## Docker Examples + +### Basic Docker Deployment with Customization + +```bash +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -e "CS_LOGIN_TITLE=Development Environment" \ + -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured in container environment" \ + -e "CS_PASSWORD_PLACEHOLDER=Enter development password" \ + -e "CS_SUBMIT_TEXT=ACCESS ENVIRONMENT" \ + codercom/code-server:latest +``` + +### Corporate Branding Example + +```bash +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -e "CS_APP_NAME=ACME Corporation Dev Portal" \ + -e "CS_LOGIN_TITLE=ACME Development Portal" \ + -e "CS_LOGIN_BELOW=Enter your corporate credentials" \ + -e "CS_PASSWORD_PLACEHOLDER=Corporate Password" \ + -e "CS_SUBMIT_TEXT=SIGN IN" \ + -e "CS_LOGIN_ENV_PASSWORD_MSG=Password managed by IT department" \ + codercom/code-server:latest +``` + +## Priority Order + +Settings are applied in the following priority order (highest to lowest): + +1. **CLI arguments** - Highest priority +2. **Environment variables** - Medium priority +3. **Config file** - Lowest priority + +This allows you to set defaults in your config file and override them with environment variables or CLI arguments as needed. + +## Complete Reference + +| CLI Argument | Environment Variable | Description | +|--------------|---------------------|-------------| +| `--app-name` | `CS_APP_NAME` | Application name used throughout the interface | +| `--welcome-text` | `CS_WELCOME_TEXT` | Welcome message on login page | +| `--login-title` | `CS_LOGIN_TITLE` | Main title on login page | +| `--login-below` | `CS_LOGIN_BELOW` | Text below the login title | +| `--password-placeholder` | `CS_PASSWORD_PLACEHOLDER` | Password field placeholder text | +| `--submit-text` | `CS_SUBMIT_TEXT` | Login button text | +| `--login-password-msg` | `CS_LOGIN_PASSWORD_MSG` | Message for config file password | +| `--login-env-password-msg` | `CS_LOGIN_ENV_PASSWORD_MSG` | Message when using `$PASSWORD` env var | +| `--login-hashed-password-msg` | `CS_LOGIN_HASHED_PASSWORD_MSG` | Message when using `$HASHED_PASSWORD` env var | +| `--login-rate-limit-msg` | `CS_LOGIN_RATE_LIMIT_MSG` | Rate limiting error message | +| `--missing-password-msg` | `CS_MISSING_PASSWORD_MSG` | Empty password error message | +| `--incorrect-password-msg` | `CS_INCORRECT_PASSWORD_MSG` | Wrong password error message | \ No newline at end of file diff --git a/docs/install.md b/docs/install.md index e2dd905f9401..b2b57ce9fa3e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -287,6 +287,30 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ codercom/code-server:latest ``` +### Customizing the login page + +You can customize the login page by setting environment variables: + +```bash +# Example with login customization +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -e "CS_LOGIN_TITLE=My Development Environment" \ + -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured via environment variable" \ + -e "CS_PASSWORD_PLACEHOLDER=Enter your secure password" \ + -e "CS_SUBMIT_TEXT=ACCESS" \ + codercom/code-server:latest +``` + +Available customization environment variables: +- `CS_LOGIN_TITLE` - Custom login page title +- `CS_LOGIN_BELOW` - Custom text below the login title +- `CS_PASSWORD_PLACEHOLDER` - Custom password field placeholder +- `CS_SUBMIT_TEXT` - Custom submit button text +- `CS_LOGIN_PASSWORD_MSG` - Custom message for config file password +- `CS_LOGIN_ENV_PASSWORD_MSG` - Custom message when using `$PASSWORD` env var +- `CS_LOGIN_HASHED_PASSWORD_MSG` - Custom message when using `$HASHED_PASSWORD` env var + Our official image supports `amd64` and `arm64`. For `arm32` support, you can use a [community-maintained code-server alternative](https://hub.docker.com/r/linuxserver/code-server). diff --git a/src/node/cli.ts b/src/node/cli.ts index a29ec591e0a4..0bdd36fd93e0 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -92,6 +92,16 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { verbose?: boolean "app-name"?: string "welcome-text"?: string + "login-title"?: string + "login-below"?: string + "password-placeholder"?: string + "submit-text"?: string + "login-password-msg"?: string + "login-env-password-msg"?: string + "login-hashed-password-msg"?: string + "login-rate-limit-msg"?: string + "missing-password-msg"?: string + "incorrect-password-msg"?: string "abs-proxy-base-path"?: string /* Positional arguments. */ _?: string[] @@ -291,6 +301,46 @@ export const options: Options> = { short: "w", description: "Text to show on login page", }, + "login-title": { + type: "string", + description: "Custom login page title", + }, + "login-below": { + type: "string", + description: "Custom text to show below login title", + }, + "password-placeholder": { + type: "string", + description: "Custom placeholder text for password field", + }, + "submit-text": { + type: "string", + description: "Custom text for login submit button", + }, + "login-password-msg": { + type: "string", + description: "Custom message for config file password", + }, + "login-env-password-msg": { + type: "string", + description: "Custom message when password is set from $PASSWORD environment variable", + }, + "login-hashed-password-msg": { + type: "string", + description: "Custom message when password is set from $HASHED_PASSWORD environment variable", + }, + "login-rate-limit-msg": { + type: "string", + description: "Custom message for login rate limiting", + }, + "missing-password-msg": { + type: "string", + description: "Custom message for missing password error", + }, + "incorrect-password-msg": { + type: "string", + description: "Custom message for incorrect password error", + }, "abs-proxy-base-path": { type: "string", description: "The base path to prefix to all absproxy requests", @@ -593,6 +643,47 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args["disable-proxy"] = true } + // Login message customization via environment variables + if (process.env.CS_LOGIN_TITLE) { + args["login-title"] = process.env.CS_LOGIN_TITLE + } + + if (process.env.CS_LOGIN_BELOW) { + args["login-below"] = process.env.CS_LOGIN_BELOW + } + + if (process.env.CS_PASSWORD_PLACEHOLDER) { + args["password-placeholder"] = process.env.CS_PASSWORD_PLACEHOLDER + } + + if (process.env.CS_SUBMIT_TEXT) { + args["submit-text"] = process.env.CS_SUBMIT_TEXT + } + + if (process.env.CS_LOGIN_PASSWORD_MSG) { + args["login-password-msg"] = process.env.CS_LOGIN_PASSWORD_MSG + } + + if (process.env.CS_LOGIN_ENV_PASSWORD_MSG) { + args["login-env-password-msg"] = process.env.CS_LOGIN_ENV_PASSWORD_MSG + } + + if (process.env.CS_LOGIN_HASHED_PASSWORD_MSG) { + args["login-hashed-password-msg"] = process.env.CS_LOGIN_HASHED_PASSWORD_MSG + } + + if (process.env.CS_LOGIN_RATE_LIMIT_MSG) { + args["login-rate-limit-msg"] = process.env.CS_LOGIN_RATE_LIMIT_MSG + } + + if (process.env.CS_MISSING_PASSWORD_MSG) { + args["missing-password-msg"] = process.env.CS_MISSING_PASSWORD_MSG + } + + if (process.env.CS_INCORRECT_PASSWORD_MSG) { + args["incorrect-password-msg"] = process.env.CS_INCORRECT_PASSWORD_MSG + } + const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD if (process.env.HASHED_PASSWORD) { args["hashed-password"] = process.env.HASHED_PASSWORD diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 29d51a59d13b..bbc1b945131f 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -31,23 +31,32 @@ const getRoot = async (req: Request, error?: Error): Promise => { const locale = req.args["locale"] || "en" i18n.changeLanguage(locale) const appName = req.args["app-name"] || "code-server" - const welcomeText = req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string) - let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) + const welcomeText = escapeHtml(req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)) + + // Determine password message with custom overrides + let passwordMsg = req.args["login-password-msg"] || i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) if (req.args.usingEnvPassword) { - passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD") + passwordMsg = req.args["login-env-password-msg"] || i18n.t("LOGIN_USING_ENV_PASSWORD") } else if (req.args.usingEnvHashedPassword) { - passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD") + passwordMsg = req.args["login-hashed-password-msg"] || i18n.t("LOGIN_USING_HASHED_PASSWORD") } + passwordMsg = escapeHtml(passwordMsg) + + // Get custom messages or fall back to i18n (with HTML escaping for security) + const loginTitle = escapeHtml(req.args["login-title"] || i18n.t("LOGIN_TITLE", { app: appName })) + const loginBelow = escapeHtml(req.args["login-below"] || i18n.t("LOGIN_BELOW")) + const passwordPlaceholder = escapeHtml(req.args["password-placeholder"] || i18n.t("PASSWORD_PLACEHOLDER")) + const submitText = escapeHtml(req.args["submit-text"] || i18n.t("SUBMIT")) return replaceTemplates( req, content - .replace(/{{I18N_LOGIN_TITLE}}/g, i18n.t("LOGIN_TITLE", { app: appName })) + .replace(/{{I18N_LOGIN_TITLE}}/g, loginTitle) .replace(/{{WELCOME_TEXT}}/g, welcomeText) .replace(/{{PASSWORD_MSG}}/g, passwordMsg) - .replace(/{{I18N_LOGIN_BELOW}}/g, i18n.t("LOGIN_BELOW")) - .replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, i18n.t("PASSWORD_PLACEHOLDER")) - .replace(/{{I18N_SUBMIT}}/g, i18n.t("SUBMIT")) + .replace(/{{I18N_LOGIN_BELOW}}/g, loginBelow) + .replace(/{{I18N_PASSWORD_PLACEHOLDER}}/g, passwordPlaceholder) + .replace(/{{I18N_SUBMIT}}/g, submitText) .replace(/{{ERROR}}/, error ? `
${escapeHtml(error.message)}
` : ""), ) } @@ -75,11 +84,11 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: try { // Check to see if they exceeded their login attempts if (!limiter.canTry()) { - throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string) + throw new Error(req.args["login-rate-limit-msg"] || (i18n.t("LOGIN_RATE_LIMIT") as string)) } if (!password) { - throw new Error(i18n.t("MISS_PASSWORD") as string) + throw new Error(req.args["missing-password-msg"] || (i18n.t("MISS_PASSWORD") as string)) } const passwordMethod = getPasswordMethod(hashedPasswordFromArgs) @@ -113,7 +122,7 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: }), ) - throw new Error(i18n.t("INCORRECT_PASSWORD") as string) + throw new Error(req.args["incorrect-password-msg"] || (i18n.t("INCORRECT_PASSWORD") as string)) } catch (error: any) { const renderedHtml = await getRoot(req, error) res.send(renderedHtml) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index d62edb840464..f0e426814651 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -49,6 +49,10 @@ describe("parser", () => { delete process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE delete process.env.VSCODE_PROXY_URI delete process.env.CS_DISABLE_PROXY + delete process.env.CS_LOGIN_TITLE + delete process.env.CS_LOGIN_ENV_PASSWORD_MSG + delete process.env.CS_PASSWORD_PLACEHOLDER + delete process.env.CS_SUBMIT_TEXT console.log = jest.fn() }) @@ -75,6 +79,10 @@ describe("parser", () => { "--verbose", ["--app-name", "custom instance name"], ["--welcome-text", "welcome to code"], + ["--login-title", "Custom Login Portal"], + ["--login-env-password-msg", "Password from environment"], + ["--password-placeholder", "Enter code"], + ["--submit-text", "ACCESS"], "2", ["--locale", "ja"], @@ -145,6 +153,10 @@ describe("parser", () => { verbose: true, "app-name": "custom instance name", "welcome-text": "welcome to code", + "login-title": "Custom Login Portal", + "login-env-password-msg": "Password from environment", + "password-placeholder": "Enter code", + "submit-text": "ACCESS", version: true, "bind-addr": "192.169.0.1:8080", "session-socket": "/tmp/override-code-server-ipc-socket", @@ -347,6 +359,24 @@ describe("parser", () => { }) }) + it("should use env var login customization", async () => { + process.env.CS_LOGIN_TITLE = "Custom Portal" + process.env.CS_LOGIN_ENV_PASSWORD_MSG = "Password from env" + process.env.CS_PASSWORD_PLACEHOLDER = "Enter code here" + process.env.CS_SUBMIT_TEXT = "ACCESS NOW" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "login-title": "Custom Portal", + "login-env-password-msg": "Password from env", + "password-placeholder": "Enter code here", + "submit-text": "ACCESS NOW", + }) + }) + it("should use env var github token", async () => { process.env.GITHUB_TOKEN = "ga-foo" const args = parse([]) diff --git a/test/unit/node/routes/login.test.ts b/test/unit/node/routes/login.test.ts index 2835bad82354..06c8e1b22ceb 100644 --- a/test/unit/node/routes/login.test.ts +++ b/test/unit/node/routes/login.test.ts @@ -146,5 +146,70 @@ describe("login", () => { expect(resp.status).toBe(200) expect(htmlContent).toContain(`欒迎ζ₯到 code-server`) }) + + it("should return custom login title", async () => { + process.env.PASSWORD = previousEnvPassword + const loginTitle = "Custom Access Portal" + const codeServer = await integration.setup([`--login-title=${loginTitle}`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(`${loginTitle}`) + }) + + it("should return custom password placeholder", async () => { + process.env.PASSWORD = previousEnvPassword + const placeholder = "Enter access code" + const codeServer = await integration.setup([`--password-placeholder=${placeholder}`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(`placeholder="${placeholder}"`) + }) + + it("should return custom submit button text", async () => { + process.env.PASSWORD = previousEnvPassword + const submitText = "ACCESS PORTAL" + const codeServer = await integration.setup([`--submit-text=${submitText}`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(`value="${submitText}"`) + }) + + it("should return custom env password message", async () => { + const envMessage = "Password configured via container environment" + const codeServer = await integration.setup([`--login-env-password-msg=${envMessage}`, `--password=test123`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(envMessage) + }) + + it("should escape HTML in custom messages", async () => { + process.env.PASSWORD = previousEnvPassword + const maliciousTitle = "" + const codeServer = await integration.setup([`--login-title=${maliciousTitle}`, `--password=test123`], "") + const resp = await codeServer.fetch("/login", { method: "GET" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain("<script>alert('xss')</script>") + expect(htmlContent).not.toContain("") + }) + + it("should return custom error messages", async () => { + const customMissingMsg = "Access code required" + const codeServer = await integration.setup([`--missing-password-msg=${customMissingMsg}`, `--password=test123`], "") + const resp = await codeServer.fetch("/login", { method: "POST" }) + + const htmlContent = await resp.text() + expect(resp.status).toBe(200) + expect(htmlContent).toContain(customMissingMsg) + }) }) }) From 2553f4548be94e602fa968c6c8256f43462d2834 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 17 Jun 2025 14:37:45 +0200 Subject: [PATCH 2/4] feat: replace individual UI flags with unified --custom-strings flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace non-scalable individual flags (--login-title, --login-below, etc.) with a unified --custom-strings flag that accepts JSON file paths or inline JSON for UI customization. This leverages the existing i18n system for better scalability and maintainability. Changes: - Add --custom-strings flag with JSON validation - Extend i18n system to merge custom strings with defaults - Remove newly-added individual login/UI flags - Deprecate legacy --app-name and --welcome-text flags - Update login route to use unified i18n system - Add comprehensive tests for new functionality - Update documentation with migration guide and examples πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/FAQ.md | 29 +---- docs/customization.md | 254 +++++++++++++++++++++---------------- src/node/cli.ts | 112 ++++------------ src/node/i18n/index.ts | 83 +++++++++--- src/node/main.ts | 12 ++ src/node/routes/login.ts | 24 ++-- test/unit/node/cli.test.ts | 45 +++---- 7 files changed, 287 insertions(+), 272 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 0f1e78f58bb6..1d97d8c2e21d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -83,37 +83,20 @@ The default location respects `$XDG_CONFIG_HOME`. ### Login page customization -You can customize the login page appearance using CLI flags or environment variables: +You can customize the login page using the `--custom-strings` flag: -**CLI flags:** ```bash -code-server --login-title "My Code Server" \ - --login-env-password-msg "Password set via environment" \ - --password-placeholder "Enter password" \ - --submit-text "LOGIN" +code-server --custom-strings '{"LOGIN_TITLE": "My Code Server", "WELCOME": "Welcome to my portal"}' ``` -**Environment variables:** +Or use a JSON file: ```bash -export CS_LOGIN_TITLE="My Code Server" -export CS_LOGIN_ENV_PASSWORD_MSG="Password set via environment" -export CS_PASSWORD_PLACEHOLDER="Enter password" -export CS_SUBMIT_TEXT="LOGIN" -code-server +code-server --custom-strings /path/to/custom-strings.json ``` -**Config file:** -```yaml -bind-addr: 127.0.0.1:8080 -auth: password -password: your-password -login-title: "My Code Server" -login-env-password-msg: "Password set via environment" -password-placeholder: "Enter password" -submit-text: "LOGIN" -``` +Legacy individual flags (`--app-name`, `--welcome-text`) are still supported but deprecated. -CLI flags take priority over environment variables, which take priority over config file settings. +For detailed customization options and examples, see the [customization guide](./customization.md). ## How do I make my keyboard shortcuts work? diff --git a/docs/customization.md b/docs/customization.md index 0dd8d9d4e1c0..55797e12ec50 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,68 +1,39 @@ # Login Page Customization -code-server allows you to customize the login page appearance and messages through CLI arguments, environment variables, or configuration files. - -## Available Customization Options - -### Branding and Appearance -- **Login Title**: Customize the main title on the login page -- **Welcome Text**: Set custom welcome message -- **App Name**: Change the application branding throughout the interface - -### Login Messages -- **Password Instructions**: Customize the message explaining where to find the password -- **Environment Password Message**: Custom message when password is set via `$PASSWORD` -- **Hashed Password Message**: Custom message when password is set via `$HASHED_PASSWORD` - -### Form Elements -- **Password Placeholder**: Custom placeholder text for the password field -- **Submit Button**: Custom text for the login button - -### Error Messages -- **Rate Limit Message**: Custom message when login attempts are rate limited -- **Missing Password**: Custom message for empty password submissions -- **Incorrect Password**: Custom message for wrong password attempts - -## Configuration Methods - -### CLI Arguments +code-server allows you to customize the login page appearance and messages through a unified `--custom-strings` flag or legacy CLI arguments. + +## Recommended Approach: Custom Strings + +The `--custom-strings` flag provides a scalable way to customize any UI text by leveraging the built-in internationalization system. + +### Using JSON File + +Create a JSON file with your customizations: + +```json +{ + "WELCOME": "Welcome to {{app}} Development Portal", + "LOGIN_TITLE": "{{app}} Secure Access", + "LOGIN_BELOW": "Please authenticate to continue", + "PASSWORD_PLACEHOLDER": "Enter your access code", + "SUBMIT": "AUTHENTICATE", + "LOGIN_PASSWORD": "Check the config file at {{configFile}} for the password.", + "LOGIN_USING_ENV_PASSWORD": "Access code provided via environment variable", + "LOGIN_USING_HASHED_PASSWORD": "Access code configured securely", + "LOGIN_RATE_LIMIT": "Too many attempts. Please wait before trying again.", + "MISS_PASSWORD": "Access code is required", + "INCORRECT_PASSWORD": "Invalid access code" +} +``` ```bash -code-server \ - --app-name "My Development Server" \ - --welcome-text "Welcome to the development environment" \ - --login-title "Secure Access Portal" \ - --login-below "Please authenticate to continue" \ - --password-placeholder "Enter your access code" \ - --submit-text "AUTHENTICATE" \ - --login-env-password-msg "Access code provided via environment variable" \ - --login-rate-limit-msg "Too many attempts. Please wait before trying again." \ - --missing-password-msg "Access code is required" \ - --incorrect-password-msg "Invalid access code" +code-server --custom-strings /path/to/custom-strings.json ``` -### Environment Variables - -Perfect for Docker deployments and containerized environments: +### Using Inline JSON ```bash -# Basic branding -export CS_APP_NAME="My Development Server" -export CS_WELCOME_TEXT="Welcome to the development environment" - -# Login page customization -export CS_LOGIN_TITLE="Secure Access Portal" -export CS_LOGIN_BELOW="Please authenticate to continue" -export CS_PASSWORD_PLACEHOLDER="Enter your access code" -export CS_SUBMIT_TEXT="AUTHENTICATE" - -# Message customization -export CS_LOGIN_ENV_PASSWORD_MSG="Access code provided via environment variable" -export CS_LOGIN_RATE_LIMIT_MSG="Too many attempts. Please wait before trying again." -export CS_MISSING_PASSWORD_MSG="Access code is required" -export CS_INCORRECT_PASSWORD_MSG="Invalid access code" - -code-server +code-server --custom-strings '{"WELCOME": "Welcome to My Dev Portal", "LOGIN_TITLE": "Development Access", "SUBMIT": "SIGN IN"}' ``` ### Configuration File @@ -73,75 +44,142 @@ Add to your `~/.config/code-server/config.yaml`: bind-addr: 127.0.0.1:8080 auth: password password: your-password - -# Branding -app-name: "My Development Server" -welcome-text: "Welcome to the development environment" - -# Login page -login-title: "Secure Access Portal" -login-below: "Please authenticate to continue" -password-placeholder: "Enter your access code" -submit-text: "AUTHENTICATE" - -# Messages -login-env-password-msg: "Access code provided via environment variable" -login-rate-limit-msg: "Too many attempts. Please wait before trying again." -missing-password-msg: "Access code is required" -incorrect-password-msg: "Invalid access code" +custom-strings: | + { + "WELCOME": "Welcome to {{app}} Development Portal", + "LOGIN_TITLE": "{{app}} Secure Access", + "PASSWORD_PLACEHOLDER": "Enter your access code", + "SUBMIT": "AUTHENTICATE" + } ``` +## Available Customization Keys + +| Key | Description | Default | Supports {{app}} placeholder | +|-----|-------------|---------|------------------------------| +| `WELCOME` | Welcome message on login page | "Welcome to {{app}}" | βœ… | +| `LOGIN_TITLE` | Main title on login page | "{{app}} login" | βœ… | +| `LOGIN_BELOW` | Text below the login title | "Please log in below." | ❌ | +| `PASSWORD_PLACEHOLDER` | Password field placeholder text | "PASSWORD" | ❌ | +| `SUBMIT` | Login button text | "SUBMIT" | ❌ | +| `LOGIN_PASSWORD` | Message for config file password | "Check the config file at {{configFile}} for the password." | ❌ | +| `LOGIN_USING_ENV_PASSWORD` | Message when using `$PASSWORD` env var | "Password was set from $PASSWORD." | ❌ | +| `LOGIN_USING_HASHED_PASSWORD` | Message when using `$HASHED_PASSWORD` env var | "Password was set from $HASHED_PASSWORD." | ❌ | +| `LOGIN_RATE_LIMIT` | Rate limiting error message | "Login rate limited!" | ❌ | +| `MISS_PASSWORD` | Empty password error message | "Missing password" | ❌ | +| `INCORRECT_PASSWORD` | Wrong password error message | "Incorrect password" | ❌ | + ## Docker Examples -### Basic Docker Deployment with Customization +### Basic Docker Deployment ```bash docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - -e "CS_LOGIN_TITLE=Development Environment" \ - -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured in container environment" \ - -e "CS_PASSWORD_PLACEHOLDER=Enter development password" \ - -e "CS_SUBMIT_TEXT=ACCESS ENVIRONMENT" \ - codercom/code-server:latest + -v "$PWD/custom-strings.json:/custom-strings.json" \ + codercom/code-server:latest --custom-strings /custom-strings.json ``` -### Corporate Branding Example +### Corporate Branding with Inline JSON ```bash docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - -e "CS_APP_NAME=ACME Corporation Dev Portal" \ - -e "CS_LOGIN_TITLE=ACME Development Portal" \ - -e "CS_LOGIN_BELOW=Enter your corporate credentials" \ - -e "CS_PASSWORD_PLACEHOLDER=Corporate Password" \ - -e "CS_SUBMIT_TEXT=SIGN IN" \ - -e "CS_LOGIN_ENV_PASSWORD_MSG=Password managed by IT department" \ - codercom/code-server:latest + codercom/code-server:latest --custom-strings '{ + "WELCOME": "Welcome to ACME Corporation Development Portal", + "LOGIN_TITLE": "ACME Dev Portal Access", + "LOGIN_BELOW": "Enter your corporate credentials", + "PASSWORD_PLACEHOLDER": "Corporate Password", + "SUBMIT": "SIGN IN", + "LOGIN_USING_ENV_PASSWORD": "Password managed by IT department" + }' +``` + +## Legacy Support (Deprecated) + +The following individual flags are still supported but deprecated. Use `--custom-strings` for new deployments: + +```bash +# Deprecated - use --custom-strings instead +code-server \ + --app-name "My Development Server" \ + --welcome-text "Welcome to the development environment" +``` + +These legacy flags will show deprecation warnings and may be removed in future versions. + +## Migration Guide + +### From Individual Flags to Custom Strings + +**Old approach:** +```bash +code-server \ + --app-name "Dev Portal" \ + --welcome-text "Welcome to development" \ + --login-title "Portal Access" +``` + +**New approach:** +```bash +code-server --custom-strings '{ + "WELCOME": "Welcome to development", + "LOGIN_TITLE": "Portal Access" +}' +``` + +**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. Use it alongside `--custom-strings` or customize the full text without placeholders. + +### From Environment Variables + +**Old approach:** +```bash +export CS_LOGIN_TITLE="Portal Access" +export CS_WELCOME_TEXT="Welcome message" +code-server +``` + +**New approach:** +```bash +echo '{"LOGIN_TITLE": "Portal Access", "WELCOME": "Welcome message"}' > strings.json +code-server --custom-strings strings.json ``` -## Priority Order +## Benefits of Custom Strings + +- βœ… **Scalable**: Add any new UI strings without new CLI flags +- βœ… **Flexible**: Supports both files and inline JSON +- βœ… **Future-proof**: Automatically supports new UI strings as they're added +- βœ… **Organized**: All customizations in one place +- βœ… **Version-controlled**: JSON files can be tracked in your repository -Settings are applied in the following priority order (highest to lowest): +## Advanced Usage -1. **CLI arguments** - Highest priority -2. **Environment variables** - Medium priority -3. **Config file** - Lowest priority +### Multi-language Support -This allows you to set defaults in your config file and override them with environment variables or CLI arguments as needed. +Create different JSON files for different languages: -## Complete Reference +```bash +# English +code-server --custom-strings /config/strings-en.json + +# Spanish +code-server --custom-strings /config/strings-es.json --locale es +``` -| CLI Argument | Environment Variable | Description | -|--------------|---------------------|-------------| -| `--app-name` | `CS_APP_NAME` | Application name used throughout the interface | -| `--welcome-text` | `CS_WELCOME_TEXT` | Welcome message on login page | -| `--login-title` | `CS_LOGIN_TITLE` | Main title on login page | -| `--login-below` | `CS_LOGIN_BELOW` | Text below the login title | -| `--password-placeholder` | `CS_PASSWORD_PLACEHOLDER` | Password field placeholder text | -| `--submit-text` | `CS_SUBMIT_TEXT` | Login button text | -| `--login-password-msg` | `CS_LOGIN_PASSWORD_MSG` | Message for config file password | -| `--login-env-password-msg` | `CS_LOGIN_ENV_PASSWORD_MSG` | Message when using `$PASSWORD` env var | -| `--login-hashed-password-msg` | `CS_LOGIN_HASHED_PASSWORD_MSG` | Message when using `$HASHED_PASSWORD` env var | -| `--login-rate-limit-msg` | `CS_LOGIN_RATE_LIMIT_MSG` | Rate limiting error message | -| `--missing-password-msg` | `CS_MISSING_PASSWORD_MSG` | Empty password error message | -| `--incorrect-password-msg` | `CS_INCORRECT_PASSWORD_MSG` | Wrong password error message | \ No newline at end of file +### Dynamic Customization + +Generate JSON dynamically in scripts: + +```bash +#!/bin/bash +COMPANY_NAME="ACME Corp" +cat > /tmp/strings.json << EOF +{ + "WELCOME": "Welcome to ${COMPANY_NAME} Development Portal", + "LOGIN_TITLE": "${COMPANY_NAME} Access Portal" +} +EOF + +code-server --custom-strings /tmp/strings.json +``` \ No newline at end of file diff --git a/src/node/cli.ts b/src/node/cli.ts index 0bdd36fd93e0..132f29dc7a2e 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -92,17 +92,8 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { verbose?: boolean "app-name"?: string "welcome-text"?: string - "login-title"?: string - "login-below"?: string - "password-placeholder"?: string - "submit-text"?: string - "login-password-msg"?: string - "login-env-password-msg"?: string - "login-hashed-password-msg"?: string - "login-rate-limit-msg"?: string - "missing-password-msg"?: string - "incorrect-password-msg"?: string "abs-proxy-base-path"?: string + "custom-strings"?: string /* Positional arguments. */ _?: string[] } @@ -295,56 +286,22 @@ export const options: Options> = { type: "string", short: "an", description: "The name to use in branding. Will be shown in titlebar and welcome message", + deprecated: true, }, "welcome-text": { type: "string", short: "w", description: "Text to show on login page", - }, - "login-title": { - type: "string", - description: "Custom login page title", - }, - "login-below": { - type: "string", - description: "Custom text to show below login title", - }, - "password-placeholder": { - type: "string", - description: "Custom placeholder text for password field", - }, - "submit-text": { - type: "string", - description: "Custom text for login submit button", - }, - "login-password-msg": { - type: "string", - description: "Custom message for config file password", - }, - "login-env-password-msg": { - type: "string", - description: "Custom message when password is set from $PASSWORD environment variable", - }, - "login-hashed-password-msg": { - type: "string", - description: "Custom message when password is set from $HASHED_PASSWORD environment variable", - }, - "login-rate-limit-msg": { - type: "string", - description: "Custom message for login rate limiting", - }, - "missing-password-msg": { - type: "string", - description: "Custom message for missing password error", - }, - "incorrect-password-msg": { - type: "string", - description: "Custom message for incorrect password error", + deprecated: true, }, "abs-proxy-base-path": { type: "string", description: "The base path to prefix to all absproxy requests", }, + "custom-strings": { + type: "string", + description: "Path to JSON file or raw JSON string with custom UI strings. Merges with default strings and supports all i18n keys.", + }, } export const optionDescriptions = (opts: Partial>> = options): string[] => { @@ -509,6 +466,21 @@ export const parse = ( throw new Error("--cert-key is missing") } + // Validate custom-strings flag + if (args["custom-strings"]) { + try { + // First try to parse as JSON directly + JSON.parse(args["custom-strings"]) + } catch (jsonError) { + // If JSON parsing fails, check if it's a valid file path + if (!args["custom-strings"].startsWith("{") && !args["custom-strings"].startsWith("[")) { + // Assume it's a file path - validation will happen later when the file is read + } else { + throw error(`--custom-strings contains invalid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`) + } + } + } + logger.debug(() => [`parsed ${opts?.configFile ? "config" : "command line"}`, field("args", redactArgs(args))]) return args @@ -643,46 +615,6 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args["disable-proxy"] = true } - // Login message customization via environment variables - if (process.env.CS_LOGIN_TITLE) { - args["login-title"] = process.env.CS_LOGIN_TITLE - } - - if (process.env.CS_LOGIN_BELOW) { - args["login-below"] = process.env.CS_LOGIN_BELOW - } - - if (process.env.CS_PASSWORD_PLACEHOLDER) { - args["password-placeholder"] = process.env.CS_PASSWORD_PLACEHOLDER - } - - if (process.env.CS_SUBMIT_TEXT) { - args["submit-text"] = process.env.CS_SUBMIT_TEXT - } - - if (process.env.CS_LOGIN_PASSWORD_MSG) { - args["login-password-msg"] = process.env.CS_LOGIN_PASSWORD_MSG - } - - if (process.env.CS_LOGIN_ENV_PASSWORD_MSG) { - args["login-env-password-msg"] = process.env.CS_LOGIN_ENV_PASSWORD_MSG - } - - if (process.env.CS_LOGIN_HASHED_PASSWORD_MSG) { - args["login-hashed-password-msg"] = process.env.CS_LOGIN_HASHED_PASSWORD_MSG - } - - if (process.env.CS_LOGIN_RATE_LIMIT_MSG) { - args["login-rate-limit-msg"] = process.env.CS_LOGIN_RATE_LIMIT_MSG - } - - if (process.env.CS_MISSING_PASSWORD_MSG) { - args["missing-password-msg"] = process.env.CS_MISSING_PASSWORD_MSG - } - - if (process.env.CS_INCORRECT_PASSWORD_MSG) { - args["incorrect-password-msg"] = process.env.CS_INCORRECT_PASSWORD_MSG - } const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD if (process.env.HASHED_PASSWORD) { diff --git a/src/node/i18n/index.ts b/src/node/i18n/index.ts index 4ee718e13aa2..c78fcbbf4188 100644 --- a/src/node/i18n/index.ts +++ b/src/node/i18n/index.ts @@ -1,33 +1,82 @@ import i18next, { init } from "i18next" +import { promises as fs } from "fs" import * as en from "./locales/en.json" import * as ja from "./locales/ja.json" import * as th from "./locales/th.json" import * as ur from "./locales/ur.json" import * as zhCn from "./locales/zh-cn.json" +const defaultResources = { + en: { + translation: en, + }, + "zh-cn": { + translation: zhCn, + }, + th: { + translation: th, + }, + ja: { + translation: ja, + }, + ur: { + translation: ur, + }, +} + +let customStrings: Record = {} + +export async function loadCustomStrings(customStringsArg?: string): Promise { + if (!customStringsArg) { + return + } + + try { + let customStringsData: Record + + // Try to parse as JSON first + try { + customStringsData = JSON.parse(customStringsArg) + } catch { + // If JSON parsing fails, treat as file path + const fileContent = await fs.readFile(customStringsArg, "utf8") + customStringsData = JSON.parse(fileContent) + } + + customStrings = customStringsData + + // Re-initialize i18next with merged resources + const mergedResources = Object.keys(defaultResources).reduce((acc, lang) => { + const langKey = lang as keyof typeof defaultResources + acc[langKey] = { + translation: { + ...defaultResources[langKey].translation, + ...customStrings, + }, + } + return acc + }, {} as typeof defaultResources) + + await i18next.init({ + lng: "en", + fallbackLng: "en", + returnNull: false, + lowerCaseLng: true, + debug: process.env.NODE_ENV === "development", + resources: mergedResources, + }) + } catch (error) { + throw new Error(`Failed to load custom strings: ${error instanceof Error ? error.message : String(error)}`) + } +} + init({ lng: "en", fallbackLng: "en", // language to use if translations in user language are not available. returnNull: false, lowerCaseLng: true, debug: process.env.NODE_ENV === "development", - resources: { - en: { - translation: en, - }, - "zh-cn": { - translation: zhCn, - }, - th: { - translation: th, - }, - ja: { - translation: ja, - }, - ur: { - translation: ur, - }, - }, + resources: defaultResources, }) export default i18next diff --git a/src/node/main.ts b/src/node/main.ts index 470ddeb25cc7..2969476fb6d8 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -7,6 +7,7 @@ import { plural } from "../common/util" import { createApp, ensureAddress } from "./app" import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli" import { commit, version, vsRootPath } from "./constants" +import { loadCustomStrings } from "./i18n" import { register } from "./routes" import { VSCodeModule } from "./routes/vscode" import { isDirectory, open } from "./util" @@ -122,6 +123,17 @@ export const runCodeServer = async ( ): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => { logger.info(`code-server ${version} ${commit}`) + // Load custom strings if provided + if (args["custom-strings"]) { + try { + await loadCustomStrings(args["custom-strings"]) + logger.info("Loaded custom strings") + } catch (error) { + logger.error("Failed to load custom strings", field("error", error)) + throw error + } + } + logger.info(`Using user-data-dir ${args["user-data-dir"]}`) logger.debug(`Using extensions-dir ${args["extensions-dir"]}`) diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index bbc1b945131f..511d4817455e 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -33,20 +33,20 @@ const getRoot = async (req: Request, error?: Error): Promise => { const appName = req.args["app-name"] || "code-server" const welcomeText = escapeHtml(req.args["welcome-text"] || (i18n.t("WELCOME", { app: appName }) as string)) - // Determine password message with custom overrides - let passwordMsg = req.args["login-password-msg"] || i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) + // Determine password message using i18n + let passwordMsg = i18n.t("LOGIN_PASSWORD", { configFile: req.args.config }) if (req.args.usingEnvPassword) { - passwordMsg = req.args["login-env-password-msg"] || i18n.t("LOGIN_USING_ENV_PASSWORD") + passwordMsg = i18n.t("LOGIN_USING_ENV_PASSWORD") } else if (req.args.usingEnvHashedPassword) { - passwordMsg = req.args["login-hashed-password-msg"] || i18n.t("LOGIN_USING_HASHED_PASSWORD") + passwordMsg = i18n.t("LOGIN_USING_HASHED_PASSWORD") } passwordMsg = escapeHtml(passwordMsg) - // Get custom messages or fall back to i18n (with HTML escaping for security) - const loginTitle = escapeHtml(req.args["login-title"] || i18n.t("LOGIN_TITLE", { app: appName })) - const loginBelow = escapeHtml(req.args["login-below"] || i18n.t("LOGIN_BELOW")) - const passwordPlaceholder = escapeHtml(req.args["password-placeholder"] || i18n.t("PASSWORD_PLACEHOLDER")) - const submitText = escapeHtml(req.args["submit-text"] || i18n.t("SUBMIT")) + // Get messages from i18n (with HTML escaping for security) + const loginTitle = escapeHtml(i18n.t("LOGIN_TITLE", { app: appName })) + const loginBelow = escapeHtml(i18n.t("LOGIN_BELOW")) + const passwordPlaceholder = escapeHtml(i18n.t("PASSWORD_PLACEHOLDER")) + const submitText = escapeHtml(i18n.t("SUBMIT")) return replaceTemplates( req, @@ -84,11 +84,11 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: try { // Check to see if they exceeded their login attempts if (!limiter.canTry()) { - throw new Error(req.args["login-rate-limit-msg"] || (i18n.t("LOGIN_RATE_LIMIT") as string)) + throw new Error(i18n.t("LOGIN_RATE_LIMIT") as string) } if (!password) { - throw new Error(req.args["missing-password-msg"] || (i18n.t("MISS_PASSWORD") as string)) + throw new Error(i18n.t("MISS_PASSWORD") as string) } const passwordMethod = getPasswordMethod(hashedPasswordFromArgs) @@ -122,7 +122,7 @@ router.post<{}, string, { password?: string; base?: string } | undefined, { to?: }), ) - throw new Error(req.args["incorrect-password-msg"] || (i18n.t("INCORRECT_PASSWORD") as string)) + throw new Error(i18n.t("INCORRECT_PASSWORD") as string) } catch (error: any) { const renderedHtml = await getRoot(req, error) res.send(renderedHtml) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index f0e426814651..c62ec563a8fc 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -79,10 +79,7 @@ describe("parser", () => { "--verbose", ["--app-name", "custom instance name"], ["--welcome-text", "welcome to code"], - ["--login-title", "Custom Login Portal"], - ["--login-env-password-msg", "Password from environment"], - ["--password-placeholder", "Enter code"], - ["--submit-text", "ACCESS"], + ["--custom-strings", '{"LOGIN_TITLE": "Custom Portal"}'], "2", ["--locale", "ja"], @@ -153,10 +150,7 @@ describe("parser", () => { verbose: true, "app-name": "custom instance name", "welcome-text": "welcome to code", - "login-title": "Custom Login Portal", - "login-env-password-msg": "Password from environment", - "password-placeholder": "Enter code", - "submit-text": "ACCESS", + "custom-strings": '{"LOGIN_TITLE": "Custom Portal"}', version: true, "bind-addr": "192.169.0.1:8080", "session-socket": "/tmp/override-code-server-ipc-socket", @@ -359,21 +353,28 @@ describe("parser", () => { }) }) - it("should use env var login customization", async () => { - process.env.CS_LOGIN_TITLE = "Custom Portal" - process.env.CS_LOGIN_ENV_PASSWORD_MSG = "Password from env" - process.env.CS_PASSWORD_PLACEHOLDER = "Enter code here" - process.env.CS_SUBMIT_TEXT = "ACCESS NOW" - const args = parse([]) - expect(args).toEqual({}) + it("should parse custom-strings flag", async () => { + // Test with JSON string + const jsonString = '{"WELCOME": "Custom Welcome", "LOGIN_TITLE": "My App"}' + const args = parse(["--custom-strings", jsonString]) + expect(args).toEqual({ + "custom-strings": jsonString, + }) + }) - const defaultArgs = await setDefaults(args) - expect(defaultArgs).toEqual({ - ...defaults, - "login-title": "Custom Portal", - "login-env-password-msg": "Password from env", - "password-placeholder": "Enter code here", - "submit-text": "ACCESS NOW", + it("should validate custom-strings JSON", async () => { + // Test with invalid JSON + expect(() => parse(["--custom-strings", '{"invalid": json}'])).toThrowError(/contains invalid JSON/) + + // Test with valid JSON that looks like a file path + expect(() => parse(["--custom-strings", "/path/to/file.json"])).not.toThrow() + }) + + it("should support deprecated app-name and welcome-text flags", async () => { + const args = parse(["--app-name", "My App", "--welcome-text", "Welcome!"]) + expect(args).toEqual({ + "app-name": "My App", + "welcome-text": "Welcome!", }) }) From c2eb61d5f3e2f53e59df8094ea15d6b0a3c35e4c Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 17 Jun 2025 14:40:28 +0200 Subject: [PATCH 3/4] docs: simplify migration guide to only cover released flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove migration examples for flags that were never released. Only --app-name and --welcome-text were in the original codebase and might be used by existing users. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/customization.md | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 55797e12ec50..4a9f7998de4b 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -97,7 +97,7 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ ## Legacy Support (Deprecated) -The following individual flags are still supported but deprecated. Use `--custom-strings` for new deployments: +The following flags are still supported but deprecated. Use `--custom-strings` for new deployments: ```bash # Deprecated - use --custom-strings instead @@ -110,40 +110,25 @@ These legacy flags will show deprecation warnings and may be removed in future v ## Migration Guide -### From Individual Flags to Custom Strings +### From Legacy Flags to Custom Strings **Old approach:** ```bash code-server \ --app-name "Dev Portal" \ - --welcome-text "Welcome to development" \ - --login-title "Portal Access" + --welcome-text "Welcome to development" ``` **New approach:** ```bash code-server --custom-strings '{ - "WELCOME": "Welcome to development", - "LOGIN_TITLE": "Portal Access" + "WELCOME": "Welcome to development" }' ``` -**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. Use it alongside `--custom-strings` or customize the full text without placeholders. - -### From Environment Variables - -**Old approach:** -```bash -export CS_LOGIN_TITLE="Portal Access" -export CS_WELCOME_TEXT="Welcome message" -code-server -``` - -**New approach:** -```bash -echo '{"LOGIN_TITLE": "Portal Access", "WELCOME": "Welcome message"}' > strings.json -code-server --custom-strings strings.json -``` +**Note:** The `--app-name` flag controls the `{{app}}` placeholder in templates. You can either: +1. Keep using `--app-name` alongside `--custom-strings` +2. Customize the full text without placeholders in your JSON ## Benefits of Custom Strings From c0189ed3faa9e05e18cb9dd792d5f6a41d10b7a0 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 17 Jun 2025 14:42:48 +0200 Subject: [PATCH 4/4] docs: update Docker customization examples to use --custom-strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace outdated CS_* environment variable examples with the new --custom-strings flag approach. Include both inline JSON and mounted file examples for Docker users. πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/install.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/install.md b/docs/install.md index b2b57ce9fa3e..2cda3583605f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -289,27 +289,31 @@ docker run -it --name code-server -p 127.0.0.1:8080:8080 \ ### Customizing the login page -You can customize the login page by setting environment variables: +You can customize the login page using the `--custom-strings` flag: ```bash -# Example with login customization +# Example with inline JSON customization docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$PWD:/home/coder/project" \ - -e "CS_LOGIN_TITLE=My Development Environment" \ - -e "CS_LOGIN_ENV_PASSWORD_MSG=Password configured via environment variable" \ - -e "CS_PASSWORD_PLACEHOLDER=Enter your secure password" \ - -e "CS_SUBMIT_TEXT=ACCESS" \ - codercom/code-server:latest + codercom/code-server:latest --custom-strings '{ + "LOGIN_TITLE": "My Development Environment", + "WELCOME": "Welcome to your coding workspace", + "PASSWORD_PLACEHOLDER": "Enter your secure password", + "SUBMIT": "ACCESS" + }' +``` + +Or mount a JSON file: + +```bash +# Example with JSON file +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ + -v "$PWD:/home/coder/project" \ + -v "$PWD/custom-strings.json:/config/strings.json" \ + codercom/code-server:latest --custom-strings /config/strings.json ``` -Available customization environment variables: -- `CS_LOGIN_TITLE` - Custom login page title -- `CS_LOGIN_BELOW` - Custom text below the login title -- `CS_PASSWORD_PLACEHOLDER` - Custom password field placeholder -- `CS_SUBMIT_TEXT` - Custom submit button text -- `CS_LOGIN_PASSWORD_MSG` - Custom message for config file password -- `CS_LOGIN_ENV_PASSWORD_MSG` - Custom message when using `$PASSWORD` env var -- `CS_LOGIN_HASHED_PASSWORD_MSG` - Custom message when using `$HASHED_PASSWORD` env var +For detailed customization options, see the [customization guide](./customization.md). Our official image supports `amd64` and `arm64`. For `arm32` support, you can use a [community-maintained code-server