diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5081e2d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +dist_keycloak +.git +.github +README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7c55aca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: Identity CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: identity-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-theme: + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7.0.1 + + - uses: actions/setup-node@v7 + with: + node-version: 20 + + - name: Use pre-installed Temurin JDK 17 + run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" + + - name: Install dependencies + id: install + continue-on-error: true + run: | + set -o pipefail + npm install 2>&1 | tee ci-install.log + + - name: Typecheck and web build + id: check + if: steps.install.outcome == 'success' + continue-on-error: true + run: | + set -o pipefail + npm run check 2>&1 | tee ci-check.log + + - name: Build Keycloak theme + id: theme + if: steps.install.outcome == 'success' && steps.check.outcome == 'success' + continue-on-error: true + run: | + set -o pipefail + npm run build:keycloak 2>&1 | tee ci-theme.log + + - name: Upload diagnostics + if: always() + uses: actions/upload-artifact@v7.0.1 + with: + name: identity-ci-diagnostics + path: | + ci-install.log + ci-check.log + ci-theme.log + if-no-files-found: ignore + + - name: Upload theme JAR + if: steps.theme.outcome == 'success' + uses: actions/upload-artifact@v7.0.1 + with: + name: corelink-keycloak-theme + path: dist_keycloak/*.jar + if-no-files-found: error + + - name: Fail if validation failed + if: steps.install.outcome != 'success' || steps.check.outcome != 'success' || steps.theme.outcome != 'success' + run: exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..357dae3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + packages: write + +jobs: + release: + runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7.0.1 + + - uses: actions/setup-node@v7 + with: + node-version: 20 + + - name: Use pre-installed Temurin JDK 17 + run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" + + - name: Install dependencies + run: npm install + + - name: Build Keycloak theme JAR + run: npm run build:keycloak + + - name: Generate checksums + run: sha256sum dist_keycloak/*.jar > SHA256SUMS + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "${GITHUB_REF_NAME}" dist_keycloak/*.jar SHA256SUMS --generate-notes --title "CoreLink Identity ${GITHUB_REF_NAME}" + + - name: Setup Blacksmith Docker builder + uses: useblacksmith/setup-docker-builder@v1 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Keycloak image + uses: useblacksmith/build-push-action@v2 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + ghcr.io/corelinkplatform/identity:${{ github.ref_name }} + ghcr.io/corelinkplatform/identity:latest + provenance: mode=max + sbom: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac3b2ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +dist_keycloak/ +*.jar +.env +.env.* +!.env.example +.DS_Store +.idea/ +.vscode/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..8f731ae --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{"semi":true,"singleQuote":false,"tabWidth":2,"trailingComma":"none","printWidth":100} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..293b6b6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +ARG KEYCLOAK_VERSION=26.7.2 + +FROM node:22-alpine AS theme-builder +WORKDIR /src +ARG KEYCLOAK_THEME_NAME=corelink +ARG VITE_BRAND_NAME=CoreLink +ARG VITE_BRAND_TAGLINE="Secure connected intelligence" +ENV KEYCLOAK_THEME_NAME=${KEYCLOAK_THEME_NAME} +ENV VITE_BRAND_NAME=${VITE_BRAND_NAME} +ENV VITE_BRAND_TAGLINE=${VITE_BRAND_TAGLINE} +COPY package.json ./ +RUN npm pkg set name="${KEYCLOAK_THEME_NAME}" && npm install +COPY . . +RUN npm run build:keycloak \ + && cp dist_keycloak/*-26.2-and-above.jar /tmp/identity-theme.jar + +FROM quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} AS keycloak-builder +COPY --from=theme-builder /tmp/identity-theme.jar /opt/keycloak/providers/identity-theme.jar +RUN /opt/keycloak/bin/kc.sh build + +FROM quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} +COPY --from=keycloak-builder /opt/keycloak/ /opt/keycloak/ +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] +CMD ["start", "--optimized"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..24879ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CoreLink Platform + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.fa.md b/README.fa.md new file mode 100644 index 0000000..4b39616 --- /dev/null +++ b/README.fa.md @@ -0,0 +1,89 @@ +# CoreLink Identity + +یک قالب مدرن، قابل استفاده مجدد و مناسب محیط Production برای صفحه‌های احراز هویت Keycloak که با **Keycloakify، React، TypeScript و Vite** ساخته شده است. برند پیش‌فرض CoreLink است، اما نام theme، نام برند، شعار، لوگو، رنگ‌ها و ترجمه‌ها قابل تغییر هستند و پروژه می‌تواند برای هر محصول دیگری استفاده شود. + +[English README](README.md) · [راهنمای نصب فارسی](docs/INSTALLATION.fa.md) · [Branding](docs/BRANDING.md) · [راهنمای Release](docs/RELEASES.md) + +## امکانات + +- رابط مدرن برای احراز هویت Keycloak بدون ظاهر پیش‌فرض Keycloak. +- خروجی JAR قابل نصب برای Keycloak 26.2 به بالا. +- پشتیبانی انگلیسی و فارسی با RTL واقعی. +- پوشش flowهای استاندارد ورود، ثبت‌نام، فراموشی رمز، تأیید ایمیل، OTP/TOTP و required actionها. +- امکان تغییر branding هنگام build. +- Docker image چندمرحله‌ای Keycloak با theme از پیش نصب‌شده. +- CI برای بررسی build. +- Release خودکار شامل JAR آماده نصب، checksum و Docker image در GHCR. + +## سازگاری + +نسخه پایه فعلی Docker روی **Keycloak 26.7.2** است و theme برای `26.2-and-above` ساخته می‌شود. + +## اجرای سریع برای توسعه + +نیازمندی‌ها: Node.js 20 یا بالاتر و npm. + +```bash +git clone https://github.com/CoreLinkPlatform/Identity.git +cd Identity +npm install +npm run dev +``` + +ساخت فایل قابل نصب روی Keycloak: + +```bash +npm run build:keycloak +``` + +خروجی پیش‌فرض: + +```text +dist_keycloak/corelink-26.2-and-above.jar +``` + +برای نصب روی Keycloak موجود، فایل JAR را داخل `/opt/keycloak/providers/` قرار بده، `kc.sh build` را اجرا کن، Keycloak را restart کن و در **Realm settings → Themes → Login theme** مقدار `corelink` را انتخاب کن. جزئیات کامل در [docs/INSTALLATION.fa.md](docs/INSTALLATION.fa.md) آمده است. + +## استفاده برای محصول خودتان + +این repository به هیچ backend اختصاصی CoreLink وابسته نیست و می‌توان آن را fork و rebrand کرد. + +در Keycloakify نام واقعی theme از `package.json:name` گرفته می‌شود. پس در fork خودتان ابتدا نام package را عوض کنید و بعد branding ظاهری را هنگام build بدهید: + +```bash +npm pkg set name=my-product +VITE_BRAND_NAME="My Product" \ +VITE_BRAND_TAGLINE="Secure access" \ +npm run build:keycloak +``` + +بعد theme با نام `my-product` در Keycloak قابل انتخاب است. برای لوگو می‌توان فایل `public/img/corelink-mark.svg` را جایگزین کرد یا در build سورس مقدار `VITE_BRAND_MARK` را تعیین کرد. راهنمای کامل branding در [docs/BRANDING.md](docs/BRANDING.md) است. + +در Docker، مقدار `KEYCLOAK_THEME_NAME` داخل build stage نام package/theme را هم تنظیم می‌کند: + +```bash +docker build \ + --build-arg KEYCLOAK_THEME_NAME=my-product \ + --build-arg VITE_BRAND_NAME="My Product" \ + --build-arg VITE_BRAND_TAGLINE="Secure access" \ + -t my-product-keycloak . +``` + +## فایل‌های Release + +با push کردن tagهایی مثل `v0.1.0`، GitHub Actions این خروجی‌ها را منتشر می‌کند: + +- `corelink-26.2-and-above.jar` — فایل نصب مستقیم نسخه رسمی CoreLink روی Keycloak. +- `SHA256SUMS` — checksum برای بررسی صحت فایل. +- `ghcr.io/corelinkplatform/identity:` — image آماده Keycloak همراه theme. +- `ghcr.io/corelinkplatform/identity:latest` — آخرین image منتشرشده. + +راهنمای استفاده از فایل‌های Release و روند انتشار در [docs/RELEASES.md](docs/RELEASES.md) است. + +## مرزبندی امنیتی repository + +این repository عمداً شامل secret، password، realm credential، اطلاعات tenant یا تنظیمات Production CoreLink نیست. provisioning مربوط به realm/client و تنظیمات Compose/Traefik باید در لایه Deployment نگهداری شود. + +## مجوز + +MIT. استفاده شخصی و تجاری، fork، تغییر و بازتوزیع مطابق شرایط مجوز آزاد است. diff --git a/README.md b/README.md index 6742271..993d69e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,89 @@ # CoreLink Identity -CoreLink Identity is the branded authentication and account experience for CoreLink Platform, built on Keycloak and Keycloakify. +A reusable, production-oriented Keycloak login theme built with **Keycloakify**, **React**, **TypeScript**, and **Vite**. It ships with CoreLink branding by default, but the theme name, visible brand name, tagline, logo asset, colors, and translations can be replaced for another product. -> Initial scaffold. See the `feat/corelink-identity-foundation` pull request for the complete foundation. +[راهنمای فارسی](README.fa.md) · [Installation](docs/INSTALLATION.md) · [Branding](docs/BRANDING.md) · [Releases](docs/RELEASES.md) + +## What it provides + +- Modern Keycloak authentication UI without exposing the default Keycloak look. +- Keycloak 26.2+ theme JAR output. +- English and Persian support with RTL handling. +- Standard Keycloak login, registration, reset-password, verification, OTP/TOTP and required-action flows. +- Build-time brand configuration. +- Multi-stage Keycloak Docker image. +- CI build validation. +- Tagged releases containing a ready-to-install JAR, SHA-256 checksums and a GHCR image. + +## Compatibility + +The current image baseline is **Keycloak 26.7.2**. The generated theme target is `26.2-and-above`. + +## Quick start + +Requirements: Node.js 20+ and npm. + +```bash +git clone https://github.com/CoreLinkPlatform/Identity.git +cd Identity +npm install +npm run dev +``` + +Build the installable Keycloak theme: + +```bash +npm run build:keycloak +``` + +The generated JAR is written to: + +```text +dist_keycloak/corelink-26.2-and-above.jar +``` + +Install it into an existing Keycloak instance by copying the JAR to `/opt/keycloak/providers/`, running `kc.sh build`, restarting Keycloak, and selecting `corelink` under **Realm settings → Themes → Login theme**. See [docs/INSTALLATION.md](docs/INSTALLATION.md) for complete Docker and bare-Keycloak instructions. + +## Use it for your own product + +No CoreLink backend is required. The repository can be forked and rebranded independently. + +Keycloakify derives the actual Keycloak theme name from `package.json:name`. For a fork, set that first, then build with your visible branding: + +```bash +npm pkg set name=my-product +VITE_BRAND_NAME="My Product" \ +VITE_BRAND_TAGLINE="Secure access" \ +npm run build:keycloak +``` + +The resulting theme is selectable as `my-product` in Keycloak. Replace `public/img/corelink-mark.svg` or set `VITE_BRAND_MARK` when building from source. Colors and layout are documented in [docs/BRANDING.md](docs/BRANDING.md). + +For Docker, `KEYCLOAK_THEME_NAME` updates the package/theme name inside the build stage automatically: + +```bash +docker build \ + --build-arg KEYCLOAK_THEME_NAME=my-product \ + --build-arg VITE_BRAND_NAME="My Product" \ + --build-arg VITE_BRAND_TAGLINE="Secure access" \ + -t my-product-keycloak . +``` + +## Releases + +On tags matching `v*`, GitHub Actions publishes: + +- `corelink-26.2-and-above.jar` — installable Keycloak theme for official CoreLink releases. +- `SHA256SUMS` — integrity checksum file. +- `ghcr.io/corelinkplatform/identity:` — Keycloak image with the CoreLink theme pre-installed. +- `ghcr.io/corelinkplatform/identity:latest` — latest tagged image. + +See [docs/RELEASES.md](docs/RELEASES.md) for installation from release files and release-maintainer instructions. + +## Repository boundaries + +This project intentionally does **not** contain production secrets, realm credentials, database passwords, tenant data, or CoreLink deployment configuration. Realm/client provisioning and Compose/Traefik integration belong in the deployment layer. + +## License + +MIT. You can fork, modify, redistribute, and use it commercially subject to the license terms. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..3c548b7 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,31 @@ +# CoreLink Identity architecture + +## Scope + +Identity owns the visual authentication surface delivered by Keycloak. Business authorization remains in CoreLink Platform; realm/client provisioning belongs to Deployment. + +## Boundaries + +- **Identity:** Keycloakify theme, brand tokens, translations, theme JAR, Keycloak image contract. +- **Deployment:** database, realm bootstrap/import, clients, redirect URIs, secrets, reverse proxy, health checks. +- **Console:** OIDC client integration and application session UX. + +## Theme contract + +- Theme name: `corelink` +- Supported build target: Keycloak 26.2+ +- Production baseline: Keycloak 26.7.2 +- Default languages: English + Persian; Arabic remains compatible through Keycloak's built-in i18n. +- Direction is derived from active language and set on ``. + +## Security rules + +1. Authentication JavaScript/CSS/assets ship inside the theme JAR. +2. Do not load executable resources from Console or tenant domains. +3. Never commit realm secrets or client secrets to this repository. +4. Pin Keycloak production images; upgrades require rebuilding and smoke-testing the theme. +5. CSP and proxy headers are configured by Deployment, not by theme code. + +## White-label roadmap + +Tenant branding should be data-driven and restricted to safe values (logo URL or bundled asset, color tokens, display name). Executable HTML/JS is never tenant-configurable. diff --git a/docs/BRANDING.md b/docs/BRANDING.md new file mode 100644 index 0000000..0526e08 --- /dev/null +++ b/docs/BRANDING.md @@ -0,0 +1,96 @@ +# Branding and Reuse + +CoreLink Identity is intentionally reusable. CoreLink is the default brand, not a runtime dependency. + +## Theme name vs visible brand + +Keycloakify derives the actual Keycloak theme identifier from `package.json:name`. The default package name is `corelink`, so the theme appears as `corelink` in Realm settings. + +For a source fork: + +```bash +npm pkg set name=acme +``` + +For the provided Dockerfile, use: + +```bash +--build-arg KEYCLOAK_THEME_NAME=acme +``` + +The Dockerfile updates the package name inside the build stage automatically. + +## Visible branding options + +| Variable | Default | Purpose | +|---|---|---| +| `VITE_BRAND_NAME` | `CoreLink` | Visible product/brand name. | +| `VITE_BRAND_TAGLINE` | `Secure connected intelligence` | Footer tagline. | +| `VITE_BRAND_MARK` | `img/corelink-mark.svg` | Logo path relative to Keycloak theme resources. | + +Source-build example: + +```bash +npm pkg set name=acme +VITE_BRAND_NAME="Acme Cloud" \ +VITE_BRAND_TAGLINE="Identity for your workspace" \ +VITE_BRAND_MARK="img/acme.svg" \ +npm run build:keycloak +``` + +## Logo + +The default asset lives at: + +```text +public/img/corelink-mark.svg +``` + +For a fork, replace it with your own SVG or add another file under `public/img/` and set `VITE_BRAND_MARK`. + +Prefer SVG for crisp rendering. Do not embed secrets, tenant identifiers, tracking code, remote scripts or private URLs in theme assets. + +## Colors and layout + +Authentication styles are currently centralized in: + +```text +src/styles/corelink.css +``` + +A public fork may rename that file, but keep design tokens and structural rules centralized rather than scattering product-specific values through individual Keycloak pages. + +When modifying layout, preserve: + +- keyboard focus visibility; +- sufficient color contrast; +- responsive behaviour; +- logical CSS properties for RTL support; +- Keycloak form semantics and error messages. + +## Persian and RTL + +Standard translations, including Persian, are provided through Keycloakify's login i18n layer. The CoreLink-specific i18n wrapper is at: + +```text +src/login/i18n.ts +``` + +The page applies `dir=rtl` automatically for Persian (`fa`) and Arabic (`ar`). Prefer logical properties such as `margin-inline-start`, `padding-inline-end` and `inset-inline-start` instead of hard-coded left/right rules. + +## White-label / multi-tenant branding + +The current implementation uses build-time branding. This is deliberately simple and safe for the first public version. + +For installations that need different branding per realm without rebuilding, add a separately reviewed realm-brand configuration mechanism or Keycloak SPI. Do not allow arbitrary stored HTML or JavaScript. Validate URLs/colors strictly and provide safe bundled fallbacks. + +## Public fork checklist + +1. Change `package.json:name` to the desired Keycloak theme identifier. +2. Set `VITE_BRAND_NAME` and `VITE_BRAND_TAGLINE`. +3. Replace or configure the logo asset. +4. Adjust colors/styles. +5. Update repository metadata and documentation. +6. Build a JAR and select the new package/theme name in Keycloak. + +`private: true` in `package.json` only prevents accidental npm publication. It does **not** make the GitHub repository private and does not restrict use under the MIT license. diff --git a/docs/DEPLOYMENT-CONTRACT.md b/docs/DEPLOYMENT-CONTRACT.md new file mode 100644 index 0000000..7dfaa91 --- /dev/null +++ b/docs/DEPLOYMENT-CONTRACT.md @@ -0,0 +1,42 @@ +# Deployment contract + +This repository intentionally does not contain CoreLink production credentials or realm exports. + +## Image + +Build: + +```bash +docker build -t ghcr.io/corelinkplatform/identity:26.7.2-corelink.1 . +``` + +The resulting image contains the `corelink` login theme and starts Keycloak with `start --optimized`. + +## Required runtime configuration + +Deployment must provide at minimum: + +- `KC_DB` +- `KC_DB_URL` +- `KC_DB_USERNAME` +- `KC_DB_PASSWORD` +- `KC_HOSTNAME` +- bootstrap admin credentials only for first initialization + +Behind a reverse proxy, Deployment must configure Keycloak's proxy/hostname settings for the actual topology. + +## Realm configuration + +Set **Realm settings → Themes → Login theme** to `corelink`. + +Before production integration, test at minimum: + +- password login +- registration (if enabled) +- forgot/reset password +- email verification +- OTP/TOTP +- required actions / update password +- invalid credentials and system error pages +- Persian RTL and English LTR +- mobile viewport diff --git a/docs/INSTALLATION.fa.md b/docs/INSTALLATION.fa.md new file mode 100644 index 0000000..52e6430 --- /dev/null +++ b/docs/INSTALLATION.fa.md @@ -0,0 +1,150 @@ +# راهنمای نصب CoreLink Identity + +این راهنما سه روش اصلی استفاده را پوشش می‌دهد: نصب JAR منتشرشده روی Keycloak موجود، استفاده از Docker image آماده، یا ساخت نسخه اختصاصی با برند خودتان. + +## ۱. نصب JAR روی Keycloak موجود + +### نیازمندی‌ها + +- Keycloak نسخه 26.2 یا بالاتر. +- دسترسی به مسیر `providers` در Keycloak. +- امکان rebuild/restart کردن Keycloak. + +### مراحل + +1. از بخش Releases فایل `corelink-26.2-and-above.jar` و `SHA256SUMS` را دانلود کنید. +2. صحت فایل را بررسی کنید: + +```bash +sha256sum -c SHA256SUMS +``` + +3. فایل JAR را داخل Keycloak کپی کنید: + +```bash +cp corelink-26.2-and-above.jar /opt/keycloak/providers/corelink-theme.jar +``` + +4. Keycloak را rebuild کنید: + +```bash +/opt/keycloak/bin/kc.sh build +``` + +5. سرویس را restart کنید. +6. در Admin Console مسیر زیر را باز کنید: + +```text +Realm settings → Themes → Login theme → corelink +``` + +7. تنظیم را Save کنید و login واقعی همان realm را تست کنید. + +### اگر Keycloak با Docker اجرا می‌شود + +بهتر است به‌جای mount کردن JAR در runtime یک image مشتق‌شده بسازید: + +```dockerfile +FROM quay.io/keycloak/keycloak:26.7.2 AS builder +COPY corelink-26.2-and-above.jar /opt/keycloak/providers/corelink-theme.jar +RUN /opt/keycloak/bin/kc.sh build + +FROM quay.io/keycloak/keycloak:26.7.2 +COPY --from=builder /opt/keycloak/ /opt/keycloak/ +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] +CMD ["start", "--optimized"] +``` + +## ۲. اجرای Docker image آماده GHCR + +```bash +docker pull ghcr.io/corelinkplatform/identity:v0.1.0 +``` + +برای تست محلی: + +```bash +docker run --rm -p 8080:8080 \ + -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ + -e KC_BOOTSTRAP_ADMIN_PASSWORD=change-me \ + ghcr.io/corelinkplatform/identity:v0.1.0 \ + start-dev +``` + +برای Production از `start-dev` استفاده نکنید. دیتابیس، hostname، TLS/proxy و secretها باید توسط سیستم Deployment تأمین شوند. + +## ۳. ساخت نسخه CoreLink از سورس + +نیازمندی‌ها: Node.js 20+ و npm. + +```bash +git clone https://github.com/CoreLinkPlatform/Identity.git +cd Identity +npm install +npm run build:keycloak +``` + +خروجی: + +```text +dist_keycloak/corelink-26.2-and-above.jar +``` + +## ۴. ساخت fork با برند اختصاصی + +در Keycloakify نام واقعی theme از `package.json:name` گرفته می‌شود. در fork خودتان ابتدا نام package را تغییر دهید: + +```bash +npm pkg set name=acme +VITE_BRAND_NAME="Acme" \ +VITE_BRAND_TAGLINE="Secure workspace" \ +npm run build:keycloak +``` + +بعد theme با نام `acme` در Realm settings قابل انتخاب خواهد بود. برای لوگو فایل `public/img/corelink-mark.svg` را جایگزین کنید یا `VITE_BRAND_MARK` را روی resource دیگری بگذارید. + +## ۵. ساخت Docker image اختصاصی + +Dockerfile این repository مقدار `KEYCLOAK_THEME_NAME` را به نام package/theme تبدیل می‌کند: + +```bash +docker build \ + --build-arg KEYCLOAK_VERSION=26.7.2 \ + --build-arg KEYCLOAK_THEME_NAME=acme \ + --build-arg VITE_BRAND_NAME="Acme" \ + --build-arg VITE_BRAND_TAGLINE="Secure workspace" \ + -t acme-keycloak:26.7.2 . +``` + +## ۶. تنظیم Realm + +این repository عمداً Realm یا Client ایجاد نمی‌کند. بعد از نصب theme، realm را به‌صورت معمول تنظیم کنید و theme را در Realm settings انتخاب کنید. realm export، client secret، password دیتابیس و تنظیمات Production باید در repository مربوط به Deployment نگهداری شوند. + +## ۷. روند Upgrade + +1. Release Notes نسخه جدید را بخوانید. +2. طبق رویه عملیاتی خود از دیتابیس و تنظیمات Realm backup بگیرید. +3. JAR یا Docker image قبلی را با نسخه tagشده جدید جایگزین کنید. +4. در نصب دستی JAR دوباره `kc.sh build` را اجرا کنید. +5. Keycloak را restart کنید. +6. login، register، reset password، verify email، OTP/TOTP و required actionهای اختصاصی را تست کنید. + +چند نسخه از یک theme را هم‌زمان داخل `providers/` قرار ندهید. + +## ۸. رفع اشکال + +### Theme در لیست دیده نمی‌شود + +وجود JAR در `/opt/keycloak/providers/` را بررسی کنید، `kc.sh build` را دوباره اجرا کنید، سرویس را restart کنید و مطمئن شوید نام انتخاب‌شده با `package.json:name` زمان build یکی است؛ در Docker این نام از `KEYCLOAK_THEME_NAME` گرفته می‌شود. + +### استایل قدیمی نمایش داده می‌شود + +Keycloak را restart و cache مرورگر را پاک کنید. + +### فارسی نمایش داده نمی‌شود + +Internationalization را در Realm فعال کنید و زبان فارسی (`fa`) را به Supported locales اضافه کنید. رابط برای فارسی و عربی به‌صورت خودکار RTL می‌شود. + +### Container بالا می‌آید ولی Production configuration خطا دارد + +Image فقط Keycloak و theme را بسته‌بندی می‌کند. دیتابیس، hostname، proxy/TLS، provisioning و secretها باید از لایه Deployment تأمین شوند. diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md new file mode 100644 index 0000000..8f309da --- /dev/null +++ b/docs/INSTALLATION.md @@ -0,0 +1,150 @@ +# Installation Guide + +This guide covers three supported ways to use CoreLink Identity: install the released JAR into an existing Keycloak, run the prebuilt container image, or build a custom-branded image. + +## 1. Install a released JAR into existing Keycloak + +### Requirements + +- Keycloak 26.2 or newer. +- Access to the Keycloak `providers` directory. +- Permission to restart/rebuild the Keycloak installation. + +### Steps + +1. Download `corelink-26.2-and-above.jar` and `SHA256SUMS` from the GitHub Release. +2. Verify the download: + +```bash +sha256sum -c SHA256SUMS +``` + +3. Copy the JAR into Keycloak: + +```bash +cp corelink-26.2-and-above.jar /opt/keycloak/providers/corelink-theme.jar +``` + +4. Rebuild Keycloak: + +```bash +/opt/keycloak/bin/kc.sh build +``` + +5. Restart Keycloak. +6. Open Admin Console and select: + +```text +Realm settings → Themes → Login theme → corelink +``` + +7. Save and exercise a real login flow for that realm. + +### Existing Docker-based Keycloak + +Prefer a derived image over runtime mounting: + +```dockerfile +FROM quay.io/keycloak/keycloak:26.7.2 AS builder +COPY corelink-26.2-and-above.jar /opt/keycloak/providers/corelink-theme.jar +RUN /opt/keycloak/bin/kc.sh build + +FROM quay.io/keycloak/keycloak:26.7.2 +COPY --from=builder /opt/keycloak/ /opt/keycloak/ +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] +CMD ["start", "--optimized"] +``` + +## 2. Run the released GHCR image + +```bash +docker pull ghcr.io/corelinkplatform/identity:v0.1.0 +``` + +Local smoke test: + +```bash +docker run --rm -p 8080:8080 \ + -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \ + -e KC_BOOTSTRAP_ADMIN_PASSWORD=change-me \ + ghcr.io/corelinkplatform/identity:v0.1.0 \ + start-dev +``` + +For production, do not use `start-dev`. Supply a production database, hostname, TLS/proxy configuration and secrets through your deployment system. + +## 3. Build the CoreLink theme from source + +Requirements: Node.js 20+ and npm. + +```bash +git clone https://github.com/CoreLinkPlatform/Identity.git +cd Identity +npm install +npm run build:keycloak +``` + +Output: + +```text +dist_keycloak/corelink-26.2-and-above.jar +``` + +## 4. Build a custom-branded fork + +Keycloakify derives the actual theme identifier from `package.json:name`. Change it in your fork before building: + +```bash +npm pkg set name=acme +VITE_BRAND_NAME="Acme" \ +VITE_BRAND_TAGLINE="Secure workspace" \ +npm run build:keycloak +``` + +The generated theme will be selectable as `acme`. The JAR artifact is named using the configured artifact/theme name. Replace `public/img/corelink-mark.svg` or set `VITE_BRAND_MARK` to another resource path. + +## 5. Build a custom Keycloak image + +The Dockerfile performs the package-name change automatically from `KEYCLOAK_THEME_NAME`: + +```bash +docker build \ + --build-arg KEYCLOAK_VERSION=26.7.2 \ + --build-arg KEYCLOAK_THEME_NAME=acme \ + --build-arg VITE_BRAND_NAME="Acme" \ + --build-arg VITE_BRAND_TAGLINE="Secure workspace" \ + -t acme-keycloak:26.7.2 . +``` + +## 6. Realm configuration + +This repository does not create realms or clients. Configure the realm normally and select the installed login theme under Realm settings. Production realm exports, client secrets and database credentials belong in a separate deployment/configuration repository. + +## 7. Upgrade procedure + +1. Read release notes. +2. Back up realm configuration and the Keycloak database according to your operations policy. +3. Replace the old theme JAR or container image with the new tagged version. +4. Run `kc.sh build` for manual JAR installation. +5. Restart Keycloak. +6. Verify login, registration, password reset, email verification, OTP/TOTP and custom required actions. + +Do not deploy multiple versions of the same theme JAR into `providers/` simultaneously. + +## 8. Troubleshooting + +### Theme is not visible + +Confirm the JAR is under `/opt/keycloak/providers/`, rerun `kc.sh build`, restart Keycloak and make sure the selected theme name matches the `package.json:name` used during the build (or `KEYCLOAK_THEME_NAME` for the provided Dockerfile). + +### Old styles are still shown + +Restart Keycloak and clear browser cache. Production theme caching can hide recent development changes. + +### Persian is not shown + +Enable realm internationalization and add Persian (`fa`) to Supported locales. The UI automatically applies RTL direction for Persian and Arabic. + +### Container starts but production configuration fails + +The image packages Keycloak and the theme only. Database, hostname, proxy/TLS, realm provisioning and secrets must be supplied by the deployment layer. diff --git a/docs/RELEASES.md b/docs/RELEASES.md new file mode 100644 index 0000000..25bef53 --- /dev/null +++ b/docs/RELEASES.md @@ -0,0 +1,66 @@ +# Releases + +Tagged releases are the supported distribution channel for prebuilt CoreLink Identity artifacts. + +## Published artifacts + +For a tag such as `v0.1.0`, the Release workflow publishes: + +| Artifact | Purpose | +|---|---| +| `corelink-26.2-and-above.jar` | Install directly into Keycloak 26.2+ under `/opt/keycloak/providers/`. | +| `SHA256SUMS` | Verify the integrity of downloaded JAR files. | +| `ghcr.io/corelinkplatform/identity:v0.1.0` | Versioned Keycloak image containing the theme. | +| `ghcr.io/corelinkplatform/identity:latest` | Convenience image pointing at the latest tagged release. | + +The GitHub source archives (`Source code (zip)` and `Source code (tar.gz)`) are generated automatically by GitHub. + +## Install from a release JAR + +```bash +sha256sum -c SHA256SUMS +cp corelink-26.2-and-above.jar /opt/keycloak/providers/corelink-theme.jar +/opt/keycloak/bin/kc.sh build +``` + +Restart Keycloak and choose `corelink` under **Realm settings → Themes → Login theme**. + +## Install from the released container + +```bash +docker pull ghcr.io/corelinkplatform/identity:v0.1.0 +``` + +Use an immutable version tag in production. Avoid relying on `latest` for reproducible deployments. + +## Creating a release (maintainers) + +1. Ensure CI is green on `main`. +2. Update the package version and release notes/changelog when applicable. +3. Create and push a semantic version tag: + +```bash +git tag -a v0.1.0 -m "CoreLink Identity v0.1.0" +git push origin v0.1.0 +``` + +4. The `Release` GitHub Actions workflow builds the JAR, generates `SHA256SUMS`, creates the GitHub Release and publishes the GHCR images. +5. Verify the release page contains the expected JAR and checksum file. +6. Pull the tagged image and run a smoke test against a test realm before promoting it to production. + +## Versioning policy + +Use Semantic Versioning for this repository: + +- **PATCH**: styling/translation fixes without changing expected Keycloak integration. +- **MINOR**: new pages, branding features, supported flows or backwards-compatible integration capabilities. +- **MAJOR**: incompatible theme/configuration contract changes. + +Keycloak compatibility is documented independently from the project version. A project version such as `v1.2.0` does not imply Keycloak `1.2`; always check the JAR filename and release notes. + +## Release security + +- Never place realm secrets, admin credentials, database passwords or private certificates in release assets. +- Verify `SHA256SUMS` before manual JAR installation. +- Pin versioned image tags in production. +- Rebuild and release when the pinned Keycloak base image receives relevant security updates. diff --git a/index.html b/index.html new file mode 100644 index 0000000..55198dc --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + CoreLink Identity + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..b3b8db6 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "corelink", + "version": "0.1.0", + "private": true, + "description": "Reusable Keycloak identity theme with CoreLink defaults", + "repository": {"type":"git","url":"https://github.com/CoreLinkPlatform/Identity.git"}, + "type": "module", + "scripts": { + "dev": "vite", + "typecheck": "tsc --noEmit", + "build": "tsc && vite build", + "build:keycloak": "npm run build && keycloakify build", + "format": "prettier . --write", + "check": "npm run typecheck && npm run build" + }, + "license": "MIT", + "dependencies": { + "keycloakify": "^11.15.13", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "@types/react": "^18.3.24", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.7.0", + "prettier": "^3.6.2", + "typescript": "^5.9.2", + "vite": "^5.4.21" + }, + "engines": {"node": ">=20"} +} diff --git a/public/brand/corelink-mark.svg b/public/brand/corelink-mark.svg new file mode 100644 index 0000000..58264af --- /dev/null +++ b/public/brand/corelink-mark.svg @@ -0,0 +1 @@ + diff --git a/public/img/corelink-mark.svg b/public/img/corelink-mark.svg new file mode 100644 index 0000000..58264af --- /dev/null +++ b/public/img/corelink-mark.svg @@ -0,0 +1 @@ + diff --git a/src/kc.gen.tsx b/src/kc.gen.tsx new file mode 100644 index 0000000..f6b5a50 --- /dev/null +++ b/src/kc.gen.tsx @@ -0,0 +1,15 @@ +/* eslint-disable */ +// @ts-nocheck +import { lazy, Suspense, type ReactNode } from "react"; +export type ThemeName = "corelink"; +export const themeNames: ThemeName[] = ["corelink"]; +export type KcEnvName = never; +export const kcEnvNames: KcEnvName[] = []; +export const kcEnvDefaults: Record = {}; +export type KcContext = import("./login/KcContext").KcContext; +declare global { interface Window { kcContext?: KcContext; } } +export const KcLoginPage = lazy(() => import("./login/KcPage")); +export function KcPage(props: { kcContext: KcContext; fallback?: ReactNode }) { + return {props.kcContext.themeType === "login" ? : null}; +} +export const BASE_URL = import.meta.env.BASE_URL; diff --git a/src/login/KcContext.ts b/src/login/KcContext.ts new file mode 100644 index 0000000..ca8a79f --- /dev/null +++ b/src/login/KcContext.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ +import type { ExtendKcContext } from "keycloakify/login"; +import type { KcEnvName, ThemeName } from "../kc.gen"; + +export type KcContextExtension = { + themeName: ThemeName; + properties: Record & {}; +}; +export type KcContextExtensionPerPage = {}; +export type KcContext = ExtendKcContext; diff --git a/src/login/KcPage.tsx b/src/login/KcPage.tsx new file mode 100644 index 0000000..c3193e0 --- /dev/null +++ b/src/login/KcPage.tsx @@ -0,0 +1,44 @@ +import { lazy, Suspense, useEffect } from "react"; +import DefaultPage from "keycloakify/login/DefaultPage"; +import Template from "keycloakify/login/Template"; +import type { KcContext } from "./KcContext"; +import { useI18n } from "./i18n"; + +const UserProfileFormFields = lazy(() => import("keycloakify/login/UserProfileFormFields")); + +const brandName = import.meta.env.VITE_BRAND_NAME || "CoreLink"; +const brandTagline = import.meta.env.VITE_BRAND_TAGLINE || "Secure connected intelligence"; +const brandMark = import.meta.env.VITE_BRAND_MARK || "img/corelink-mark.svg"; + +export default function KcPage({ kcContext }: { kcContext: KcContext }) { + const { i18n } = useI18n({ kcContext }); + const lang = i18n.currentLanguage.languageTag; + const isRtl = lang === "fa" || lang === "ar"; + + useEffect(() => { + document.documentElement.lang = lang; + document.documentElement.dir = isRtl ? "rtl" : "ltr"; + document.body.dataset.identityPage = kcContext.pageId; + }, [lang, isRtl, kcContext.pageId]); + + return ( + +
+ {`${brandName} + {brandName} +
+ +
+ {brandName} Identity{brandTagline} +
+
+ ); +} diff --git a/src/login/i18n.ts b/src/login/i18n.ts new file mode 100644 index 0000000..04d556f --- /dev/null +++ b/src/login/i18n.ts @@ -0,0 +1,12 @@ +import { i18nBuilder } from "keycloakify/login"; +import type { ThemeName } from "../kc.gen"; + +// Keycloakify already provides the standard Keycloak login translations, +// including Persian. Keep product branding outside translation keys so forks +// can rebrand without rewriting authentication copy. +const { useI18n, ofTypeI18n } = i18nBuilder + .withThemeName() + .build(); + +export type I18n = typeof ofTypeI18n; +export { useI18n }; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..4f81e4b --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,21 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { KcPage } from "./kc.gen"; +import "./styles/corelink.css"; + +createRoot(document.getElementById("root")!).render( + + {window.kcContext ? ( + + ) : ( +
+
+ CoreLink +

CoreLink Identity

+

Run this theme in a Keycloak context to preview authentication pages.

+ npm run build:keycloak +
+
+ )} +
+); diff --git a/src/styles/corelink.css b/src/styles/corelink.css new file mode 100644 index 0000000..4708909 --- /dev/null +++ b/src/styles/corelink.css @@ -0,0 +1 @@ +:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#e8eef7;background:#07111f;font-synthesis:none;--cl-bg:#07111f;--cl-panel:#0d1a2b;--cl-panel-2:#111f32;--cl-text:#edf4ff;--cl-muted:#8fa3bc;--cl-border:#22334a;--cl-primary:#2f81f7;--cl-primary-hover:#4b93f8;--cl-danger:#ff6b6b;--cl-radius:16px}*{box-sizing:border-box}html,body,#root{min-height:100%;margin:0}body{min-height:100vh;background:radial-gradient(circle at 15% 0%,#102c50 0,transparent 32rem),radial-gradient(circle at 100% 100%,#0b253d 0,transparent 34rem),var(--cl-bg);color:var(--cl-text)}body:before{content:"";position:fixed;inset:0;pointer-events:none;background-image:linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);background-size:32px 32px;mask-image:linear-gradient(to bottom,black,transparent 75%)}a{color:#70a9ff;text-decoration:none}a:hover{text-decoration:underline}.corelink-brand-bar{position:fixed;top:28px;inset-inline-start:32px;display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.01em;z-index:2}.corelink-brand-bar img{width:30px;height:30px}.corelink-auth-footer{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);display:flex;gap:9px;align-items:center;color:var(--cl-muted);font-size:12px;white-space:nowrap}#kc-header{display:none}#kc-login,#kc-register-form,#kc-reset-password-form,#kc-passwd-update-form,#kc-totp-settings-form{width:100%}#kc-content{width:min(100% - 32px,440px);margin:0 auto;padding:96px 0 84px}#kc-content-wrapper{background:linear-gradient(180deg,rgba(17,31,50,.96),rgba(10,23,39,.96));border:1px solid var(--cl-border);border-radius:var(--cl-radius);padding:30px;box-shadow:0 24px 70px rgba(0,0,0,.38);backdrop-filter:blur(18px)}#kc-page-title{margin:0 0 24px;text-align:center;font-size:24px;line-height:1.25;letter-spacing:-.025em}form{display:grid;gap:18px}label{display:block;margin-bottom:7px;color:#c7d4e5;font-size:13px;font-weight:600}input[type=text],input[type=email],input[type=password],input[type=tel],input[type=number],select{width:100%;min-height:44px;border:1px solid var(--cl-border);border-radius:10px;background:#091625;color:var(--cl-text);padding:10px 12px;outline:none;transition:border-color .15s,box-shadow .15s}input:focus,select:focus{border-color:var(--cl-primary);box-shadow:0 0 0 3px rgba(47,129,247,.17)}input[type=checkbox]{accent-color:var(--cl-primary)}button,input[type=submit],.pf-c-button{width:100%;min-height:44px;border:0;border-radius:10px;background:var(--cl-primary);color:#fff;font:inherit;font-weight:700;cursor:pointer;padding:10px 16px;transition:background .15s,transform .1s}button:hover,input[type=submit]:hover,.pf-c-button:hover{background:var(--cl-primary-hover);text-decoration:none}button:active,input[type=submit]:active{transform:translateY(1px)}#kc-form-options,#kc-form-buttons,#kc-registration,#kc-info,#kc-social-providers{margin-top:16px}.alert,.pf-c-alert{padding:11px 13px;border:1px solid var(--cl-border);border-radius:10px;background:var(--cl-panel-2);font-size:13px}.alert-error,.pf-m-danger{border-color:rgba(255,107,107,.4);color:#ffb3b3}.instruction,.subtitle,#kc-info-wrapper{color:var(--cl-muted);font-size:13px;line-height:1.6}.corelink-dev-shell{display:grid;place-items:center;min-height:100vh;padding:24px}.corelink-dev-card{width:min(100%,460px);padding:32px;border:1px solid var(--cl-border);border-radius:var(--cl-radius);background:var(--cl-panel);text-align:center}.corelink-dev-card h1{margin:16px 0 8px}.corelink-dev-card p{color:var(--cl-muted)}.corelink-dev-card code{display:block;margin-top:20px;padding:12px;border-radius:9px;background:#07111f;color:#a9c7ef}html[dir=rtl] body{font-family:Vazirmatn,Tahoma,Arial,sans-serif}html[dir=rtl] input{text-align:right}@media(max-width:600px){.corelink-brand-bar{top:20px;inset-inline-start:20px}.corelink-auth-footer{bottom:16px}.corelink-auth-footer span:last-child{display:none}#kc-content{padding-top:80px}#kc-content-wrapper{padding:24px 20px}} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3abb777 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{"path":"./tsconfig.node.json"}] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..5eef90e --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0b8e478 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { keycloakify } from "keycloakify/vite-plugin"; + +const artifactId = process.env.KEYCLOAK_THEME_NAME ?? "corelink"; + +export default defineConfig({ + plugins: [ + react(), + keycloakify({ + artifactId, + accountThemeImplementation: "none", + keycloakVersionTargets: { + "26.2-and-above": `${artifactId}-26.2-and-above.jar` + } + }) + ] +});