Skip to content
This repository was archived by the owner on Apr 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ INITIAL_ADMIN_EMAIL="admin@example.com"

# Secure values: generate with `openssl rand -base64 32`
AUTH_SECRET="REPLACE_WITH_A_SECURE_RANDOM_VALUE"
INITIAL_ADMIN_PASSWORD="REPLACE_WITH_A_SECURE_RANDOM_VALUE"

# For Testing Only
# Cookies are marked secure only when AUTH_URL starts with https://
Expand All @@ -21,9 +20,9 @@ AUTH_TRUST_HOST=true
#GOOGLE_CLIENT_ID=
#GOOGLE_CLIENT_SECRET=

# Optional: toggle password auth (default: enabled)
# Set to "false" to disable the credentials provider and hide the password form.
#AUTH_CREDENTIALS_ENABLED=true
# Optional: enable passkey auth (default: disabled)
# Set to "true" to allow passkey login and enrollment.
#AUTH_PASSKEYS_ENABLED=true

# Optional: logging level (default: debug in dev, info in prod)
#LOG_LEVEL=info
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Use `eslint-plugin-boundaries` and `no-restricted-imports` to discourage cross
- Do not duplicate auth checks in child layouts/pages under the group. Rely on the group layout for auth.
- Keep `src/app/(protected-routes)/settings/layout.tsx` for the admin-only rule; it should only enforce `session.user.role === ADMIN` (assumes auth already passed).
- Keep public auth at `src/app/(public-routes)/auth/signin/**`.
- `/auth/change-password` lives inside the protected group; middleware explicitly allows it during `mustChangePassword` flows.
- Passkey enrollment happens from the account page after first login via a one-time link.
- The homepage `/` is under the protected group and does not need page-level `auth()`.

## API Architecture
Expand Down
4 changes: 1 addition & 3 deletions deploy/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ services:
AUTH_URL: ${TTPX_AUTH_URL}
AUTH_SECRET: ${TTPX_AUTH_SECRET}
INITIAL_ADMIN_EMAIL: ${TTPX_INITIAL_ADMIN_EMAIL}
INITIAL_ADMIN_PASSWORD: ${TTPX_INITIAL_ADMIN_PASSWORD}
AUTH_CREDENTIALS_ENABLED: ${TTPX_AUTH_CREDENTIALS_ENABLED}
AUTH_PASSKEYS_ENABLED: ${TTPX_AUTH_PASSKEYS_ENABLED}
GOOGLE_CLIENT_ID: ${TTPX_GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET: ${TTPX_GOOGLE_CLIENT_SECRET}
# INITIAL_ADMIN_PASSWORD_FILE: /run/secrets/initial_admin_password
ports:
- "${TTPX_PORT}:${TTPX_PORT}"
restart: unless-stopped
Expand Down
8 changes: 4 additions & 4 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ docker system prune -a --volumes

Notes:

- First login forces a password change.
- The initialization script prints a one-time login URL for the initial admin. After using it, register a passkey from the account page.

## Logging

Expand All @@ -56,13 +56,13 @@ Notes:

SSO is enabled through environment variables. Users must be provisioned ahead of time; they are **not** auto-created on first SSO login.

For a pure-SSO setup, set `INITIAL_ADMIN_EMAIL` to a value from the SSO provider and disable password authentication if desired.
For a pure-SSO setup, set `INITIAL_ADMIN_EMAIL` to a value from the SSO provider. Passkeys can be enabled alongside SSO when desired.

Environment variables:

```
# Toggle credentials provider (default: enabled)
AUTH_CREDENTIALS_ENABLED=true
# Toggle passkey provider (default: disabled)
AUTH_PASSKEYS_ENABLED=true

# Register Google provider when present (optional)
GOOGLE_CLIENT_ID=
Expand Down
19 changes: 0 additions & 19 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,6 @@ export const middleware = auth(async (request) => {
signInUrl.searchParams.set('callbackUrl', request.url);
return NextResponse.redirect(signInUrl);
}
// Enforce password change if the token/session indicates reset is required
const mustChange = (session.user as { mustChangePassword?: boolean }).mustChangePassword === true;
if (mustChange) {
const isApiRoute = pathname.startsWith('/api') && !pathname.startsWith('/api/auth');
// Allow the password change page and the specific tRPC mutation endpoint used by that page
const isChangePage =
pathname.startsWith('/auth/change-password') ||
pathname.startsWith('/api/auth') ||
pathname.startsWith('/api/trpc/users.changeOwnPassword');
if (!isChangePage) {
if (isApiRoute) {
return NextResponse.json({ error: 'PASSWORD_CHANGE_REQUIRED' }, { status: 403 });
}
const changeUrl = new URL('/auth/change-password', request.url);
changeUrl.searchParams.set('callbackUrl', request.url);
return NextResponse.redirect(changeUrl);
}
}

return NextResponse.next();
});

Expand Down
Loading
Loading