Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
48e1b28
feat: handle refresh token
peppescg Nov 19, 2025
a6125db
refactor: move to constants file
peppescg Nov 19, 2025
15b6005
refactor: env var
peppescg Nov 20, 2025
ec0e948
refactor: use NEXT_PUBLIC_OIDC_PROVIDER_ID as unique env var
peppescg Nov 20, 2025
cddb515
refactor: update doc for env vars
peppescg Nov 20, 2025
f87d93e
lint and format
peppescg Nov 20, 2025
50f154f
feat: configure hey-api client passing the bearer token to api services
peppescg Nov 20, 2025
a57c8a5
feat(auth): handle refresh token
peppescg Nov 20, 2025
c89da94
refactor: refresh token diagram flow
peppescg Nov 20, 2025
c3a228a
fix: remove leftover catalogue page
peppescg Nov 20, 2025
04656fc
fix: api-client new instance
peppescg Nov 20, 2025
b253e31
fix: raise console error in case of missing token or userId
peppescg Nov 20, 2025
19231fe
fix: redirect to sign in if refresh token is expired
peppescg Nov 20, 2025
b46fdd6
feat: implement OIDC authentication with automatic token refresh
peppescg Nov 20, 2025
1daf0f3
refactor: move to util reusable account auth info
peppescg Nov 20, 2025
477661e
fix: update env var
peppescg Nov 20, 2025
07434cf
fix: remove throw error on better auth secret env var
peppescg Nov 20, 2025
4eb6f4a
fix: remove throw error on api base url env var
peppescg Nov 20, 2025
e33fca5
refactor: comment
peppescg Nov 20, 2025
2f6ee5d
fix: types
peppescg Nov 20, 2025
1e04eac
leftover
peppescg Nov 20, 2025
afd590c
feat(card): add server card component
peppescg Nov 20, 2025
4e3deba
fix: client api on server actions
peppescg Nov 20, 2025
99ce1ec
chore: pnpm script for run real oidc + msw
peppescg Nov 21, 2025
2085ddf
fix: redirect issue
peppescg Nov 21, 2025
7396a89
format
peppescg Nov 21, 2025
b1100e3
test: update use cases
peppescg Nov 21, 2025
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
11 changes: 9 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ export async function createServer(formData: FormData) {
### Using Generated API Client

**⚠️ IMPORTANT:**
- Never edit files in `src/generated/*`** - they are auto-generated and will be overwritten

- Never edit files in `src/generated/*`\*\* - they are auto-generated and will be overwritten
- **Always use server actions** - Client components should not call the API directly
- The API client is server-side only (no `NEXT_PUBLIC_` env vars needed)

Expand Down Expand Up @@ -325,7 +326,13 @@ pnpm generate-client # Fetch swagger.json and regenerate

- OIDC provider agnostic
- Stateless JWT authentication
- Environment variables: `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`
- Environment variables:
- `OIDC_ISSUER_URL` - OIDC provider URL
- `OIDC_CLIENT_ID` - OAuth2 client ID
- `OIDC_CLIENT_SECRET` - OAuth2 client secret
- `NEXT_PUBLIC_OIDC_PROVIDER_ID` - Provider identifier (e.g., "okta", "oidc") - **Required**, must use `NEXT_PUBLIC_` prefix. Not sensitive data - it's just an identifier.
- `BETTER_AUTH_URL` - Application base URL
- `BETTER_AUTH_SECRET` - Secret for token encryption

### Development

Expand Down
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ pnpm generate-client:nofetch # Regenerate without fetching
### Mocking & Testing

- **MSW Auto-Mocker**

- Auto-generates handlers from `swagger.json` and creates fixtures under `src/mocks/fixtures` on first run.
- Strict validation with Ajv + ajv-formats; fixtures are type-checked against `@api/types.gen` by default.
- Hand-written, non-schema mocks live in `src/mocks/customHandlers` and take precedence over schema-based mocks.
Expand Down Expand Up @@ -274,7 +275,13 @@ git push origin v0.x.x
### Authentication Not Working

- **Development**: Ensure OIDC mock is running (`pnpm oidc`)
- **Production**: Check environment variables (OIDC_ISSUER, CLIENT_ID, etc.)
- **Production**: Check environment variables:
- `OIDC_ISSUER_URL` - OIDC provider URL
- `OIDC_CLIENT_ID` - OAuth2 client ID
- `OIDC_CLIENT_SECRET` - OAuth2 client secret
- `NEXT_PUBLIC_OIDC_PROVIDER_ID` - Provider identifier (e.g., "okta", "oidc") - Required, must use `NEXT_PUBLIC_` prefix. Not sensitive data - it's just an identifier.
- `BETTER_AUTH_URL` - Application base URL
- `BETTER_AUTH_SECRET` - Secret for token encryption

### API Calls Failing

Expand Down
14 changes: 12 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand All @@ -11,10 +11,20 @@
},
"overrides": [
{
"includes": ["src/generated/core/bodySerializer.gen.ts"],
"includes": ["src/generated/**"],
"linter": {
"enabled": false
}
},
{
"includes": ["src/mocks/**"],
"linter": {
"rules": {
"suspicious": {
"noConsole": "off"
}
}
}
}
],
"formatter": {
Expand Down
9 changes: 6 additions & 3 deletions dev-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ The provider is pre-configured with:

Replace this with a real OIDC provider (Okta, Keycloak, Auth0, etc.) by updating the environment variables in `.env.local`:

- `OIDC_ISSUER`
- `OIDC_CLIENT_ID`
- `OIDC_CLIENT_SECRET`
- `OIDC_ISSUER_URL` - OIDC provider URL
- `OIDC_CLIENT_ID` - OAuth2 client ID
- `OIDC_CLIENT_SECRET` - OAuth2 client secret
- `NEXT_PUBLIC_OIDC_PROVIDER_ID` - Provider identifier (e.g., "okta", "oidc") - **Required**, must use `NEXT_PUBLIC_` prefix. Not sensitive data - it's just an identifier.
- `BETTER_AUTH_URL` - Application base URL (e.g., `http://localhost:3000`)
- `BETTER_AUTH_SECRET` - Secret for token encryption
1 change: 1 addition & 0 deletions dev-auth/oidc-provider.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const configuration = {
"http://localhost:3001/api/auth/oauth2/callback/oidc",
"http://localhost:3002/api/auth/oauth2/callback/oidc",
"http://localhost:3003/api/auth/oauth2/callback/oidc",
"http://localhost:3000/api/auth/oauth2/callback/okta",
],
response_types: ["code"],
grant_types: ["authorization_code", "refresh_token"],
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "concurrently -n \"OIDC,Mock,Next\" -c \"blue,magenta,green\" \"pnpm oidc\" \"pnpm mock:server\" \"pnpm dev:next\"",
"dev:next": "next dev",
"dev:mock-server": "concurrently -n \"Mock,Next\" -c \"magenta,green\" \"pnpm mock:server\" \"pnpm dev:next\"",
"build": "next build",
"start": "next start",
"lint": "biome check",
Expand All @@ -22,6 +23,7 @@
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-slot": "^1.2.4",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"better-auth": "1.4.0-beta.25",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 78 additions & 0 deletions src/app/api/auth/refresh-token/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { refreshAccessToken } from "@/lib/auth/auth";
import { BETTER_AUTH_SECRET, COOKIE_NAME } from "@/lib/auth/constants";
import type { OidcTokenData } from "@/lib/auth/types";
import { decrypt } from "@/lib/auth/utils";

/**
* API Route Handler to refresh OIDC access token.
*
* This Route Handler can modify cookies (unlike Server Actions during render).
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { userId } = body;

if (!userId) {
return NextResponse.json({ error: "Missing userId" }, { status: 400 });
}

const cookieStore = await cookies();
const encryptedCookie = cookieStore.get(COOKIE_NAME);

if (!encryptedCookie?.value) {
return NextResponse.json({ error: "No token found" }, { status: 401 });
}

let tokenData: OidcTokenData;
try {
tokenData = await decrypt(encryptedCookie.value, BETTER_AUTH_SECRET);
} catch (error) {
console.error("[Refresh API] Token decryption failed:", error);
cookieStore.delete(COOKIE_NAME);
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
}

if (tokenData.userId !== userId) {
console.error("[Refresh API] Token userId mismatch");
cookieStore.delete(COOKIE_NAME);
return NextResponse.json({ error: "Invalid token" }, { status: 401 });
}

if (!tokenData.refreshToken) {
console.error("[Refresh API] No refresh token available");
cookieStore.delete(COOKIE_NAME);
return NextResponse.json({ error: "No refresh token" }, { status: 401 });
}

// Call refreshAccessToken which will save the new token in the cookie
const refreshedData = await refreshAccessToken(
tokenData.refreshToken,
userId,
tokenData.refreshTokenExpiresAt,
);

if (!refreshedData) {
console.error("[Refresh API] Token refresh failed");
cookieStore.delete(COOKIE_NAME);
return NextResponse.json(
{ error: "[Refresh API] Refresh failed" },
{ status: 401 },
);
}

return NextResponse.json({
success: true,
accessToken: refreshedData.accessToken,
});
} catch (error) {
console.error("[Refresh API] Error during token refresh:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
68 changes: 47 additions & 21 deletions src/app/catalog/actions.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,54 @@
"use server";

import { getRegistryV01Servers } from "@/generated/sdk.gen";
import type { V0ServerJson } from "@/generated/types.gen";
import { getAuthenticatedClient } from "@/lib/api-client";

export async function getServers(): Promise<V0ServerJson[]> {
const api = await getAuthenticatedClient();
const resp = await api.getRegistryV01Servers({
client: api.client,
});

if (resp.error) {
console.error("[catalog] Failed to fetch servers:", resp.error);
return [];
}

if (!resp.data) {
return [];
}

const data = resp.data;
const items = Array.isArray(data?.servers) ? data.servers : [];

// Extract the server objects from the response
return items
.map((item) => item?.server)
.filter((server): server is V0ServerJson => server != null);
}

export async function getServersSummary() {
try {
const resp = await getRegistryV01Servers();
const data = resp.data;
const items = Array.isArray(data?.servers) ? data.servers : [];

const titles = items
.map((it) => it?.server?.title ?? it?.server?.name)
.filter((t): t is string => typeof t === "string")
.slice(0, 5);

const sample = items.slice(0, 5).map((it) => ({
title: it?.server?.title ?? it?.server?.name ?? "Unknown",
name: it?.server?.name ?? "unknown",
version: it?.server?.version,
}));

return { count: items.length, titles, sample };
} catch (error) {
// Log the error for debugging
console.error("[catalog] Failed to fetch servers:", error);
const api = await getAuthenticatedClient();
const resp = await api.getRegistryV01Servers({ client: api.client });

if (resp.error) {
console.error("[catalog] Failed to fetch servers:", resp.error);
return { count: 0, titles: [], sample: [] };
}

if (!resp.data) {
return { count: 0, titles: [], sample: [] };
}

const items = Array.isArray(resp.data?.servers) ? resp.data.servers : [];

const sample = items.slice(0, 5).map((it) => ({
title: it?.server?.title ?? it?.server?.name ?? "Unknown",
name: it?.server?.name ?? "unknown",
version: it?.server?.version,
}));

const titles = sample.map((s) => s.title);

return { count: items.length, titles, sample };
}
74 changes: 0 additions & 74 deletions src/app/catalogue/page.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Toaster } from "sonner";
import { Navbar } from "@/components/navbar";
import "@/lib/api-client";
import "./globals.css";

const inter = Inter({
Expand Down
Loading
Loading