diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2cbd64a..419c8cd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -80,3 +80,31 @@ jobs: files: release-artifacts/*.jar env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + update_nixos_plugin_pins: + name: Update NixOS plugin pins in docs + runs-on: ubuntu-latest + needs: [check_if_version_upgraded, create_github_release] + if: needs.check_if_version_upgraded.outputs.is_pre_release != 'true' + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: main + - uses: actions/setup-node@v4 + - name: Re-pin docs/deployment-nixos.md to the new release + run: node scripts/update-nixos-plugin-pins.mjs --tag v${{ needs.check_if_version_upgraded.outputs.to_version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Commit and push updated pins + run: | + if git diff --quiet docs/deployment-nixos.md; then + echo "Pins already up to date, nothing to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/deployment-nixos.md + git commit -m "docs: pin NixOS plugins to v${{ needs.check_if_version_upgraded.outputs.to_version }}" + git push origin main diff --git a/README.md b/README.md index 039d089..3c3c987 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,42 @@ Keycloak login theme using helpwave hightide components, plus the Keycloak SPI extensions that power the registration flow (Cloudflare Turnstile + privacy acceptance) and the profile picture upload. -## Quick start +## Contents + +- [Features](#features) +- [Development](#development) + - [Quick start](#quick-start) + - [Building the theme](#building-the-theme) + - [Building the Keycloak SPIs](#building-the-keycloak-spis) + - [Local development with Docker](#local-development-with-docker) + - [NixOS development](#nixos-development) +- [Deployment](#deployment) + - [1. Install the plugins](#1-install-the-plugins) + - [2. Enable the Cloudflare Turnstile form action](#2-enable-the-cloudflare-turnstile-form-action) + - [3. Enable the policy-acceptance required actions](#3-enable-the-policy-acceptance-required-actions) + - [4. Configure the profile picture storage](#4-configure-the-profile-picture-storage) + - [5. Wire the theme to the SPIs](#5-wire-the-theme-to-the-spis) +- [Releases & version pinning](#releases--version-pinning) -```bash -npm ci -``` +## Features + +- hightide component integration +- Realm indicator chip with deterministic color mapping +- Custom login, register, and forgot password pages +- Field-level validation matching hightide patterns +- **Cloudflare Turnstile** CAPTCHA on signup (`helpwave-turnstile` FormAction SPI) +- **Versioned policy consents** (privacy + future forms) via a reusable RequiredAction + SPI (`helpwave-policy-acceptance`). Bumping the version on the realm re-prompts every + user on next login; acceptance metadata is persisted on the user account. +- **Profile picture upload** with server-side scaling to multiple sizes and storage in any + S3-compatible bucket (`helpwave-picture` Realm Resource SPI) ## Development -### Linting and type checking +### Quick start ```bash +npm ci npm run lint npm run typecheck ``` @@ -52,7 +77,7 @@ independently): Drop all three (alongside the theme jar) into Keycloak's `providers/` directory and run `kc.sh build`. -## Local development with Docker +### Local development with Docker Start keycloak and postgres services: @@ -72,7 +97,8 @@ Default admin credentials: - Username: `admin` - Password: `admin` -### Verification URLs +For SPI configuration, copy [`.env.example`](.env.example) to `.env` and fill it in, then +`docker compose --env-file .env up`. After starting the services, you can access: @@ -80,28 +106,16 @@ After starting the services, you can access: - Team realm login: http://localhost:8080/realms/team/protocol/openid-connect/auth?client_id=account-console&redirect_uri=http://localhost:8080/realms/team/account/&response_type=code&scope=openid - Keycloak admin console: http://localhost:8080/admin -## NixOS development +### NixOS development For nixos users, see [docs/nixos.md](docs/nixos.md) for nix-shell setup instructions. -## Features - -- hightide component integration -- Realm indicator chip with deterministic color mapping -- Custom login, register, and forgot password pages -- Field-level validation matching hightide patterns -- **Cloudflare Turnstile** CAPTCHA on signup (`helpwave-turnstile` FormAction SPI) -- **Versioned policy consents** (privacy + future forms) via a reusable RequiredAction - SPI (`helpwave-policy-acceptance`). Bumping the version on the realm re-prompts every - user on next login; acceptance metadata is persisted on the user account. -- **Profile picture upload** with server-side scaling to multiple sizes and storage in any - S3-compatible bucket (`helpwave-picture` Realm Resource SPI) - ---- - ## Deployment -The release workflow publishes the following jars on every version bump in `package.json`: +### 1. Install the plugins + +Every release publishes the following jars (see +[Releases & version pinning](#releases--version-pinning) for how releases are cut): | Jar | Purpose | |--------------------------------------------------|--------------------------------------------------| @@ -113,7 +127,13 @@ The release workflow publishes the following jars on every version bump in `pack Copy all jars into Keycloak's `providers/` directory (or mount them into the container) and run `kc.sh build` to rebuild the runtime, then start Keycloak normally. -### 1. Enable the Cloudflare Turnstile form action +**On NixOS**, don't do any of this by hand: a complete `services.keycloak` example — +including an auto-updated, hash-pinned plugin block and [sops-nix] secret handling — +lives in [docs/deployment-nixos.md](docs/deployment-nixos.md). + +[sops-nix]: https://github.com/Mic92/sops-nix + +### 2. Enable the Cloudflare Turnstile form action 1. Open the Keycloak admin console. 2. Go to **Authentication** → **Flows** and duplicate the built-in **registration** flow. @@ -124,7 +144,7 @@ and run `kc.sh build` to rebuild the runtime, then start Keycloak normally. Get these from . 5. Set this flow as the realm's **Registration flow** binding. -### 2. Enable the policy-acceptance required actions +### 3. Enable the policy-acceptance required actions 1. **Authentication** → **Required actions** → enable **Privacy Policy Acceptance (helpwave)**. @@ -142,7 +162,7 @@ Add more consent forms (e.g. terms of service, data-processing agreement) by add file. The React `Terms.tsx` page renders the policy variant automatically when a `policyId` attribute is set. -### 3. Configure the profile picture storage +### 4. Configure the profile picture storage The profile picture SPI accepts standard AWS S3 or Cloudflare R2 (any S3-compatible backend). It exposes itself at: @@ -192,7 +212,7 @@ KC_SPI_REALM_RESTAPI_EXTENSION_HELPWAVE_PICTURE_SECRET_KEY=... KC_SPI_REALM_RESTAPI_EXTENSION_HELPWAVE_PICTURE_PUBLIC_BASE_URL=https://avatars.helpwave.de ``` -### 3. Wire the theme to the SPIs +### 5. Wire the theme to the SPIs Two Keycloakify env vars expose the SPI to the theme at render time: @@ -210,16 +230,26 @@ KC_PROFILE_PICTURE_API_URL=https://id.helpwave.de/realms/customer/helpwave-pictu (Keycloakify reads `KC_` and exposes it as `kcContext.properties.`.) -### 4. NixOS deployment +## Releases & version pinning -A complete `services.keycloak` example with [sops-nix] secret handling and the matching -admin-console steps lives in [docs/deployment-nixos.md](docs/deployment-nixos.md). For -local development with `docker compose`, copy [`.env.example`](.env.example) to `.env` -and fill in the values. +Cutting and consuming a release is a single repeatable motion: -[sops-nix]: https://github.com/Mic92/sops-nix +1. **Bump** `version` in `package.json` on `main` (and the Maven version in + `keycloak-extensions/pom.xml` + module poms if the SPIs changed). +2. **CI releases**: the workflow builds the theme + SPIs and publishes a GitHub release + `v` with all jars attached. +3. **CI re-pins the docs**: the `update_nixos_plugin_pins` job then updates the + `themeVersion` / `spiVersion` / `sha256` pins in + [docs/deployment-nixos.md](docs/deployment-nixos.md) from the release assets' digests + and commits the result to `main` — the checked-in NixOS snippet always matches the + latest release. +4. **Deploy**: copy the refreshed pin block into your NixOS config and + `nixos-rebuild switch`. -### 5. Releases +To re-pin the docs manually (e.g. against a specific release): -Bump `version` in `package.json` on `main`. The CI workflow builds the theme + SPIs and -publishes a GitHub release with all four jars attached. +```bash +npm run update-nix-pins # latest release +npm run update-nix-pins -- --tag v0.6.0 # specific release +npm run update-nix-pins -- --check # verify pins are current +``` diff --git a/docs/deployment-nixos.md b/docs/deployment-nixos.md index d7b4122..acbb1c6 100644 --- a/docs/deployment-nixos.md +++ b/docs/deployment-nixos.md @@ -6,7 +6,7 @@ secrets with [sops-nix]. [sops-nix]: https://github.com/Mic92/sops-nix -## 1. Jars per release +## 1. Release artifacts Every release attaches the following artifacts to the GitHub release: @@ -17,35 +17,65 @@ Every release attaches the following artifacts to the GitHub release: | `helpwave-policy-acceptance-.jar` | `policy-acceptance/` | `RequiredAction` SPI: versioned policy consents (privacy + future forms). | | `helpwave-picture-.jar` | `picture/` | `RealmResourceProvider` SPI: avatar upload to S3 / R2. | -`` is the SPI Maven version (`keycloak-extensions/pom.xml`, currently `0.3.0`), -which is independent from the theme/npm version in `package.json`. All four jars go into -Keycloak's `providers/` directory — `services.keycloak.plugins` does that for you. +Two independent version numbers are in play: -## 2. Full NixOS module example +- **`themeVersion`** — the npm version in `package.json`. Bumping it on `main` is what + cuts a release; the release tag is `v` and **all** artifacts (theme and + SPI jars) are downloaded from that tag. +- **`spiVersion`** — the Maven version in `keycloak-extensions/pom.xml`. It only appears + in the SPI jar *file names* (`` above) and changes independently of the theme. + +All four jars go into Keycloak's `providers/` directory — +`services.keycloak.plugins` does that for you. + +## 2. Plugin pins + +The pins below are **kept up to date automatically**: after every release, CI recomputes +the versions and sha256 hashes from the release assets and commits them back to this file +(see [§6 Updating](#6-updating)). Whatever is checked in here always matches the latest +release — copy it as-is. + +To re-pin manually (e.g. against an older release): + +```sh +npm run update-nix-pins # pin to the latest release +npm run update-nix-pins -- --tag v0.6.0 # pin to a specific release +npm run update-nix-pins -- --check # verify the pins are current (CI-friendly) +``` + +## 3. Full NixOS module example ```nix { pkgs, config, ... }: let domain = "id.helpwave.de"; - themeVersion = "0.6.0"; # ⇄ package.json version → release tag v0.6.0 - spiVersion = "0.3.0"; # ⇄ keycloak-extensions/pom.xml + themeVersion = "0.6.0"; + spiVersion = "0.3.0"; - release = file: sha: + release = + ver: file: sha: pkgs.fetchurl { name = file; - url = "https://github.com/helpwave/id.helpwave.de/releases/download/v${themeVersion}/${file}"; + url = "https://github.com/helpwave/id.helpwave.de/releases/download/v${ver}/${file}"; sha256 = sha; }; - themePlugin = release "keycloak-theme-for-kc-26.2-and-above.jar" - "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - captchaPlugin = release "helpwave-captcha-${spiVersion}.jar" - "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="; - policyPlugin = release "helpwave-policy-acceptance-${spiVersion}.jar" - "sha256-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC="; - picturePlugin = release "helpwave-picture-${spiVersion}.jar" - "sha256-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD="; + themePlugin = + release themeVersion "keycloak-theme-for-kc-26.2-and-above.jar" + "sha256-/u43lyHHizfXivbwsTS3ydQZd6dGXEhrsTxlXalkuJc="; + + captchaSPI = + release themeVersion "helpwave-captcha-${spiVersion}.jar" + "sha256-m0Yqi8lCiZlxACpiUxnmT/gOdbrRmatDKDhSV5/o9ME="; + + pictureSPI = + release themeVersion "helpwave-picture-${spiVersion}.jar" + "sha256-VWL5NsyBGsd1nzah8EYNTwpyao0LOYZAla71vFXJjGY="; + + policySPI = + release themeVersion "helpwave-policy-acceptance-${spiVersion}.jar" + "sha256-0E4dv+lgUxe6ZEiNNAYw8L2Of3GtsBlzyunER5Z8pmc="; in { # ── sops-nix secrets ────────────────────────────────────────────────────── @@ -71,9 +101,9 @@ in plugins = [ themePlugin - captchaPlugin - policyPlugin - picturePlugin + captchaSPI + policySPI + pictureSPI ]; settings = { @@ -115,17 +145,7 @@ in } ``` -> **Computing the sha256 placeholders** -> -> ```sh -> nix-prefetch-url --type sha256 \ -> "https://github.com/helpwave/id.helpwave.de/releases/download/v0.5.0/helpwave-picture-0.2.0.jar" -> ``` -> -> Or just run `nixos-rebuild switch` once with all four `sha256-AAA…` placeholders, copy -> each `got: sha256-…` line out of the error message, and paste it back. - -## 3. Wiring the auth flow (Turnstile + policy consents) +## 4. Wiring the auth flow (Turnstile + policy consents) ### Turnstile (registration FormAction) @@ -219,32 +239,60 @@ disk in cleartext or in the Nix store. 2. `Realm settings → General → Attributes` (or via the admin API): set `helpwave.policy.privacy.url` and `helpwave.policy.privacy.version`. -See also the manual setup section in [README.md](../README.md#1-enable-the-cloudflare-turnstile-and-privacy-form-actions). +See also the manual setup section in [README.md](../README.md#2-enable-the-cloudflare-turnstile-form-action). -## 4. Example `.env` (for local dev with `docker compose`) +## 5. Local development A flat env file matching the same variables works for the docker-compose local stack (`docker-compose.yml` in the repo root). Copy `.env.example` to `.env`, fill it in, then `docker compose --env-file .env up`. -See [`.env.example`](../.env.example) at the repo root. +See [`.env.example`](../.env.example) at the repo root and +[nixos.md](./nixos.md) for the nix-shell development setup. -## 5. CORS / cookie note for the picture endpoint +## 6. Updating + +The whole motion is automated end-to-end: + +1. **Cut a release**: bump `version` in `package.json` on `main` (and the Maven version + in `keycloak-extensions/pom.xml` + module poms if the SPIs changed). CI builds the + theme and SPIs and publishes a GitHub release with all jars attached. +2. **Pins update themselves**: the `update_nixos_plugin_pins` CI job then runs + `scripts/update-nixos-plugin-pins.mjs --tag v`, which rewrites + `themeVersion`, `spiVersion`, and the four `sha256-…` hashes in this file from the + release assets' digests, and commits the result to `main`. +3. **Deploy**: copy the refreshed pin block from [§3](#3-full-nixos-module-example) into + your NixOS config (or `git pull` if you vendor this file) and run + `nixos-rebuild switch`. Keycloak restarts automatically because + `services.keycloak.plugins` changed. +4. If an SPI's config keys changed (check the release notes), update + `services.keycloak.settings` accordingly. + +> **Note**: even when `spiVersion` stays the same, the SPI jars are rebuilt for every +> release, so their hashes change. Always take the complete pin block for a given +> `themeVersion` — never mix hashes across releases. + +
+Manual fallback: computing a sha256 by hand + +```sh +nix-prefetch-url --type sha256 \ + "https://github.com/helpwave/id.helpwave.de/releases/download/v0.6.0/helpwave-picture-0.3.0.jar" +``` + +Or run `nixos-rebuild switch` once with a wrong hash and copy the `got: sha256-…` line +out of the error message. + +
+ +## 7. CORS / cookie note for the picture endpoint The Account Console talks to `/realms//helpwave-picture` from `https:///realms//account` — same origin, no extra CORS config needed. If you host the account console under a different origin, add `POST`, `DELETE`, `Authorization`, `credentials: include` to your reverse proxy's allow-list. -## 6. Updating - -1. Bump `themeVersion` (and `spiVersion` if it changed — check release notes). -2. Replace the four `sha256-…` placeholders with the new digests. -3. `nixos-rebuild switch`. Keycloak restarts automatically because - `services.keycloak.plugins` changed. -4. If the SPI's config keys changed, update `services.keycloak.settings` accordingly. - -## 7. Smoke test +## 8. Smoke test ```sh DOMAIN=id.helpwave.de diff --git a/package.json b/package.json index 369e856..fddb0bd 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "id.helpwave.de", - "version": "0.6.0", + "version": "0.6.1", "repository": { "type": "git", "url": "git://github.com/helpwave/id.helpwave.de.git" @@ -12,6 +12,7 @@ "storybook": "storybook dev -p 6006", "build-intl": "npx --package=@helpwave/internationalization build-intl --force -i ./locales -o ./src/i18n/translations.ts -n helpwaveIdTranslation", "check-translations": "node scripts/check-translation-keys.mjs", + "update-nix-pins": "node scripts/update-nixos-plugin-pins.mjs", "build": "npm run build-intl && tsc && vite build", "build-keycloak-theme": "npm run build && keycloakify build", "build-storybook": "storybook build", diff --git a/scripts/update-nixos-plugin-pins.mjs b/scripts/update-nixos-plugin-pins.mjs new file mode 100644 index 0000000..2dc3e93 --- /dev/null +++ b/scripts/update-nixos-plugin-pins.mjs @@ -0,0 +1,196 @@ +#!/usr/bin/env node +/** + * Updates the pinned plugin versions and sha256 (SRI) hashes in + * docs/deployment-nixos.md from a GitHub release of this repository. + * + * Usage: + * node scripts/update-nixos-plugin-pins.mjs # pin to the latest release + * node scripts/update-nixos-plugin-pins.mjs --tag v0.6.0 # pin to a specific release + * node scripts/update-nixos-plugin-pins.mjs --check # exit 1 if the doc is stale + * node scripts/update-nixos-plugin-pins.mjs --release-json ./release.json # offline/testing + * + * The sha256 values are taken from the `digest` field the GitHub API reports + * per release asset. If a digest is missing (very old releases), the asset is + * downloaded and hashed locally. Set GITHUB_TOKEN to avoid API rate limits. + */ + +import fs from 'node:fs' +import path from 'node:path' +import crypto from 'node:crypto' +import { fileURLToPath } from 'node:url' + +const REPO = 'helpwave/id.helpwave.de' +const THEME_JAR = 'keycloak-theme-for-kc-26.2-and-above.jar' +const SPI_ARTIFACTS = ['helpwave-captcha', 'helpwave-picture', 'helpwave-policy-acceptance'] + +const DOC_PATH = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../docs/deployment-nixos.md' +) + +function fail(message) { + console.error(`✖ ${message}`) + process.exit(1) +} + +function parseArgs(argv) { + const args = { tag: null, check: false, releaseJson: null } + for (let i = 0; i < argv.length; i++) { + switch (argv[i]) { + case '--tag': + args.tag = argv[++i] + break + case '--check': + args.check = true + break + case '--release-json': + args.releaseJson = argv[++i] + break + default: + fail(`Unknown argument: ${argv[i]}`) + } + } + return args +} + +async function githubApi(url) { + const headers = { 'Accept': 'application/vnd.github+json', 'User-Agent': REPO } + if (process.env.GITHUB_TOKEN) { + headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}` + } + const response = await fetch(url, { headers }) + if (!response.ok) { + fail(`GitHub API request failed: ${response.status} ${response.statusText} (${url})`) + } + return response.json() +} + +async function fetchRelease({ tag, releaseJson }) { + if (releaseJson) { + return JSON.parse(fs.readFileSync(releaseJson, 'utf8')) + } + const endpoint = tag + ? `https://api.github.com/repos/${REPO}/releases/tags/${tag.startsWith('v') ? tag : `v${tag}`}` + : `https://api.github.com/repos/${REPO}/releases/latest` + return githubApi(endpoint) +} + +/** GitHub reports digests as `sha256:`; Nix fetchurl wants SRI `sha256-`. */ +function digestToSri(digest) { + const match = /^sha256:([0-9a-f]{64})$/.exec(digest ?? '') + if (!match) return null + return `sha256-${Buffer.from(match[1], 'hex').toString('base64')}` +} + +async function downloadAndHash(asset) { + console.log(` no API digest for ${asset.name}, downloading to hash locally …`) + const response = await fetch(asset.browser_download_url, { redirect: 'follow' }) + if (!response.ok) { + fail(`Download failed: ${response.status} ${response.statusText} (${asset.browser_download_url})`) + } + const bytes = Buffer.from(await response.arrayBuffer()) + return `sha256-${crypto.createHash('sha256').update(bytes).digest('base64')}` +} + +async function assetSri(asset) { + return digestToSri(asset.digest) ?? await downloadAndHash(asset) +} + +function findAsset(release, predicate, description) { + const asset = (release.assets ?? []).find(predicate) + if (!asset) fail(`Release ${release.tag_name} has no asset matching: ${description}`) + return asset +} + +/** Replaces exactly one occurrence; fails loudly if the doc anchor shape changed. */ +function replaceOnce(content, pattern, replacement, description) { + const matches = content.match(new RegExp(pattern, 'g')) ?? [] + if (matches.length !== 1) { + fail( + `Expected exactly 1 match for ${description} in ${path.relative(process.cwd(), DOC_PATH)}, ` + + `found ${matches.length}. Did the pin block in the doc change shape?` + ) + } + return content.replace(pattern, replacement) +} + +async function main() { + const args = parseArgs(process.argv.slice(2)) + const release = await fetchRelease(args) + + const themeVersion = release.tag_name.replace(/^v/, '') + + const themeAsset = findAsset(release, (a) => a.name === THEME_JAR, THEME_JAR) + + const spiAssets = Object.fromEntries(SPI_ARTIFACTS.map((artifact) => { + const pattern = new RegExp(`^${artifact}-(\\d+\\.\\d+\\.\\d+(?:-[\\w.]+)?)\\.jar$`) + const asset = findAsset(release, (a) => pattern.test(a.name), `${artifact}-.jar`) + return [artifact, { asset, version: pattern.exec(asset.name)[1] }] + })) + + const spiVersions = new Set(Object.values(spiAssets).map(({ version }) => version)) + if (spiVersions.size !== 1) { + fail(`SPI jars in release ${release.tag_name} disagree on their version: ${[...spiVersions].join(', ')}`) + } + const spiVersion = [...spiVersions][0] + + console.log(`Release ${release.tag_name}: themeVersion=${themeVersion}, spiVersion=${spiVersion}`) + + const hashes = { + themePlugin: await assetSri(themeAsset), + captchaSPI: await assetSri(spiAssets['helpwave-captcha'].asset), + pictureSPI: await assetSri(spiAssets['helpwave-picture'].asset), + policySPI: await assetSri(spiAssets['helpwave-policy-acceptance'].asset), + } + for (const [binding, sri] of Object.entries(hashes)) { + console.log(` ${binding.padEnd(12)} ${sri}`) + } + + const original = fs.readFileSync(DOC_PATH, 'utf8') + let content = original + + content = replaceOnce( + content, + /themeVersion = "[^"]+";/, + `themeVersion = "${themeVersion}";`, + 'themeVersion pin' + ) + content = replaceOnce( + content, + /spiVersion = "[^"]+";/, + `spiVersion = "${spiVersion}";`, + 'spiVersion pin' + ) + + // Each binding is anchored to its jar file name so the hashes can never be + // assigned to the wrong plugin, no matter how the doc is reordered. + const bindingAnchors = { + themePlugin: THEME_JAR, + captchaSPI: 'helpwave-captcha-${spiVersion}.jar', + pictureSPI: 'helpwave-picture-${spiVersion}.jar', + policySPI: 'helpwave-policy-acceptance-${spiVersion}.jar', + } + for (const [binding, fileAnchor] of Object.entries(bindingAnchors)) { + const escapedAnchor = fileAnchor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + content = replaceOnce( + content, + new RegExp(`(release themeVersion "${escapedAnchor}"\\s*\\n\\s*")sha256-[A-Za-z0-9+/=]+(")`), + `$1${hashes[binding]}$2`, + `${binding} sha256` + ) + } + + if (content === original) { + console.log(`✔ ${path.relative(process.cwd(), DOC_PATH)} is already pinned to ${release.tag_name}`) + return + } + + if (args.check) { + fail(`docs are stale: pins do not match release ${release.tag_name}. Run: npm run update-nix-pins`) + } + + fs.writeFileSync(DOC_PATH, content) + console.log(`✔ Updated ${path.relative(process.cwd(), DOC_PATH)} to ${release.tag_name}`) +} + +await main() diff --git a/src/fonts.css b/src/fonts.css new file mode 100644 index 0000000..0fac072 --- /dev/null +++ b/src/fonts.css @@ -0,0 +1,15 @@ +@font-face { + font-family: "Inter"; + src: url("./fonts/Inter-Variable.ttf") format("truetype"); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Space Grotesk"; + src: url("./fonts/SpaceGrotesk-Variable.ttf") format("truetype"); + font-weight: 300 700; + font-style: normal; + font-display: swap; +} diff --git a/src/fonts/Inter-OFL.txt b/src/fonts/Inter-OFL.txt new file mode 100644 index 0000000..63db4fa --- /dev/null +++ b/src/fonts/Inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/fonts/Inter-Variable.ttf b/src/fonts/Inter-Variable.ttf new file mode 100644 index 0000000..e31b51e Binary files /dev/null and b/src/fonts/Inter-Variable.ttf differ diff --git a/src/fonts/SpaceGrotesk-OFL.txt b/src/fonts/SpaceGrotesk-OFL.txt new file mode 100644 index 0000000..6a31484 --- /dev/null +++ b/src/fonts/SpaceGrotesk-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/fonts/SpaceGrotesk-Variable.ttf b/src/fonts/SpaceGrotesk-Variable.ttf new file mode 100644 index 0000000..2c6cc59 Binary files /dev/null and b/src/fonts/SpaceGrotesk-Variable.ttf differ diff --git a/src/login/pages/Login.tsx b/src/login/pages/Login.tsx index a97e4ad..714ac10 100644 --- a/src/login/pages/Login.tsx +++ b/src/login/pages/Login.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { LogIn, KeyRound, UserPlus } from 'lucide-react' -import { Button, Input, FormFieldLayout, Checkbox } from '@helpwave/hightide' +import { Button, Input, FormFieldLayout, LabelledCheckbox } from '@helpwave/hightide' import type { KcContext } from '../KcContext' import { useI18n } from '../i18n' import Template from 'keycloakify/login/Template' @@ -109,15 +109,14 @@ export default function Login({ kcContext }: LoginProps) { {kcContext.realm?.rememberMe && ( -
- setRememberMe(value)} - onEditComplete={() => {}} - size="md" - /> - -
+ setRememberMe(value)} + style={{ marginTop: '-1rem', marginBottom: '0.5rem' }} + label={t('rememberMe')} + /> )}
diff --git a/src/login/pages/Register.tsx b/src/login/pages/Register.tsx index a125495..abc31e2 100644 --- a/src/login/pages/Register.tsx +++ b/src/login/pages/Register.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import { Button, Input, FormFieldLayout, Checkbox } from '@helpwave/hightide' +import { Button, Input, FormFieldLayout, LabelledCheckbox } from '@helpwave/hightide' import type { KcContext } from '../KcContext' import { useI18n } from '../i18n' import Template from 'keycloakify/login/Template' @@ -219,29 +219,27 @@ export default function Register({ kcContext }: RegisterProps) { )} {kcContext.termsAcceptanceRequired && ( -
- setTermsAccepted(value)} - onEditComplete={() => {}} - size="md" - /> - -
+ )} + /> )} {captchaEnabled && ( diff --git a/src/login/pages/Terms.tsx b/src/login/pages/Terms.tsx index eb2b761..19756be 100644 --- a/src/login/pages/Terms.tsx +++ b/src/login/pages/Terms.tsx @@ -1,5 +1,5 @@ import { ArrowLeft, Check } from 'lucide-react' -import { Button, Checkbox } from '@helpwave/hightide' +import { Button, LabelledCheckbox } from '@helpwave/hightide' import type { KcContext } from '../KcContext' import { useI18n } from '../i18n' import Template from 'keycloakify/login/Template' @@ -108,27 +108,17 @@ export default function Terms({ kcContext }: TermsProps) { )}
-
- setAccepted(value)} - onEditComplete={() => {}} - size="md" - /> - -
+ setAccepted(value)} + label={isPolicyVariant ? ( + + ) : ( + t('acceptTerms') + )} + /> {isPolicyVariant && kcContext.policyRequiredError && !accepted && (
diff --git a/src/main.tsx b/src/main.tsx index 8c0372d..1dd5891 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client' import { StrictMode } from 'react' import { KcPage } from './kc.gen' import '@helpwave/hightide/style/globals.css' +import './fonts.css' import './index.css' import { HelpwaveLogo, HightideProvider } from '@helpwave/hightide'