Skip to content

Add configurable OIDC login and Atlas-style shell - #1

Merged
pepicrft merged 17 commits into
mainfrom
feat/replicate-fabrik-login-layout
May 29, 2026
Merged

Add configurable OIDC login and Atlas-style shell#1
pepicrft merged 17 commits into
mainfrom
feat/replicate-fabrik-login-layout

Conversation

@pepicrft

@pepicrft pepicrft commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a generic, env-configured OIDC login flow with PKCE, session storage, and logout support.
  • Copy the Atlas-style Noora login screen, headerbar, sidebar, and account control structure while keeping product and provider labels configurable.
  • Extend runtime config, README docs, and the Helm chart for generic auth branding and optional OIDC secrets.
  • Add focused controller coverage for the auth gate and login page copy.

Testing

  • mix format --check-formatted
  • mix test
  • mix assets.build
  • helm template hive infra/helm/hive --set image.tag=test
  • PORT=4861 mix phx.server with curl checks for / and /login

@pepicrft
pepicrft force-pushed the feat/replicate-fabrik-login-layout branch from 23b85ac to a462d73 Compare May 28, 2026 13:06
@pepicrft
pepicrft force-pushed the feat/replicate-fabrik-login-layout branch from a462d73 to 1aec247 Compare May 28, 2026 13:39
@pepicrft pepicrft changed the title Add configurable OIDC login shell Add configurable OIDC login and Atlas-style shell May 28, 2026
pepicrft and others added 14 commits May 28, 2026 16:41
Introduce a Layouts module that owns the app shell (root HTML, dashboard
chrome with headerbar + sidebar + content area) so pages render content
into a slot instead of inlining the full chrome. Simplify the overview
page to a title, subtitle, and Getting started card — drop the hero,
3-column metric grid, and Workspace badge that didn't match the design
direction.

Ship a honeycomb logo asset at priv/static/images/logo.png, wire it as
the headerbar logo, the login frame logo, and the favicon, and feature
it at the top of README.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the login frame typography, sizing, and background to tuist's
auth page: heading-medium title (instead of display-small), body-small
centered subtitle, 50×50 logo, and large blurred purple oklch gradients
translated into the top-right and bottom-left corners. Tighten the
frame with overflow:hidden and a max-width clamp so it never exceeds
the viewport on small screens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generalize the auth controller into a provider-keyed start/callback
flow at /auth/:provider so any number of OIDC providers can coexist
on the login screen. Move provider configuration into Hive.Auth.providers/0,
which returns a list of configured provider maps; the login page
renders one button per provider.

Add Google as a first-class provider with hardcoded endpoints
(accounts.google.com, oauth2.googleapis.com, openidconnect.googleapis.com),
driven by HIVE_GOOGLE_CLIENT_ID / HIVE_GOOGLE_CLIENT_SECRET. Add an
email-domain allowlist via HIVE_GOOGLE_ALLOWED_DOMAINS (also
HIVE_OIDC_ALLOWED_DOMAINS) enforced on the callback, plus Google's
`hd=` hint on the authorize redirect when a single domain is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip Tuist-specific assumptions from the default values so the chart
is usable for any Hive deployment. Defaults now leave host, image
pull secret, External Secrets Operator integration, and the Postgres
S3 backup unconfigured. A new `secrets.existingSecret` value lets
operators bring their own Kubernetes Secret.

Move Tuist's production configuration into values-production.yaml:
hive.tuist.dev host, HIVE_AUTH_MODE=oidc with HIVE_GOOGLE_ALLOWED_DOMAINS,
ghcr-pull image pull secret, externalSecrets with the onepassword-hive
ClusterSecretStore and 1Password remote refs for SECRET_KEY_BASE and
the Google OAuth credentials, hcloud-volumes storage, and the S3
backup endpoint. The deploy workflow applies this overlay via
`helm upgrade --values`.

`helm template hive infra/helm/hive` now renders a generic-only set
of resources with zero references to Tuist, 1Password, or hcloud.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the placeholder "Phoenix application for orchestrating product
work" intro with a storytelling lede that frames Hive as Tuist's take
on shaping and building product with LLMs and agentic workflows,
designed for internal use and equally for opening up to the people
who use the deployer's products.

Document the new multi-provider auth (Google as a first-class option
with allowed-domains, plus the generic OIDC fallback) and the
split between the generic Helm chart and the Tuist production
overlay, including a copy-pasteable bring-your-own-secret recipe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capture project orientation for agentic tools: tech stack, layout,
setup, common commands, auth model, conventions, and deployment.
Symlink CLAUDE.md → AGENTS.md so both conventions point at the same
source of truth (relative target works in any clone).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…minator

Drop the parallel HIVE_GOOGLE_* config namespace in favor of a single
HIVE_OIDC_* surface with a HIVE_OIDC_PROVIDER=google|generic
discriminator. When set to google, Hive injects Google's hardcoded
authorize/token/userinfo URLs and applies the hd= authorize hint;
otherwise the operator supplies the URLs themselves.

Hive.Auth now exposes a single provider/0 instead of a list-returning
providers/0 — one provider per instance is the only supported shape,
which matches how operators actually deploy it. The login page
renders one button, the controller flow at /auth/:provider is
unchanged.

The Tuist production overlay sets HIVE_OIDC_PROVIDER=google and wires
the 1Password hive-google-oauth/username|credential refs into
HIVE_OIDC_CLIENT_ID/HIVE_OIDC_CLIENT_SECRET. No change required in
Google Cloud Console (callback URL is still /auth/google/callback)
or in 1Password (item name unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hive is the product, not a rebrand surface. Drop the
HIVE_PRODUCT_NAME, HIVE_PRODUCT_TAGLINE, and HIVE_AUTH_PROVIDER_NAME
env vars; hardcode "Hive" in the one place it appears and remove the
"Product work orchestration" tagline that was placeholder copy on
both the login subtitle and the overview page. The generic-OIDC
button label falls back to a fixed "Identity provider" string —
operators who use HIVE_OIDC_PROVIDER=google get a "Google" label
either way, which is the realistic path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Break the 431-line app.css monolith into co-located files:

  assets/css/
  ├── app.css                              # imports + base resets only
  ├── layouts/dashboard.css                # .layout, .headerbar, .layout__main, .layout__content
  ├── components/account_dropdown.css      # .account-dropdown (extension of <.avatar>)
  └── routes/login.css                     # everything under #login

Rewrite every rule with native CSS nesting (& operator) so styles are
co-located with the selector they belong to, no utility classes, and
all spacing/colors/typography reference Noora variables.

Document the convention in AGENTS.md so future routes follow it:
routes are identified by an #id, components live in components/,
nest with &, prefer Noora vars over hardcoded values.

Verified by rebuilding the bundle and screenshotting /login and /:
visually identical to before the split.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace BEM-style child classes (.headerbar__left, .headerbar__right,
.headerbar__brand, .headerbar__title, .layout__main, .layout__content)
with the data-part pattern that the Tuist UI codebase uses
(../tuist/server, ../atlas). Each layout/component has one anchor
class (.layout, .headerbar) and every internal region is addressed
via data-part="name" on the HTML, with CSS nested under the anchor:

  .headerbar {
    & [data-part="left-section"] { ... }
    & [data-part="right-section"] { ... }
    & [data-part="title"] { ... }
  }

Update HEEx in components/layouts.ex to swap class names for
data-part attributes. Drop the orphan [data-part="line-divider"]
rule — Noora's <.line_divider /> component renders its own styling
under .noora-line-divider and no longer needs an override.

Promote the rule to a prominent paragraph in AGENTS.md so future
work follows it from the start. Also add a security note: Hive is
an open-source repo, so credentials never go in source, fixtures,
commit messages, or PR bodies — production secrets live in
1Password and are pulled in via External Secrets at deploy time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oidcc

Delete the bespoke PKCE/state/token-exchange code in AuthController
and the provider-config plumbing in Hive.Auth in favor of Ueberauth
and ueberauth_oidcc. Both Google and a generic OIDC provider are
configured as Ueberauth strategies pointed at distinct issuers;
ueberauth_oidcc handles discovery via .well-known/openid-configuration,
state, PKCE, token exchange, and userinfo.

`config/runtime.exs` reads env vars, conditionally registers issuers +
strategies, and stores display metadata (button label, allowed
domains) under :hive, :auth, :providers. Multiple providers can run
simultaneously and show up as separate buttons on the login screen.

Hive.Auth shrinks to four helpers: enabled?/0, providers/0,
check_domain/2 (callback-side allowlist enforcement), and
current_user/1. The router pipes /auth/:provider and
/auth/:provider/callback through a dedicated :oauth pipeline running
the Ueberauth plug; AuthController.callback/2 reads
conn.assigns.ueberauth_auth, applies the domain check, and puts the
user in the session.

Env vars revert to per-provider namespaces since strategies are
independent:

  HIVE_GOOGLE_CLIENT_ID, HIVE_GOOGLE_CLIENT_SECRET,
  HIVE_GOOGLE_ALLOWED_DOMAINS
  HIVE_OIDC_ISSUER, HIVE_OIDC_CLIENT_ID, HIVE_OIDC_CLIENT_SECRET,
  HIVE_OIDC_DISPLAY_NAME, HIVE_OIDC_ALLOWED_DOMAINS

HIVE_OIDC_PROVIDER, HIVE_OIDC_AUTHORIZE_URL, HIVE_OIDC_TOKEN_URL,
HIVE_OIDC_USERINFO_URL, HIVE_*_SCOPES, and HIVE_AUTH_PROVIDER_NAME
go away — the issuer URL is sufficient for OIDC discovery, scopes
are fixed at openid+profile+email, and display name lives under
HIVE_OIDC_DISPLAY_NAME. The Tuist production overlay renames its
two ExternalSecret keys from HIVE_OIDC_* back to HIVE_GOOGLE_*; the
1Password item itself (hive-google-oauth) is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Have `Hive.Auth.check_domain/2` accept the provider map (or `nil`)
directly instead of looking up the provider by key from
`Application.get_env`. The function is now pure — pattern-matches
on `%{allowed_domains: ...}` and returns `:ok` or
`{:error, :domain_not_allowed}` without touching application state.

The controller does the lookup itself before calling
`check_domain`. Hive.AuthTest runs `async: true`, passes provider
maps in directly, and no longer needs the `Application.put_env`
setup/`on_exit` dance. Also covers case-insensitive domain
matching, which the old test set missed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a HIVE_VISIBILITY env var (`public` default, or `private`)
that controls whether `HiveWeb.Plugs.RequireAuthenticated` gates the
dashboard. `public` lets everyone reach the dashboard without
signing in; `private` redirects unauthenticated requests to /login.
Login itself stays available regardless of visibility so admins can
sign in to a public instance.

Replaces HIVE_AUTH_MODE: the old `none`/`oidc` knob conflated
"is auth available?" with "is auth required?". Visibility now
controls only the gate; provider configuration (HIVE_GOOGLE_*,
HIVE_OIDC_*) decides what sign-in options exist.

Hive.Auth's surface shrinks: `enabled?/0` and `mode/0` are gone,
replaced by `visibility/0`, `private?/0`, `public?/0`. The plug
and the login page use `private?/0`.

Add `HiveWeb.Plugs.RequireAuthenticatedTest` exercising the plug
end-to-end against the visibility config (4 cases: public passes,
default-public passes, private without session redirects, private
with session passes).

Also clean up the README:
- Drop all em dashes (replace with commas/colons/periods).
- Move the Tuist-specific production-setup section out of the
  public README and into AGENTS.md, where internal contributors
  can still find the 1Password vault contents, ClusterSecretStore
  bootstrap script, and cluster assumptions. The README now only
  documents the generic chart usage.

Tuist's production overlay is set to `HIVE_VISIBILITY: "public"`
since hive.tuist.dev is meant to be open to anyone; the Google
allowlist (HIVE_GOOGLE_ALLOWED_DOMAINS=tuist.dev) still restricts
who can actually sign in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…and Helm chart

Adapt the tuist/tuist monorepo release model to Hive. Versioning is
driven by Conventional Commits + git-cliff on two independent tracks:

- App releases tag `server@X.Y.Z` and push `ghcr.io/tuist/hive:X.Y.Z`
  (plus `:latest`). Driven by `cliff.toml` at the repo root — any
  Conventional Commit that isn't `(helm)`-scoped contributes, so
  `feat:`, `feat(auth):`, `refactor(css):` etc. all count toward the
  next app release.
- Helm chart releases tag `helm@X.Y.Z` and push to
  `oci://ghcr.io/tuist/charts/hive`. Driven by
  `infra/helm/hive/cliff.toml` — only `(helm)`-scoped commits count.

`.github/workflows/release.yml` runs on each push to main:
check-releases evaluates whether either component has unreleased
commits since the last matching tag; release-server and release-helm
build artifacts and refresh their CHANGELOGs; commit-and-release tags
the commit, publishes GitHub Releases with cliff-generated notes, and
commits the CHANGELOG + Chart.yaml bumps back to main.

`deploy.yml` is unchanged — it still ships every push to production
with a sha-tagged image. The release workflow runs in parallel and
produces versioned artifacts for out-of-band consumers.

git-cliff is added to mise.toml so contributors can preview release
notes locally; AGENTS.md documents the flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
Comment thread .github/workflows/release.yml Fixed
pepicrft and others added 2 commits May 29, 2026 16:20
Replace the Application.put_env / setup / on_exit dance in
RequireAuthenticatedTest and AuthControllerTest with Mimic stubs.
Both files now run `async: true`: stubs apply per-test in the test
process only, so the suite can parallelize without races on the
shared application env.

`test/test_helper.exs` declares Hive.Auth as Mimic-copyable. Tests
stub the narrow query helpers (`private?/0`, `current_user/1`,
`providers/0`) directly, which is what the code's API was designed
for in the first place. Add `:mimic ~> 1.7` as a test-only dep.

Also document the rule in AGENTS.md so future tests follow it from
the start: no Application config mutation in setup; use Mimic
stubs on the wrapping module instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeQL flagged 8 occurrences of unpinned third-party actions in
.github/workflows/release.yml. Pin each to the commit SHA that the
tag currently points at, with the version preserved as a trailing
comment so renovate/dependabot can keep them current:

  jdx/mise-action               v4.0.1 → 1648a781...
  docker/setup-buildx-action    v3     → 8d2750c6...
  docker/login-action           v3     → c94ce9fb...
  docker/build-push-action      v6     → 10e90e36...
  softprops/action-gh-release   v2     → 3bb12739...

Leaving actions/* refs alone — they're maintained by GitHub itself
and weren't flagged by CodeQL. The other workflows (hive.yml,
deploy.yml) are also unchanged; they pre-existed on main and
weren't part of the PR's scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pepicrft
pepicrft merged commit 8dc274b into main May 29, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants