diff --git a/.env.example b/.env.example index fef6ed3..2cd8c09 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/template REDIS_URL=redis://localhost:6379 # Auth -# AUTH_METHOD options: magic-link | pki | pki-and-magic-link | google-oauth | other +# AUTH_METHOD options: magic-link | pki | pki-and-magic-link | google-oauth | other | none AUTH_METHOD=magic-link BETTER_AUTH_SECRET=replace-with-32-byte-random-string BETTER_AUTH_URL=http://localhost:3000 diff --git a/VERSION b/VERSION index 66c4c22..7ee7020 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.9 +1.0.10 diff --git a/apps/web/src/lib/container.ts b/apps/web/src/lib/container.ts index 4cd0f9d..59c1660 100644 --- a/apps/web/src/lib/container.ts +++ b/apps/web/src/lib/container.ts @@ -81,6 +81,8 @@ const build = () => { return { type: "google-oauth" as const }; case "other": return { type: "other" as const }; + case "none": + return { type: "none" as const }; default: return { type: "magic-link" as const, sendMagicLink }; } diff --git a/apps/web/src/lib/env.ts b/apps/web/src/lib/env.ts index b7821b1..11990a0 100644 --- a/apps/web/src/lib/env.ts +++ b/apps/web/src/lib/env.ts @@ -14,7 +14,7 @@ const serverEnvSchema = z.object({ LANGFUSE_SECRET_KEY: z.string().optional(), LANGFUSE_HOST: z.string().url().optional(), AUTH_METHOD: z - .enum(["magic-link", "pki", "pki-and-magic-link", "google-oauth", "other"]) + .enum(["magic-link", "pki", "pki-and-magic-link", "google-oauth", "other", "none"]) .default("magic-link"), PKI_TRUSTED_PROXY_IPS: z.string().optional(), PKI_SESSION_TTL_HOURS: z.coerce.number().int().positive().default(8), diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index d9a5eef..b8a87e8 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -9,6 +9,10 @@ const PKI_MODES = new Set(["pki", "pki-and-magic-link"]); * cert headers are consumed server-side; all other modes go to /admin/login. */ export const middleware = (req: NextRequest): NextResponse => { + if (AUTH_METHOD === "none") { + return NextResponse.next(); + } + const { pathname } = req.nextUrl; if (!pathname.startsWith("/admin") || pathname.startsWith("/admin/login")) { diff --git a/docs/development/implemented/v1.0.10/auth-none-option.md b/docs/development/implemented/v1.0.10/auth-none-option.md new file mode 100644 index 0000000..3f41d34 --- /dev/null +++ b/docs/development/implemented/v1.0.10/auth-none-option.md @@ -0,0 +1,54 @@ +# Auth Method: none + +## Version Bump +PATCH — 1.0.9 → 1.0.10 (no DB schema changes) + +## What & Why +Add `none` as a valid value for `AUTH_METHOD`. When selected, the middleware +skips all authentication checks and all `/admin/*` routes are publicly +accessible with no session required. Intended for local development, internal +tools, or deployments where the network perimeter provides access control. + +## Affected Files + +### `packages/adapters/src/auth/better-auth.ts` +- Extend `AuthMethod` union with `{ readonly type: "none" }`. +- `createAuth` treats `none` the same as `other`: no plugins, no error thrown. + Better Auth is still initialised so the `/api/auth/*` routes remain mounted. + +### `apps/web/src/lib/env.ts` +- Add `"none"` to the `AUTH_METHOD` z.enum values. + +### `apps/web/src/lib/container.ts` +- Add `case "none": return { type: "none" as const }` to the `authMethod` + factory switch. + +### `apps/web/src/middleware.ts` +- When `AUTH_METHOD === "none"` return `NextResponse.next()` immediately, + skipping all session and redirect logic for `/admin/*` routes. + +### `scripts/init-project.sh` +- Add option `6) none (no authentication — all routes public)` to the auth + method prompt. +- Map choice `6` → `AUTH_METHOD="none"`. +- When `AUTH_METHOD=none`, add a warning that all admin routes are unprotected. + +### `packages/create/src/index.ts` +- Add `authMethod` prompt (select, after AI provider) to `collectInputs`. +- Add `authMethod` to `ScaffoldOptions` and the Summary display. +- Include `AUTH_METHOD` in `envReplacements` so `.env` is written correctly. +- Add `none` option to the choices list, labelled "None (all routes public)". + +### `.env.example` +- Update the `AUTH_METHOD` comment line to include `none`: + `# AUTH_METHOD options: magic-link | pki | pki-and-magic-link | google-oauth | other | none` + +## Acceptance Criteria +1. `AUTH_METHOD=none` passes Zod validation in `env.ts`. +2. `AuthMethod` type includes `{ type: "none" }`. +3. Middleware with `AUTH_METHOD=none` returns `NextResponse.next()` for all + `/admin/*` paths (no redirect, no session check). +4. `init-project.sh` offers option 6 and sets `AUTH_METHOD=none` in `.env.example`. +5. `packages/create` prompts for auth method including `none`, writes correct + `.env`. +6. `validate.sh` passes on 1.0.10. diff --git a/docs/development/implemented/v1.0.10/implementation-summary.md b/docs/development/implemented/v1.0.10/implementation-summary.md new file mode 100644 index 0000000..014e2d5 --- /dev/null +++ b/docs/development/implemented/v1.0.10/implementation-summary.md @@ -0,0 +1,25 @@ +# Implementation Summary — v1.0.10 Auth Method: none + +**Version bump:** 1.0.9 → 1.0.10 (PATCH — no DB schema changes) + +## What was built + +Added `none` as a valid `AUTH_METHOD` option. When selected, the middleware +short-circuits all authentication checks and all routes (including `/admin/*`) +are publicly accessible with no session required. Intended for local +development, internal tools, or deployments where network perimeter controls +access. + +## Files modified + +| Path | Change | +|---|---| +| `packages/adapters/src/auth/better-auth.ts` | Added `{ readonly type: "none" }` to `AuthMethod` union | +| `apps/web/src/lib/env.ts` | Added `"none"` to `AUTH_METHOD` Zod enum | +| `apps/web/src/lib/container.ts` | Added `case "none"` to authMethod factory switch | +| `apps/web/src/middleware.ts` | Early `NextResponse.next()` return when `AUTH_METHOD === "none"` | +| `scripts/init-project.sh` | Added option 6 for `none`; warning when selected | +| `packages/create/src/index.ts` | Added `authMethod` select prompt; wired through `ScaffoldOptions` and `envReplacements` | +| `.env.example` | Updated `AUTH_METHOD` comment to include `none` | +| `VERSION` | 1.0.9 → 1.0.10 | +| `package.json` | 1.0.9 → 1.0.10 | diff --git a/package.json b/package.json index d91c2c7..aa0d92c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "template", - "version": "1.0.9", + "version": "1.0.10", "private": true, "description": "Production-ready AI application monorepo template (hexagonal architecture).", "packageManager": "pnpm@9.12.0", diff --git a/packages/adapters/src/auth/better-auth.ts b/packages/adapters/src/auth/better-auth.ts index adffdb6..d882ea3 100644 --- a/packages/adapters/src/auth/better-auth.ts +++ b/packages/adapters/src/auth/better-auth.ts @@ -16,7 +16,8 @@ export type AuthMethod = readonly sendMagicLink: (params: { email: string; url: string }) => Promise; } | { readonly type: "google-oauth" } - | { readonly type: "other" }; + | { readonly type: "other" } + | { readonly type: "none" }; export interface AuthConfig { readonly secret: string; diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts index 66e087d..fc72be0 100644 --- a/packages/create/src/index.ts +++ b/packages/create/src/index.ts @@ -30,6 +30,7 @@ const FRAMEWORK_SCOPE = "@rbrasier"; const FRAMEWORK_PKGS = ["domain", "shared", "application", "adapters"] as const; type AiProvider = "anthropic" | "openai" | "mistral"; +type AuthMethod = "magic-link" | "pki" | "pki-and-magic-link" | "google-oauth" | "other" | "none"; type DbSetup = "local" | "docker" | "url"; interface ScaffoldOptions { @@ -37,6 +38,7 @@ interface ScaffoldOptions { appScope: string; aiProvider: AiProvider; aiProviderKey: string; + authMethod: AuthMethod; langfuseEnabled: boolean; databaseUrl: string; dbSetup: DbSetup; @@ -114,6 +116,21 @@ async function collectInputs(): Promise { initial: "", }); + const { authMethod } = await prompts({ + type: "select", + name: "authMethod", + message: "Authentication method", + choices: [ + { title: "Magic link (email, no password)", value: "magic-link" }, + { title: "PKI / client certificate", value: "pki" }, + { title: "PKI + magic link fallback", value: "pki-and-magic-link" }, + { title: "Google OAuth (requires additional setup)", value: "google-oauth" }, + { title: "Other (configure manually)", value: "other" }, + { title: "None (all routes public — dev/internal only)", value: "none" }, + ], + }); + if (!authMethod) process.exit(0); + const { dbInput } = await prompts({ type: "text", name: "dbInput", @@ -193,6 +210,7 @@ async function collectInputs(): Promise { console.log(` Project name : ${pc.cyan(projectName)}`); console.log(` App scope : ${pc.cyan(appScope)}`); console.log(` AI provider : ${pc.cyan(aiProvider)}`); + console.log(` Auth method : ${pc.cyan(authMethod)}`); console.log(` Database : ${pc.cyan(databaseUrl)}`); console.log(` Admin email : ${pc.cyan(adminEmail)}`); console.log(` Langfuse : ${pc.cyan(String(langfuseEnabled))}`); @@ -212,6 +230,7 @@ async function collectInputs(): Promise { appScope, aiProvider: aiProvider as AiProvider, aiProviderKey: aiProviderKey ?? "", + authMethod: authMethod as AuthMethod, langfuseEnabled, databaseUrl, dbSetup, @@ -230,7 +249,7 @@ function replaceInFile(filePath: string, find: string, replace: string) { async function scaffold(opts: ScaffoldOptions) { const { - projectName, appScope, aiProvider, aiProviderKey, + projectName, appScope, aiProvider, aiProviderKey, authMethod, langfuseEnabled, databaseUrl, dbSetup, adminEmail, authSecret, targetDir, } = opts; @@ -324,6 +343,7 @@ async function scaffold(opts: ScaffoldOptions) { BETTER_AUTH_SECRET: authSecret, ADMIN_SEED_EMAIL: adminEmail, AI_DEFAULT_PROVIDER: aiProvider, + AUTH_METHOD: authMethod, OTEL_SERVICE_NAME: `${projectName}-api`, }; @@ -404,6 +424,11 @@ try { console.log(pc.yellow(` ! Add your ${AI_KEY_NAMES[aiProvider]} to .env before starting.`)); console.log(); } + if (authMethod === "none") { + console.log(pc.yellow(" ! Auth method is \"none\" — all /admin/* routes are publicly accessible.")); + console.log(pc.yellow(" Do not use this setting in production.")); + console.log(); + } console.log(" Push to GitHub:"); console.log(" git remote add origin && git push -u origin main"); console.log(); diff --git a/scripts/init-project.sh b/scripts/init-project.sh index 1dfc7c8..f58c444 100755 --- a/scripts/init-project.sh +++ b/scripts/init-project.sh @@ -119,6 +119,7 @@ echo " 2) pki (client certificate via reverse proxy)" echo " 3) pki-and-magic-link (PKI primary, magic link fallback)" echo " 4) google-oauth (Google OAuth — requires additional setup)" echo " 5) other (configure manually)" +echo " 6) none (no auth — all routes public, dev/internal only)" prompt "Choice [1]: " read -r AUTH_CHOICE case "${AUTH_CHOICE:-1}" in @@ -126,6 +127,7 @@ case "${AUTH_CHOICE:-1}" in 3) AUTH_METHOD="pki-and-magic-link" ;; 4) AUTH_METHOD="google-oauth" ;; 5) AUTH_METHOD="other" ;; + 6) AUTH_METHOD="none" ;; *) AUTH_METHOD="magic-link" ;; esac @@ -290,6 +292,10 @@ if [ -f .env.example ]; then warning "google-oauth requires additional setup. See docs/guides/google-oauth.md." fi + if [ "$AUTH_METHOD" = "none" ]; then + warning "none auth selected — all /admin/* routes are publicly accessible. Do not use in production." + fi + if [ "$LANGFUSE_ENABLED" = "n" ]; then info "Langfuse disabled — commenting keys in .env.example…" sed_inplace "s|^LANGFUSE_|# LANGFUSE_|g" .env.example