Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f471a4934 | |||
| b49978710b | |||
| 88995ff59b | |||
| 2ce7966fe9 | |||
| ddc4e3e052 | |||
| 7a8b59ab87 | |||
| 250c7a5ac9 | |||
| dee7465190 | |||
| 66bac2c6f8 | |||
| 044eeaae61 | |||
| 59a29a2d03 | |||
| c58e4e4b23 | |||
| 98c8a7bb83 | |||
| 1ceac35437 | |||
| 3d0c3c551b | |||
| c7417dc9e3 | |||
| 0d52ddd9f0 |
@@ -1,54 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
This repository (`groombook/web`) is part of the GroomBook application stack. The
|
|
||||||
authoritative process, quality bar, and safety rules live in the shared
|
|
||||||
[`groombook/org`](https://git.farh.net/groombook/org) skills repository. Read
|
|
||||||
those first; this file is only a pointer.
|
|
||||||
|
|
||||||
## Authoritative skills
|
|
||||||
|
|
||||||
- **SDLC (branching, PRs, phases, handoffs):**
|
|
||||||
[`groombook/org/skills/sdlc/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/sdlc/SKILL.md)
|
|
||||||
- **Coding standards (priority ordering, PR discipline, tests, no-hardcoded-values, CalVer):**
|
|
||||||
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md)
|
|
||||||
- **Safety (no plaintext secrets, no direct `kubectl apply` to `groombook`, no self-merge, board approval for destructive actions):**
|
|
||||||
[`groombook/org/skills/safety/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/safety/SKILL.md)
|
|
||||||
|
|
||||||
For human contributors and humans reviewing agent work, see
|
|
||||||
[`CONTRIBUTING.md`](./CONTRIBUTING.md) in this repo for the phase-by-phase PR
|
|
||||||
flow and the `uat→main` merge-gate policy summary.
|
|
||||||
|
|
||||||
## Non-negotiable operational rules
|
|
||||||
|
|
||||||
These mirror the org skills; they are restated here so any agent landing in
|
|
||||||
this repo sees them without a cross-repo fetch.
|
|
||||||
|
|
||||||
- **All changes go through a PR.** Never push directly to `dev`, `uat`, or `main`.
|
|
||||||
- **Branch strategy:** `feature/<name>` → `dev` → `uat` → `main`. Engineers
|
|
||||||
always target `dev` first.
|
|
||||||
- **No self-merge contract.** The engineer who opened a PR clicks merge only
|
|
||||||
after the named reviewer (CI / QA / UAT / Security / CTO per phase)
|
|
||||||
approves. Issue-thread QA / UAT / security approvals do **not** clear the
|
|
||||||
Gitea `required_approvals` gate on `uat→main` — only a Gitea **Approve**
|
|
||||||
click from a member of the `approvals_whitelist_username` does. On this
|
|
||||||
repo that whitelist is `["gb_flea", "gb_dogfather"]` (engineer team).
|
|
||||||
Board-level accounts cannot give the Approve click by policy.
|
|
||||||
- **Always include `cc @cpfarhood`** at the bottom of every PR body for
|
|
||||||
board visibility (not as a reviewer).
|
|
||||||
- **Secrets in code are forbidden.** Use Bitnami Sealed Secrets; never commit
|
|
||||||
plaintext. See the `safety` skill.
|
|
||||||
- **Production (`groombook` namespace) is Flux-managed.** Never
|
|
||||||
`kubectl apply` directly. Infrastructure changes go through PRs in
|
|
||||||
`groombook/infra`.
|
|
||||||
|
|
||||||
## Local development
|
|
||||||
|
|
||||||
See the repo's own README, package scripts, and CI workflow. The
|
|
||||||
authoritative pipeline (Gitea Actions, image build, deploy hooks) is the
|
|
||||||
shared `groombook/infra` overlay; do not reimplement it here.
|
|
||||||
|
|
||||||
## When uncertain
|
|
||||||
|
|
||||||
If a task conflicts with the org skills, **the org skills win**. Open an
|
|
||||||
issue in `groombook/org` to propose a change rather than encoding a local
|
|
||||||
exception.
|
|
||||||
-117
@@ -1,117 +0,0 @@
|
|||||||
# Contributing to `groombook/web`
|
|
||||||
|
|
||||||
Thanks for contributing. This document is the human-facing companion to
|
|
||||||
[`AGENTS.md`](./AGENTS.md) and the authoritative
|
|
||||||
[`groombook/org`](https://git.farh.net/groombook/org) skills. The org skills
|
|
||||||
govern; this file is a quick-reference for the human/agent PR flow in this
|
|
||||||
repo.
|
|
||||||
|
|
||||||
## Branch strategy
|
|
||||||
|
|
||||||
Three long-lived branches; one PR per promotion step.
|
|
||||||
|
|
||||||
| Branch | Environment | Who merges | Prerequisites for merge |
|
|
||||||
|---------|-------------|-----------|-------------------------|
|
|
||||||
| `dev` | Dev | Engineer | CI passes |
|
|
||||||
| `uat` | UAT | Engineer | QA code review approval |
|
|
||||||
| `main` | Production | Engineer | UAT validation + CTO Gitea Approve when the `uat→main` merge-gate policy applies (see below) |
|
|
||||||
|
|
||||||
Engineers always target `dev` first. Feature branches: `<agent-name>/<short-description>`.
|
|
||||||
|
|
||||||
## Phase-by-phase PR flow
|
|
||||||
|
|
||||||
### Phase 1 — Dev
|
|
||||||
|
|
||||||
1. Branch from `dev`: `git checkout -b <name>/<short-description> origin/dev`.
|
|
||||||
2. Write code + tests. Run unit tests, type check, and lint locally (or rely on CI).
|
|
||||||
3. Open a PR against `dev`:
|
|
||||||
```bash
|
|
||||||
tea pr create --base dev --title "..." --body "..."
|
|
||||||
```
|
|
||||||
Include `cc @cpfarhood` at the bottom of the body for board visibility.
|
|
||||||
4. CI must pass. CI green → engineer self-merges.
|
|
||||||
5. CI builds and deploys to Dev automatically.
|
|
||||||
|
|
||||||
### Phase 2 — UAT promotion
|
|
||||||
|
|
||||||
1. Open a PR from `dev` to `uat`.
|
|
||||||
2. CI must pass.
|
|
||||||
3. **QA (Lint Roller)** reviews and approves on the Gitea PR.
|
|
||||||
4. QA approved → engineer self-merges.
|
|
||||||
5. CI builds and deploys to UAT automatically.
|
|
||||||
|
|
||||||
### Phase 3 — UAT regression + Security review
|
|
||||||
|
|
||||||
1. **UAT (Shedward Scissorhands)** runs full regression against UAT — every
|
|
||||||
feature, old and new, no exceptions.
|
|
||||||
2. **Security (Barkley Trimsworth)** reviews the changes.
|
|
||||||
3. Failures in either gate bounce back to Phase 1.
|
|
||||||
|
|
||||||
### Phase 4 — Production promotion (`uat → main`)
|
|
||||||
|
|
||||||
This is the gate the org PR
|
|
||||||
[`groombook/org#13`](https://git.farh.net/groombook/org/pulls/13) defines.
|
|
||||||
The full rule is in
|
|
||||||
[`groombook/org/skills/sdlc/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/sdlc/SKILL.md)
|
|
||||||
and
|
|
||||||
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md);
|
|
||||||
the summary is below.
|
|
||||||
|
|
||||||
**The CTO Gitea Approve click is NOT the default gate.** Once the four
|
|
||||||
pre-gates (QA, UAT deploy, UAT regression, security) are green, the engineer
|
|
||||||
self-merges.
|
|
||||||
|
|
||||||
**A CTO Gitea Approve click IS required** only for PRs in one of three
|
|
||||||
categories:
|
|
||||||
|
|
||||||
1. **Novel auth / session paths** — login, OIDC, OOBE, session middleware,
|
|
||||||
token issuance, password reset, MFA, new auth provider integrations.
|
|
||||||
Routine auth-gated UI (button styling, error messages, form layout) is
|
|
||||||
**not** in this category.
|
|
||||||
2. **Infra / prod-affecting merges** — deploys, infra manifests, secrets,
|
|
||||||
GitOps overlays, CI/CD, `main` branch protection, production
|
|
||||||
routing/ingress, prod state mutations. All Phase 5 infra overlay PRs in
|
|
||||||
`groombook/infra` require CTO Gitea Approve without exception.
|
|
||||||
3. **Risk-flagged merges** — `risk:cto-approve` label, or explicit CTO/CEO
|
|
||||||
sign-off request in the PR or issue thread.
|
|
||||||
|
|
||||||
The engineer opens the `uat→main` PR, classifies it against the three
|
|
||||||
categories above, and adds `cc @cpfarhood`. If the PR is in scope, the CTO
|
|
||||||
clicks Approve; once approved (and the four pre-gates are green), the
|
|
||||||
engineer merges.
|
|
||||||
|
|
||||||
### Phase 5 — Production deployment
|
|
||||||
|
|
||||||
A separate PR in `groombook/infra` bumps the overlay image tag for prod.
|
|
||||||
Handed to QA (Lint Roller) for review, then self-merged by the engineer.
|
|
||||||
|
|
||||||
## The four pre-gates (uat→main)
|
|
||||||
|
|
||||||
A `uat→main` PR is mergeable when **all four** are green:
|
|
||||||
|
|
||||||
1. **QA code review** — done on the dev→uat promotion PR.
|
|
||||||
2. **UAT deploy** — the UAT image built from the uat tip is live in UAT.
|
|
||||||
3. **UAT regression** — Shedward's full-feature UAT pass is green (no
|
|
||||||
pre-existing defects, no new defects).
|
|
||||||
4. **Security review** — Barkley's security code review is green.
|
|
||||||
|
|
||||||
Issue-thread QA / UAT / security approvals do **not** clear the Gitea
|
|
||||||
`required_approvals` gate. Only a Gitea **Approve** click from a member of
|
|
||||||
the `approvals_whitelist_username` for `main` clears it. In this repo that
|
|
||||||
whitelist is the engineer team (`gb_flea`, `gb_dogfather`).
|
|
||||||
|
|
||||||
## Style, tests, and quality bar
|
|
||||||
|
|
||||||
See
|
|
||||||
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md)
|
|
||||||
for the engineering priority ordering, test requirements, no-hardcoded-values
|
|
||||||
rules, CalVer versioning policy, and the `git.farh.net` container registry
|
|
||||||
policy.
|
|
||||||
|
|
||||||
## Safety
|
|
||||||
|
|
||||||
See
|
|
||||||
[`groombook/org/skills/safety/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/safety/SKILL.md)
|
|
||||||
for the non-negotiable rules: no plaintext secrets, no `kubectl apply` to
|
|
||||||
`groombook`, no self-merge, no direct `tofu` runs, board approval for
|
|
||||||
destructive actions, escalation protocol.
|
|
||||||
+1
-1
@@ -86,7 +86,7 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
|
|||||||
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|
||||||
|---|----------|-------|---------------|---------------|
|
|---|----------|-------|---------------|---------------|
|
||||||
| TC-WEB-SSO-1 | Sign-in page shows SSO button | Navigate to app root URL | Sign-in page displayed with "Sign in with SSO" button visible | No SSO button, 403 before page loads |
|
| TC-WEB-SSO-1 | Sign-in page shows SSO button | Navigate to app root URL | Sign-in page displayed with "Sign in with SSO" button visible | No SSO button, 403 before page loads |
|
||||||
| TC-WEB-SSO-2 | Click SSO redirects to Authentik (GRO-2572) | **Fresh session only (no pre-existing auth cookie).** Click "Sign in with SSO" button | Browser navigates to Authentik login at auth.farh.net within ~1 s — address bar changes to auth.farh.net URL | No redirect, error shown, button stays disabled, user remains on /login. Regression: prior to GRO-2572 fix the client never followed the `data.url` returned by Better Auth. Run from a clean incognito context to avoid a stale cookie masking the defect. |
|
| TC-WEB-SSO-2 | Click SSO redirects to Authentik | Click "Sign in with SSO" button | Browser redirected to Authentik login at auth.farh.net | No redirect, error shown, button does nothing |
|
||||||
| TC-WEB-SSO-3 | Valid OIDC credentials authenticate | At Authentik, enter valid credentials and authenticate | Redirected back to app with active session | Redirect loop, 403, session not established |
|
| TC-WEB-SSO-3 | Valid OIDC credentials authenticate | At Authentik, enter valid credentials and authenticate | Redirected back to app with active session | Redirect loop, 403, session not established |
|
||||||
| TC-WEB-SSO-4 | Post-login dashboard accessible | After SSO flow completes, dashboard loads | Dashboard displays correctly with user identity shown | Blank page, 403, session not active |
|
| TC-WEB-SSO-4 | Post-login dashboard accessible | After SSO flow completes, dashboard loads | Dashboard displays correctly with user identity shown | Blank page, 403, session not active |
|
||||||
| TC-WEB-SSO-5 | User identity displayed correctly | After SSO login, check header/nav | User name/email/initials shown in nav, role reflected in UI | No user indicator, wrong user shown |
|
| TC-WEB-SSO-5 | User identity displayed correctly | After SSO login, check header/nav | User name/email/initials shown in nav, role reflected in UI | No user indicator, wrong user shown |
|
||||||
|
|||||||
@@ -45,12 +45,6 @@ function LoginPage() {
|
|||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
setError(result.error.message ?? "Sign-in failed");
|
setError(result.error.message ?? "Sign-in failed");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Better Auth returns the IdP authorize URL in data.url with redirect:true rather than
|
|
||||||
// issuing an HTTP 30x — the client must follow it (GRO-2572).
|
|
||||||
if (result?.data?.url) {
|
|
||||||
window.location.href = result.data.url;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { render, screen, within, waitFor } from "@testing-library/react";
|
import { render, screen, within, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { App } from "../App";
|
import { App } from "../App";
|
||||||
|
|
||||||
@@ -233,7 +232,6 @@ describe("Dev login selector", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not redirect when a dev user is already selected", async () => {
|
it("does not redirect when a dev user is already selected", async () => {
|
||||||
|
|
||||||
localStorage.setItem("dev-user", JSON.stringify({ type: "staff", id: "s1", name: "Sarah" }));
|
localStorage.setItem("dev-user", JSON.stringify({ type: "staff", id: "s1", name: "Sarah" }));
|
||||||
|
|
||||||
global.fetch = vi.fn((url: string) => {
|
global.fetch = vi.fn((url: string) => {
|
||||||
@@ -271,61 +269,3 @@ describe("Dev login selector", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GRO-2572 — SSO button follows redirect URL", () => {
|
|
||||||
it("navigates to data.url when signIn.social returns a redirect", async () => {
|
|
||||||
// Mock signIn.social to return the redirect payload Better Auth sends
|
|
||||||
vi.mock("../lib/auth-client.js", () => ({
|
|
||||||
useSession: () => ({ data: null, isPending: false }),
|
|
||||||
signIn: {
|
|
||||||
social: vi.fn().mockResolvedValue({
|
|
||||||
data: { redirect: true, url: "https://auth.farh.net/application/o/authorize/?test=1" },
|
|
||||||
error: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
signOut: vi.fn(),
|
|
||||||
changePassword: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const assignMock = vi.fn();
|
|
||||||
Object.defineProperty(window, "location", {
|
|
||||||
value: { ...window.location, href: "", origin: "https://uat.groombook.dev" },
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
Object.defineProperty(window.location, "href", {
|
|
||||||
set: assignMock,
|
|
||||||
get: () => "",
|
|
||||||
});
|
|
||||||
|
|
||||||
global.fetch = vi.fn((url: string) => {
|
|
||||||
if (url === "/api/dev/config") {
|
|
||||||
return Promise.resolve({ ok: true, json: async () => ({ authDisabled: false }) } as Response);
|
|
||||||
}
|
|
||||||
if (url === "/api/auth/get-session") {
|
|
||||||
return Promise.resolve({ ok: true, json: async () => null } as unknown as Response);
|
|
||||||
}
|
|
||||||
if (url === "/api/setup/status") {
|
|
||||||
return Promise.resolve({ ok: true, json: async () => ({ needsSetup: false }) } as Response);
|
|
||||||
}
|
|
||||||
if (url === "/api/auth/providers") {
|
|
||||||
return Promise.resolve({ ok: true, json: async () => ({ providers: ["authentik"] }) } as Response);
|
|
||||||
}
|
|
||||||
return Promise.resolve({ ok: true, json: async () => [] } as Response);
|
|
||||||
}) as unknown as typeof fetch;
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MemoryRouter initialEntries={["/login"]}>
|
|
||||||
<App />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ssoButton = await screen.findByRole("button", { name: /sign in with sso/i });
|
|
||||||
await userEvent.click(ssoButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(assignMock).toHaveBeenCalledWith(
|
|
||||||
"https://auth.farh.net/application/o/authorize/?test=1"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user