Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion hub/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,33 @@ PORT=3001
# API logging threshold: trace, debug, info, warn, error, fatal, or silent.
LOG_LEVEL=info

# Public browser origin. Production requires HTTPS; local HTTP must use loopback.
HUB_ORIGIN=http://127.0.0.1:5173

# Keep 0 unless the API is reachable only through this exact number of trusted proxies.
HUB_TRUSTED_PROXY_HOPS=0

# Local development only. The devcontainer supplies its own database hostname.
DATABASE_URL=postgresql://replica:replica@127.0.0.1:5432/replica

# Only database-enabled tests require this URL and CREATEDB permission.
TEST_DATABASE_URL=postgresql://replica:replica@127.0.0.1:5432/postgres

# GitHub user authorization is optional until both values are configured.
# These credentials establish user identity and are separate from source integration.
# HUB_GITHUB_AUTH_CLIENT_ID=
# HUB_GITHUB_AUTH_CLIENT_SECRET=

# GitHub proves identity by default. Select legacy-repository only for compatibility.
HUB_ADMISSION_POLICY=hub-managed

# Select the GitHub source-integration credential provider. See docs/identity.md.
HUB_GITHUB_CREDENTIAL_PROVIDER=database

# Generate separate values outside the database. See docs/identity.md.
# HUB_GITHUB_CREDENTIAL_KEY=<openssl rand -hex 32>

# The environment provider is read-only and requires every GitHub integration setting below.
# The environment provider is read-only and requires every source-integration setting below.
# GITHUB_APP_ID=
# GITHUB_INSTALLATION_ID=
# GITHUB_CLIENT_ID=
Expand Down
4 changes: 2 additions & 2 deletions hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Hub is being rebuilt on `build/hub/new` using TypeScript, Express, React, Vite, and Vitest. This branch lets contributors start the new app locally. It is not ready to replace the existing Hub.

The API has one route: `GET /api/health`. The frontend displays the existing Replica logo and supports light and dark themes. Existing components, features, and pages are kept in Storybook, but they are not connected to the new API.
The API exposes its health check and GitHub authentication and session routes. The frontend provides the minimal GitHub sign-in and sign-out surface with light and dark themes. Existing components, features, and pages remain in Storybook, but they are not connected to product APIs.

Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The database has no product models yet. Sign-in, saved data, live updates, background jobs, and deployment are not implemented.
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The database has no product models yet. GitHub sign-in works when its dedicated client credentials are configured; invitations, membership administration, product authorization, saved product data, live updates, background jobs, and deployment are not implemented.

Use [Development](docs/development.md) to start the API and frontend. Review [Architecture](docs/architecture.md), [code architecture conventions](docs/backend-architecture.md), and [coding conventions](docs/coding-conventions.md) before adding behavior.

Expand Down
79 changes: 79 additions & 0 deletions hub/app/javascript/application/authentication/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useEffect, useState } from "react"

import { SignInPage } from "@pages/sign-in-page"
import type { SignInPageUser } from "@pages/sign-in-page/types"

interface SessionResponse {
authenticated: boolean
configured: boolean
csrfToken: string
user?: SignInPageUser
}

async function fetchSession(): Promise<SessionResponse> {
const response = await fetch("/api/session", { credentials: "same-origin" })
if (!response.ok) throw new Error("Session request failed")
return (await response.json()) as SessionResponse
}

export function AuthenticationApplication() {
const [session, setSession] = useState<SessionResponse | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)

useEffect(() => {
fetchSession()
.then(setSession)
.catch(() => setError("Hub could not load your sign-in status."))
.finally(() => setLoading(false))
}, [])

async function signIn() {
if (!session) return
setLoading(true)
setError(null)
try {
const response = await fetch("/api/auth/github", {
method: "POST",
credentials: "same-origin",
headers: { "X-CSRF-Token": session.csrfToken },
})
if (!response.ok) throw new Error("Sign-in request failed")
const data = (await response.json()) as { authorizationUrl: string }
window.location.assign(data.authorizationUrl)
} catch {
setError("Hub could not start GitHub sign-in.")
setLoading(false)
}
}

async function signOut() {
if (!session) return
setLoading(true)
setError(null)
try {
const response = await fetch("/api/session", {
method: "DELETE",
credentials: "same-origin",
headers: { "X-CSRF-Token": session.csrfToken },
})
if (!response.ok) throw new Error("Sign-out request failed")
setSession(await fetchSession())
} catch {
setError("Hub could not sign you out.")
} finally {
setLoading(false)
}
}

return (
<SignInPage
configured={session?.configured ?? false}
loading={loading}
user={session?.authenticated ? (session.user ?? null) : null}
error={error}
onSignIn={() => void signIn()}
onSignOut={() => void signOut()}
/>
)
}
4 changes: 2 additions & 2 deletions hub/app/javascript/entrypoints/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "@/tokens/base.css"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"

import { ReplicaLogo } from "@components/replica-logo"
import { AuthenticationApplication } from "@/application/authentication"
import { HubThemeProvider } from "@hooks/use-hub-theme"

const root = document.getElementById("root")
Expand All @@ -11,7 +11,7 @@ if (!root) throw new Error("The Hub document is missing its React root.")
createRoot(root).render(
<StrictMode>
<HubThemeProvider>
<ReplicaLogo />
<AuthenticationApplication />
</HubThemeProvider>
</StrictMode>,
)
57 changes: 57 additions & 0 deletions hub/app/javascript/pages/sign-in-page/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { fireEvent, render, screen } from "@testing-library/react"
import { describe, expect, it, vi } from "vitest"

import { SignInPage } from "."

describe("SignInPage()", () => {
it("starts GitHub sign-in without presenting repository access as authentication", () => {
const onSignIn = vi.fn()
render(
<SignInPage
configured
loading={false}
user={null}
error={null}
onSignIn={onSignIn}
onSignOut={() => undefined}
/>,
)

fireEvent.click(screen.getByRole("button", { name: "Sign in with GitHub" }))

expect(onSignIn).toHaveBeenCalledOnce()
expect(screen.getByText(/Repository access is authorized separately/)).toBeVisible()
})

it("shows the provider-neutral session identity and sign-out control", () => {
render(
<SignInPage
configured
loading={false}
user={{ displayName: "Mona Lisa", username: "mona", avatarUrl: null }}
error={null}
onSignIn={() => undefined}
onSignOut={() => undefined}
/>,
)

expect(screen.getByText(/Signed in as/)).toHaveTextContent("Mona Lisa (@mona)")
expect(screen.getByRole("button", { name: "Sign out" })).toBeEnabled()
})

it("disables sign-in when GitHub user authorization is not configured", () => {
render(
<SignInPage
configured={false}
loading={false}
user={null}
error={null}
onSignIn={() => undefined}
onSignOut={() => undefined}
/>,
)

expect(screen.getByRole("button", { name: "Sign in with GitHub" })).toBeDisabled()
expect(screen.getByText(/has not been configured/)).toBeVisible()
})
})
60 changes: 60 additions & 0 deletions hub/app/javascript/pages/sign-in-page/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Button, Heading, Stack, Text } from "@primer/react-brand"

import { ReplicaLogo } from "@components/replica-logo"

import type { SignInPageProps } from "./types"
import "./styles.css"

export function SignInPage({
configured,
loading,
user,
error,
onSignIn,
onSignOut,
}: SignInPageProps) {
return (
<main className="sign-in-page">
<Stack direction="vertical" gap="normal" alignItems="center">
<ReplicaLogo />
<Heading as="h1" size="3">
Replica Hub
</Heading>
{user ? (
<>
<Text as="p">
Signed in as <strong>{user.displayName ?? user.username}</strong>
{user.username ? ` (@${user.username})` : ""}.
</Text>
<Button type="button" variant="subtle" disabled={loading} onClick={onSignOut}>
Sign out
</Button>
</>
) : (
<>
<Text as="p">
Sign in with GitHub to create a Hub session. Repository access is authorized
separately.
</Text>
<Button
type="button"
variant="primary"
disabled={loading || !configured}
onClick={onSignIn}
>
Sign in with GitHub
</Button>
{!configured && (
<Text as="p">GitHub sign-in has not been configured for this Hub.</Text>
)}
</>
)}
{error && (
<Text as="p" className="sign-in-page__error" role="alert">
{error}
</Text>
)}
</Stack>
</main>
)
}
34 changes: 34 additions & 0 deletions hub/app/javascript/pages/sign-in-page/stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from "@storybook/react-vite"

import { SignInPage } from "."

const meta = {
title: "Hub/Pages/Identity/Sign in",
component: SignInPage,
parameters: { layout: "fullscreen" },
args: {
configured: true,
loading: false,
user: null,
error: null,
onSignIn: () => undefined,
onSignOut: () => undefined,
},
} satisfies Meta<typeof SignInPage>

export default meta
type Story = StoryObj<typeof meta>

export const SignedOut: Story = {}
export const SignedIn: Story = {
args: {
user: {
displayName: "Mona Lisa",
username: "mona",
avatarUrl: null,
},
},
}
export const Unconfigured: Story = {
args: { configured: false },
}
15 changes: 15 additions & 0 deletions hub/app/javascript/pages/sign-in-page/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.sign-in-page {
display: grid;
min-block-size: 100vh;
place-items: center;
padding: 2rem;
text-align: center;
}

.sign-in-page > * {
max-inline-size: 32rem;
}

.sign-in-page__error {
color: var(--hub-fg-danger);
}
14 changes: 14 additions & 0 deletions hub/app/javascript/pages/sign-in-page/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface SignInPageUser {
displayName: string | null
username: string | null
avatarUrl: string | null
}

export interface SignInPageProps {
configured: boolean
loading: boolean
user: SignInPageUser | null
error: string | null
onSignIn: () => void
onSignOut: () => void
}
2 changes: 1 addition & 1 deletion hub/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Hub is being rebuilt. The [Hub overview](../README.md) describes what the baseli
- [Coding conventions](coding-conventions.md): frontend layers and interface rules.
- [Development](development.md): local API and frontend setup.
- [Database](database.md): Prisma schema conventions, migrations, and the shared client.
- [Identity](identity.md): credential storage and providers.
- [Identity](identity.md): GitHub user authentication, provider-neutral sessions, admission policy, and source-integration credentials.
- [Logging](logging.md): application events, automatic request records, and payload safety.
- [Testing and operations](testing-and-operations.md): baseline checks and their limits.
- [Storybook](storybook.md): retained presentation, fixtures, and screenshot evidence.
Expand Down
8 changes: 4 additions & 4 deletions hub/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ Hub contributors are rebuilding the app on `build/hub/new` before merging it int

The team has chosen TypeScript, Express for the API, React and Vite for the frontend, and Vitest for tests. The old backend and the frontend code that connected to it have been removed rather than rewritten.

The API responds to a health check, and the frontend displays the logo with light and dark themes. Storybook keeps the existing components and pages available for reuse.
The API responds to a health check and provides GitHub authentication and Hub session routes. The frontend displays the sign-in state with light and dark themes. Storybook keeps the existing components and pages available for reuse.

Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The [identity foundation](identity.md) separates Hub users, external identities, installation membership, authentication-provider configuration, sessions, authorization attempts, security records, and GitHub source-integration credentials. Sign-in routes, deployment, background jobs, and product APIs have not been built. This app cannot yet authenticate users, evaluate prototype access, find repositories, import Builds, host previews, save product data, or support reviews.
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The [identity system](identity.md) separates Hub users, external identities, installation membership, authentication-provider configuration, sessions, authorization attempts, security records, and GitHub source-integration credentials. GitHub user authorization and provider-neutral Hub sessions work when user-auth credentials are configured. Deployment, background jobs, and product APIs have not been built. This app cannot yet evaluate prototype access, find repositories, import Builds, host previews, save product data, or support reviews.

## Backend

API code lives under `server/`. As features are added, route handlers will read requests, call domains, and return responses. The [code conventions](backend-architecture.md) explain repositories, integrations, utilities, domains, and which layers may import others. Put these directories under `server/` when they are needed; do not add empty implementations.

The only route is `GET /api/health`, which returns `{ "status": "ok" }`. Unknown paths return a JSON 404 response; invalid JSON is rejected before routing. `createApp()` builds the Express app without starting a server. `server/index.ts` starts it at a local address.
`GET /api/health` returns `{ "status": "ok" }`. Authentication routes start and complete GitHub authorization, restore or revoke Hub sessions, and perform fresh verification. Unknown paths return a JSON 404 response; invalid JSON is rejected before routing. `createApp()` builds the Express app without starting a server. `server/index.ts` composes the authentication runtime and starts the API at a local address.

The [database guide](database.md) defines the multi-file Prisma schema and shared client in integrations. The health route remains a process check, not a database readiness check.

Expand All @@ -35,7 +35,7 @@ Keep the existing `app/javascript/` layout. Application code starts React and wi

Imports follow the arrows above. Shared components, hooks, and utilities must not import pages, features, or application code. Keep browser and server code separate. Agree on the API before adding types shared between them.

The frontend displays the existing logo inside the theme provider. Existing pages and features use ordinary browser links. Their old destinations do not exist in the new API; keeping the interface does not make those features work.
The frontend displays the GitHub sign-in and session state inside the theme provider. Existing pages and features use ordinary browser links. Their old destinations do not exist in the new API; keeping the interface does not make those features work.

## Other projects

Expand Down
8 changes: 5 additions & 3 deletions hub/docs/development.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Hub development

Use the Hub devcontainer for Node.js and PostgreSQL, or run Node.js locally with the same database service. Sign-in and production deployment are not configured.
Use the Hub devcontainer for Node.js and PostgreSQL, or run Node.js locally with the same database service. GitHub sign-in is optional in local development. Production deployment is not configured.

## Devcontainer

Expand All @@ -20,7 +20,7 @@ cp .env.example .env
npm ci --engine-strict
```

The example URL connects to PostgreSQL on `127.0.0.1:5432`. To use another development database, set `DATABASE_URL` in `.env` instead. Do not commit credentials. The existing health and frontend routes do not query the database.
The example URL connects to PostgreSQL on `127.0.0.1:5432`. To use another development database, set `DATABASE_URL` in `.env` instead. Do not commit credentials. Set `HUB_GITHUB_AUTH_CLIENT_ID` and `HUB_GITHUB_AUTH_CLIENT_SECRET` to exercise real GitHub sign-in; leave both unset to run Hub with sign-in disabled.

## Start the app

Expand All @@ -36,10 +36,12 @@ In a second terminal, also from `hub/`, start Vite:
npm run dev
```

Open <http://127.0.0.1:5173>. Vite serves the frontend and forwards `/api` requests to Express at <http://127.0.0.1:3001>. `GET /api/health` returns JSON; other API routes have not been built. Both servers accept connections only from this machine. Stop each with Ctrl+C.
Open <http://127.0.0.1:5173>. Vite serves the sign-in surface and forwards `/api` requests to Express at <http://127.0.0.1:3001>. `GET /api/health` returns JSON. `GET /api/session` reports the browser session and supplies the CSRF value used by the sign-in and sign-out controls. Both servers accept connections only from this machine. Stop each with Ctrl+C.

The API reads an optional `PORT` environment variable, defaulting to 3001. If you change it, update the local Vite proxy target to match. The editor forwards ports 3001, 5173, and 6006 from the devcontainer. API startup reads `.env` when present; Compose's environment takes precedence.

Hub ignores forwarded client-address headers by default and rate-limits the direct connection address. Keep `HUB_TRUSTED_PROXY_HOPS=0` for local development and any deployment where clients can reach the API directly. A deployment whose API is reachable only through a fixed proxy chain may set the exact number of trusted hops. Every trusted proxy must replace or append the client address correctly, and the network must prevent clients from bypassing the first trusted proxy. Never enable proxy trust only to accept arbitrary `Forwarded` or `X-Forwarded-For` headers.

## Database lifecycle

PostgreSQL stores data in a named Compose volume. Stopping or rebuilding the devcontainer preserves it. For a local Node.js setup, stop the database with `docker compose -f .devcontainer/compose.yaml down`; this also preserves data. Do not add `--volumes` unless you intend to permanently delete the local database.
Expand Down
Loading
Loading