diff --git a/.env.example b/.env.example index 4e1a1ad..706aaf5 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,8 @@ API_KEY=api_key_optional BASE_URL=http://localhost:3003 # Externally facing URL for this app (set to LAN IP for access from other devices on the network) AUTH_SECRET=some_random_secret_key # Generate with `openssl rand -base64 32` AUTH_TRUSTED_ORIGINS=http://localhost:3003,http://127.0.0.1:3003 # Additional trusted origins (BASE_URL is always trusted) +# (Optional) Allow anonymous auth sessions when auth is enabled (default: `false`) +USE_ANONYMOUS_AUTH_SESSIONS= # (Optional) Sign in w/ GitHub Configuration GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= diff --git a/.gitignore b/.gitignore index 38f5d2c..6d5cc68 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ node_modules/ # vscode .vscode + +# .agents +.agents diff --git a/docs-site/docs/configure/auth.md b/docs-site/docs/configure/auth.md index fff001b..c89400c 100644 --- a/docs-site/docs/configure/auth.md +++ b/docs-site/docs/configure/auth.md @@ -9,6 +9,15 @@ This page covers application-level configuration for provider access and authent - Auth is enabled only when both `BASE_URL` and `AUTH_SECRET` are set. - Remove either value to disable auth. - Keep `AUTH_TRUSTED_ORIGINS` empty to trust only `BASE_URL`. +- Anonymous auth sessions are disabled by default. +- Set `USE_ANONYMOUS_AUTH_SESSIONS=true` to enable anonymous session flows. + +## Route behavior + +- `/` is a public landing/onboarding page and remains indexable. +- `/app` is the protected app home (document list and uploader UI). +- If auth is enabled and a valid session exists (including anonymous), visiting `/` redirects to `/app`. +- Protected app routes continue to require auth; when anonymous sessions are disabled and no session exists, users are redirected to `/signin`. ## Related docs diff --git a/docs-site/docs/reference/environment-variables.md b/docs-site/docs/reference/environment-variables.md index 85fa157..b97350a 100644 --- a/docs-site/docs/reference/environment-variables.md +++ b/docs-site/docs/reference/environment-variables.md @@ -28,6 +28,7 @@ This is the single reference page for OpenReader WebUI environment variables. | `BASE_URL` | Auth | unset | Required (with `AUTH_SECRET`) to enable auth | | `AUTH_SECRET` | Auth | unset | Required (with `BASE_URL`) to enable auth | | `AUTH_TRUSTED_ORIGINS` | Auth | empty | Add extra allowed origins | +| `USE_ANONYMOUS_AUTH_SESSIONS` | Auth | `false` | Set `true` to enable anonymous auth sessions | | `GITHUB_CLIENT_ID` | Auth/OAuth | unset | Set with `GITHUB_CLIENT_SECRET` to enable GitHub sign-in | | `GITHUB_CLIENT_SECRET` | Auth/OAuth | unset | Set with `GITHUB_CLIENT_ID` to enable GitHub sign-in | | `DISABLE_AUTH_RATE_LIMIT` | Rate limiting | `false` | Set `true` to disable auth-layer rate limiting | @@ -201,6 +202,15 @@ Additional allowed origins for auth requests. - `BASE_URL` origin is always trusted automatically - Related docs: [Auth](../configure/auth) +### USE_ANONYMOUS_AUTH_SESSIONS + +Controls whether auth-enabled deployments can create/use anonymous sessions. + +- Default: `false` (anonymous sessions disabled) +- Set `true` to allow anonymous sessions and guest-style flows +- When `false`, users must sign in or sign up with an account +- Related docs: [Auth](../configure/auth) + ### GITHUB_CLIENT_ID GitHub OAuth client ID. diff --git a/playwright.config.ts b/playwright.config.ts index c7aa815..02c4d55 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { // Disable auth rate limiting for tests to support parallel workers creating sessions - command: 'npm run build && DISABLE_AUTH_RATE_LIMIT=true npm run start', + command: `npm run build && DISABLE_AUTH_RATE_LIMIT=true npm run start`, url: 'http://localhost:3003', reuseExistingServer: !process.env.CI, timeout: 120 * 1000, diff --git a/src/app/page.tsx b/src/app/(app)/app/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/(app)/app/page.tsx diff --git a/src/app/epub/[id]/page.tsx b/src/app/(app)/epub/[id]/page.tsx similarity index 99% rename from src/app/epub/[id]/page.tsx rename to src/app/(app)/epub/[id]/page.tsx index b2b4c97..f80e05f 100644 --- a/src/app/epub/[id]/page.tsx +++ b/src/app/(app)/epub/[id]/page.tsx @@ -144,7 +144,7 @@ export default function EPUBPage() {

{error}

clearCurrDoc()} className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" > @@ -162,7 +162,7 @@ export default function EPUBPage() {
clearCurrDoc()} className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" aria-label="Back to documents" diff --git a/src/app/html/[id]/page.tsx b/src/app/(app)/html/[id]/page.tsx similarity index 99% rename from src/app/html/[id]/page.tsx rename to src/app/(app)/html/[id]/page.tsx index c9cfba0..f7922fd 100644 --- a/src/app/html/[id]/page.tsx +++ b/src/app/(app)/html/[id]/page.tsx @@ -113,7 +113,7 @@ export default function HTMLPage() {

{error}

{ clearCurrDoc(); }} className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" > @@ -131,7 +131,7 @@ export default function HTMLPage() {
clearCurrDoc()} className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" aria-label="Back to documents" diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..b07c0c4 --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,62 @@ +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; +import { Toaster } from 'react-hot-toast'; + +import { Providers } from '@/app/providers'; +import ClaimDataPopup from '@/components/auth/ClaimDataModal'; +import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled } from '@/lib/server/auth-config'; + +export const dynamic = 'force-dynamic'; + +export const metadata: Metadata = { + robots: { + index: false, + follow: false, + googleBot: { + index: false, + follow: false, + noimageindex: true, + 'max-snippet': 0, + 'max-video-preview': 0, + }, + }, +}; + +export default function AppLayout({ children }: { children: ReactNode }) { + const authEnabled = isAuthEnabled(); + const authBaseUrl = getAuthBaseUrl(); + const allowAnonymousAuthSessions = isAnonymousAuthSessionsEnabled(); + + return ( + +
+ {authEnabled && } +
{children}
+
+ +
+ ); +} diff --git a/src/app/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx similarity index 99% rename from src/app/pdf/[id]/page.tsx rename to src/app/(app)/pdf/[id]/page.tsx index ff25cb8..45ca251 100644 --- a/src/app/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -140,7 +140,7 @@ export default function PDFViewerPage() {

{error}

{ clearCurrDoc(); }} className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" > @@ -158,7 +158,7 @@ export default function PDFViewerPage() {
clearCurrDoc()} className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition-all duration-200 ease-in-out hover:scale-[1.04] hover:text-accent" aria-label="Back to documents" diff --git a/src/app/signin/page.tsx b/src/app/(app)/signin/page.tsx similarity index 93% rename from src/app/signin/page.tsx rename to src/app/(app)/signin/page.tsx index 5348213..c1a9962 100644 --- a/src/app/signin/page.tsx +++ b/src/app/(app)/signin/page.tsx @@ -29,7 +29,7 @@ function SignInContent() { const [rememberMe, setRememberMe] = useState(true); const [sessionExpired, setSessionExpired] = useState(false); const [error, setError] = useState(null); - const { authEnabled, baseUrl } = useAuthConfig(); + const { authEnabled, baseUrl, allowAnonymousAuthSessions } = useAuthConfig(); const { refresh: refreshRateLimit } = useAuthRateLimit(); const isAnyLoading = loadingEmail || loadingGithub || loadingAnonymous; @@ -37,7 +37,7 @@ function SignInContent() { // Check if auth is enabled, redirect home if not useEffect(() => { if (!authEnabled) { - router.push('/'); + router.push('/app'); } }, [router, authEnabled]); @@ -79,7 +79,7 @@ function SignInContent() { // Immediately refresh rate-limit status so the banner clears without a full reload. // This is especially important when an anonymous user upgrades to an account. await refreshRateLimit(); - router.push('/'); + router.push('/app'); } } catch (err) { console.error('Sign in error:', err); @@ -95,7 +95,7 @@ function SignInContent() { const client = getAuthClient(baseUrl); await client.signIn.social({ provider: 'github', - callbackURL: '/' + callbackURL: '/app' }); } finally { setLoadingGithub(false); @@ -109,7 +109,7 @@ function SignInContent() { const client = getAuthClient(baseUrl); await client.signIn.anonymous(); await refreshRateLimit(); - router.push('/'); + router.push('/app'); } catch (e) { console.error('Anonymous sign-in failed:', e); setError('Unable to continue anonymously. Please try again.'); @@ -226,17 +226,19 @@ function SignInContent() { {/* Anonymous */} - + > + {loadingAnonymous ? : 'Continue anonymously'} + + )}
{/* Footer */} diff --git a/src/app/signup/page.tsx b/src/app/(app)/signup/page.tsx similarity index 99% rename from src/app/signup/page.tsx rename to src/app/(app)/signup/page.tsx index d18b0c2..6bc4efe 100644 --- a/src/app/signup/page.tsx +++ b/src/app/(app)/signup/page.tsx @@ -24,7 +24,7 @@ export default function SignUpPage() { // Check if auth is enabled, redirect home if not useEffect(() => { if (!authEnabled) { - router.push('/'); + router.push('/app'); } }, [router, authEnabled]); @@ -87,7 +87,7 @@ export default function SignUpPage() { } else { await refreshRateLimit(); toast.success('Account created successfully!'); - router.push('/'); + router.push('/app'); } } } catch (err) { diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx new file mode 100644 index 0000000..b998741 --- /dev/null +++ b/src/app/(public)/layout.tsx @@ -0,0 +1,327 @@ +import type { ReactNode } from 'react'; +import Link from 'next/link'; + +export default function PublicLayout({ children }: { children: ReactNode }) { + return ( + <> + + +
+ {/* ── Ambient orbs ────────────── */} +