Fix Docker signup and document deploy, migrate, and rebuild flow.
Use FormData and server-side sign-in after register, trust proxy origins for reverse-proxy deploys, pass public URL build args in Docker, and clarify that git pull plus up -d --build is required to ship code and run pending migrations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+14
-1
@@ -1,6 +1,17 @@
|
|||||||
# beenvoice-web environment
|
# beenvoice-web environment
|
||||||
|
#
|
||||||
# Local dev: cp .env.example .env.local
|
# Local dev: cp .env.example .env.local
|
||||||
# Docker: cp .env.example .env
|
# Docker: cp .env.example .env
|
||||||
|
#
|
||||||
|
# Docker deploy checklist:
|
||||||
|
# 1. Set AUTH_SECRET, BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL to your public URL
|
||||||
|
# 2. docker compose build --no-cache app # NEXT_PUBLIC_* is baked in at build
|
||||||
|
# 3. docker compose up -d --build # migrations run on app container start
|
||||||
|
#
|
||||||
|
# Updating: git pull && docker compose up -d --build
|
||||||
|
# (git pull alone does not rebuild; up -d without --build keeps the old image)
|
||||||
|
#
|
||||||
|
# Migrations are idempotent — only pending SQL files are applied on each start.
|
||||||
|
|
||||||
# Runtime
|
# Runtime
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
@@ -9,9 +20,11 @@ WEB_PORT=3000
|
|||||||
# Auth
|
# Auth
|
||||||
# Generate with: openssl rand -base64 32
|
# Generate with: openssl rand -base64 32
|
||||||
AUTH_SECRET=change-me-generate-a-real-secret
|
AUTH_SECRET=change-me-generate-a-real-secret
|
||||||
|
# Must match the URL users open in the browser (include https:// and port if non-standard).
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
# Public app URL
|
# Public app URL — baked into the client bundle at Docker build time.
|
||||||
|
# Set this to the same value as BETTER_AUTH_URL before `docker compose build`.
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
|
||||||
# Postgres used by docker-compose.yml
|
# Postgres used by docker-compose.yml
|
||||||
|
|||||||
+5
-1
@@ -10,10 +10,14 @@ FROM base AS build
|
|||||||
COPY --from=install /usr/src/app/node_modules node_modules
|
COPY --from=install /usr/src/app/node_modules node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
ARG BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
SKIP_ENV_VALIDATION=1 \
|
SKIP_ENV_VALIDATION=1 \
|
||||||
NODE_OPTIONS=--max-old-space-size=4096 \
|
NODE_OPTIONS=--max-old-space-size=4096 \
|
||||||
BETTER_AUTH_URL=http://localhost:3000 \
|
BETTER_AUTH_URL=${BETTER_AUTH_URL} \
|
||||||
|
NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} \
|
||||||
AUTH_SECRET=docker-build-placeholder-secret-do-not-use \
|
AUTH_SECRET=docker-build-placeholder-secret-do-not-use \
|
||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ Open [http://localhost:3000](http://localhost:3000), register at `/auth/register
|
|||||||
|
|
||||||
## Docker deployment (app + database)
|
## Docker deployment (app + database)
|
||||||
|
|
||||||
The production compose file runs the Next.js app and PostgreSQL. Migrations run automatically on container start (`bun migrate.ts` in the image `CMD`).
|
The production compose file runs the Next.js app and PostgreSQL.
|
||||||
|
|
||||||
|
**Container startup** runs `bun migrate.ts && bun run start` (see `Dockerfile`). Drizzle only applies **pending** migrations — safe to run on every restart; already-applied migrations are skipped.
|
||||||
|
|
||||||
### 1. Configure
|
### 1. Configure
|
||||||
|
|
||||||
@@ -106,23 +108,43 @@ BETTER_AUTH_URL=https://your-public-hostname
|
|||||||
NEXT_PUBLIC_APP_URL=https://your-public-hostname
|
NEXT_PUBLIC_APP_URL=https://your-public-hostname
|
||||||
```
|
```
|
||||||
|
|
||||||
`BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` must match the URL users actually use in the browser. If they point at `localhost` but you access the app via another hostname, auth (sign-in / sign-up) will fail.
|
`BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` must match the URL users actually use in the browser (scheme + host + port). If they point at `localhost` but you access the app via another hostname, sign-up and sign-in will fail (often with a vague **"REQUIRED"** toast).
|
||||||
|
|
||||||
`NEXT_PUBLIC_*` values are embedded at **image build** time. Rebuild after changing white-label or Authentik client flags:
|
`NEXT_PUBLIC_*` values are embedded at **image build** time. Rebuild after changing `NEXT_PUBLIC_APP_URL`, white-label defaults, or `NEXT_PUBLIC_AUTHENTIK_ENABLED`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose build --no-cache app
|
docker compose build --no-cache app
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Start
|
`BETTER_AUTH_URL` and `AUTH_SECRET` are read at **container runtime** from `.env` — you can change them without rebuilding, then restart the app container.
|
||||||
|
|
||||||
|
### 2. First start (or after code changes)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--build` is important. A plain `docker compose up -d` reuses the existing image and **does not** pick up new code from `git pull`.
|
||||||
|
|
||||||
App listens on `${WEB_PORT:-3000}`. Postgres stays on the internal compose network.
|
App listens on `${WEB_PORT:-3000}`. Postgres stays on the internal compose network.
|
||||||
|
|
||||||
### 3. Sign-ups
|
### 3. Updating an existing deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
docker compose up -d --build # rebuild image, restart app, run any new migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
| Command | New code? | Migrations run? |
|
||||||
|
|---------|-----------|-----------------|
|
||||||
|
| `git pull` only | No | No |
|
||||||
|
| `docker compose up -d` (no `--build`) | No — old image | Only if the app container restarts (same image) |
|
||||||
|
| `docker compose up -d --build` | Yes | Yes — on app container start |
|
||||||
|
| `docker compose restart app` | No | Yes — migrate runs again (no-op if up to date) |
|
||||||
|
|
||||||
|
To verify migration files match the journal before deploy: `bun run db:verify-journal`.
|
||||||
|
|
||||||
|
### 4. Sign-ups
|
||||||
|
|
||||||
Registration is **enabled** by default. To block new email/password accounts:
|
Registration is **enabled** by default. To block new email/password accounts:
|
||||||
|
|
||||||
@@ -132,7 +154,7 @@ DISABLE_SIGNUPS=true
|
|||||||
|
|
||||||
Use the literal strings `true` or `false` (or omit the variable). Do not rely on bare boolean coercion from shell/compose — the app parses these explicitly.
|
Use the literal strings `true` or `false` (or omit the variable). Do not rely on bare boolean coercion from shell/compose — the app parses these explicitly.
|
||||||
|
|
||||||
### 4. Optional services
|
### 5. Optional services
|
||||||
|
|
||||||
| Variable | Purpose |
|
| Variable | Purpose |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ services:
|
|||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
args:
|
||||||
|
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
||||||
|
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
|
||||||
image: beenvoice:local
|
image: beenvoice:local
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
|||||||
@@ -2,31 +2,19 @@ import bcrypt from "bcryptjs";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { type NextRequest, NextResponse } from "next/server";
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { auth } from "~/lib/auth";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { accounts, users } from "~/server/db/schema";
|
import { accounts, users } from "~/server/db/schema";
|
||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
firstName: z.string().trim().optional(),
|
firstName: z.string().trim().min(1, "First name is required"),
|
||||||
lastName: z.string().trim().optional(),
|
lastName: z.string().trim().min(1, "Last name is required"),
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
email: z.string().email("Invalid email address"),
|
email: z.string().email("Invalid email address"),
|
||||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
|
||||||
const hasSplitName =
|
|
||||||
Boolean(data.firstName?.length) && Boolean(data.lastName?.length);
|
|
||||||
const hasFullName = Boolean(data.name?.length);
|
|
||||||
|
|
||||||
if (!hasSplitName && !hasFullName) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "First and last name are required",
|
|
||||||
path: ["firstName"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.transform((data) => {
|
.transform((data) => {
|
||||||
if (data.name?.length) {
|
if (data.name?.length) {
|
||||||
const parts = data.name.trim().split(/\s+/);
|
const parts = data.name.trim().split(/\s+/);
|
||||||
@@ -41,8 +29,8 @@ const registerSchema = z
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
firstName: data.firstName!.trim(),
|
firstName: data.firstName,
|
||||||
lastName: data.lastName!.trim(),
|
lastName: data.lastName,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
};
|
};
|
||||||
@@ -151,6 +139,22 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await auth.api.signInEmail({
|
||||||
|
body: {
|
||||||
|
email: normalizedEmail,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
} catch (signInError) {
|
||||||
|
console.error("Post-register sign-in failed:", signInError);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "User created successfully", signInRequired: true },
|
||||||
|
{ status: 201 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "User created successfully" },
|
{ message: "User created successfully" },
|
||||||
{ status: 201 },
|
{ status: 201 },
|
||||||
|
|||||||
@@ -9,24 +9,27 @@ import { Label } from "~/components/ui/label";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Logo } from "~/components/branding/logo";
|
import { Logo } from "~/components/branding/logo";
|
||||||
import { LegalAgreementNotice } from "~/components/legal/legal-links";
|
import { LegalAgreementNotice } from "~/components/legal/legal-links";
|
||||||
import { authClient } from "~/lib/auth-client";
|
|
||||||
import { Mail, Lock, ArrowRight, User } from "lucide-react";
|
import { Mail, Lock, ArrowRight, User } from "lucide-react";
|
||||||
|
|
||||||
|
function formatAuthError(message: string | undefined, fallback: string): string {
|
||||||
|
if (!message || message === "Required") {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
function RegisterForm() {
|
function RegisterForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [firstName, setFirstName] = useState("");
|
|
||||||
const [lastName, setLastName] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
async function handleRegister(e: React.FormEvent) {
|
async function handleRegister(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const trimmedFirstName = firstName.trim();
|
const formData = new FormData(e.currentTarget);
|
||||||
const trimmedLastName = lastName.trim();
|
const trimmedFirstName = String(formData.get("firstName") ?? "").trim();
|
||||||
const trimmedEmail = email.trim();
|
const trimmedLastName = String(formData.get("lastName") ?? "").trim();
|
||||||
|
const trimmedEmail = String(formData.get("email") ?? "").trim();
|
||||||
|
const password = String(formData.get("password") ?? "");
|
||||||
|
|
||||||
if (!trimmedFirstName || !trimmedLastName || !trimmedEmail) {
|
if (!trimmedFirstName || !trimmedLastName || !trimmedEmail) {
|
||||||
toast.error("Please enter your first name, last name, and email.");
|
toast.error("Please enter your first name, last name, and email.");
|
||||||
@@ -52,19 +55,22 @@ function RegisterForm() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = (await res.json()) as { error?: string };
|
let data: { error?: string; signInRequired?: boolean } = {};
|
||||||
|
try {
|
||||||
if (!res.ok) {
|
data = (await res.json()) as typeof data;
|
||||||
toast.error(data.error ?? "Registration failed");
|
} catch {
|
||||||
|
toast.error("Registration failed. Please try again.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error: signInError } = await authClient.signIn.email({
|
if (!res.ok) {
|
||||||
email: trimmedEmail,
|
toast.error(
|
||||||
password,
|
formatAuthError(data.error, "Registration failed. Please check the form."),
|
||||||
});
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (signInError) {
|
if (data.signInRequired) {
|
||||||
toast.success("Account created! Please sign in.");
|
toast.success("Account created! Please sign in.");
|
||||||
router.push("/auth/signin");
|
router.push("/auth/signin");
|
||||||
return;
|
return;
|
||||||
@@ -106,11 +112,11 @@ function RegisterForm() {
|
|||||||
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
id="firstName"
|
id="firstName"
|
||||||
|
name="firstName"
|
||||||
type="text"
|
type="text"
|
||||||
value={firstName}
|
|
||||||
onChange={(e) => setFirstName(e.target.value)}
|
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
|
autoComplete="given-name"
|
||||||
className="h-10 pl-10"
|
className="h-10 pl-10"
|
||||||
placeholder="John"
|
placeholder="John"
|
||||||
/>
|
/>
|
||||||
@@ -123,10 +129,10 @@ function RegisterForm() {
|
|||||||
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
<User className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
id="lastName"
|
id="lastName"
|
||||||
|
name="lastName"
|
||||||
type="text"
|
type="text"
|
||||||
value={lastName}
|
|
||||||
onChange={(e) => setLastName(e.target.value)}
|
|
||||||
required
|
required
|
||||||
|
autoComplete="family-name"
|
||||||
className="h-10 pl-10"
|
className="h-10 pl-10"
|
||||||
placeholder="Doe"
|
placeholder="Doe"
|
||||||
/>
|
/>
|
||||||
@@ -140,10 +146,10 @@ function RegisterForm() {
|
|||||||
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
<Mail className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
required
|
required
|
||||||
|
autoComplete="email"
|
||||||
className="h-10 pl-10"
|
className="h-10 pl-10"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
/>
|
/>
|
||||||
@@ -156,11 +162,11 @@ function RegisterForm() {
|
|||||||
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
<Lock className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-10 h-4 w-4 -translate-y-1/2" />
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
|
autoComplete="new-password"
|
||||||
className="h-10 pl-10"
|
className="h-10 pl-10"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(error.message ?? "Invalid email or password");
|
toast.error(
|
||||||
|
error.message && error.message !== "Required"
|
||||||
|
? error.message
|
||||||
|
: "Invalid email or password",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.success("Signed in successfully!");
|
toast.success("Signed in successfully!");
|
||||||
router.push(callbackUrl);
|
router.push(callbackUrl);
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ import { genericOAuthClient } from "better-auth/client/plugins";
|
|||||||
* Auth client configuration
|
* Auth client configuration
|
||||||
*/
|
*/
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_APP_URL,
|
||||||
plugins: [genericOAuthClient()],
|
plugins: [genericOAuthClient()],
|
||||||
});
|
});
|
||||||
|
|||||||
+33
-9
@@ -3,6 +3,7 @@ import { betterAuth } from "better-auth";
|
|||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { nextCookies } from "better-auth/next-js";
|
import { nextCookies } from "better-auth/next-js";
|
||||||
import { genericOAuth } from "better-auth/plugins";
|
import { genericOAuth } from "better-auth/plugins";
|
||||||
|
import { envBoolean } from "~/lib/env-boolean";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import * as schema from "~/server/db/schema";
|
import * as schema from "~/server/db/schema";
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ const authentikEnabled = Boolean(
|
|||||||
process.env.AUTHENTIK_CLIENT_ID &&
|
process.env.AUTHENTIK_CLIENT_ID &&
|
||||||
process.env.AUTHENTIK_CLIENT_SECRET,
|
process.env.AUTHENTIK_CLIENT_SECRET,
|
||||||
);
|
);
|
||||||
const signupsDisabled = process.env.DISABLE_SIGNUPS === "true";
|
const signupsDisabled = envBoolean(process.env.DISABLE_SIGNUPS);
|
||||||
|
|
||||||
// Derive the authentik origin from the issuer URL so the OAuth callback is
|
// Derive the authentik origin from the issuer URL so the OAuth callback is
|
||||||
// automatically trusted without needing a separate AUTHENTIK_ORIGIN env var.
|
// automatically trusted without needing a separate AUTHENTIK_ORIGIN env var.
|
||||||
@@ -20,9 +21,21 @@ const authentikOrigin =
|
|||||||
? new URL(process.env.AUTHENTIK_ISSUER).origin
|
? new URL(process.env.AUTHENTIK_ISSUER).origin
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const staticTrustedOrigins = [
|
||||||
|
...(process.env.BETTER_AUTH_URL ? [process.env.BETTER_AUTH_URL] : []),
|
||||||
|
...(process.env.NEXT_PUBLIC_APP_URL ? [process.env.NEXT_PUBLIC_APP_URL] : []),
|
||||||
|
"beenvoice://",
|
||||||
|
"exp://",
|
||||||
|
...(authentikOrigin ? [authentikOrigin] : []),
|
||||||
|
...(process.env.AUTHENTIK_ORIGIN ? [process.env.AUTHENTIK_ORIGIN] : []),
|
||||||
|
];
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
baseURL: process.env.BETTER_AUTH_URL,
|
baseURL: process.env.BETTER_AUTH_URL,
|
||||||
secret: process.env.AUTH_SECRET,
|
secret: process.env.AUTH_SECRET,
|
||||||
|
advanced: {
|
||||||
|
trustedProxyHeaders: true,
|
||||||
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
joins: true,
|
joins: true,
|
||||||
},
|
},
|
||||||
@@ -35,14 +48,25 @@ export const auth = betterAuth({
|
|||||||
verification: schema.verificationTokens,
|
verification: schema.verificationTokens,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
trustedOrigins: [
|
trustedOrigins: async (request) => {
|
||||||
...(process.env.BETTER_AUTH_URL ? [process.env.BETTER_AUTH_URL] : []),
|
const origins = [...staticTrustedOrigins];
|
||||||
...(process.env.NEXT_PUBLIC_APP_URL ? [process.env.NEXT_PUBLIC_APP_URL] : []),
|
|
||||||
"beenvoice://",
|
if (!request) return origins;
|
||||||
"exp://",
|
|
||||||
...(authentikOrigin ? [authentikOrigin] : []),
|
const origin = request.headers.get("origin");
|
||||||
...(process.env.AUTHENTIK_ORIGIN ? [process.env.AUTHENTIK_ORIGIN] : []),
|
if (origin) origins.push(origin);
|
||||||
],
|
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto") ?? "https";
|
||||||
|
if (forwardedHost) {
|
||||||
|
for (const host of forwardedHost.split(",")) {
|
||||||
|
const trimmed = host.trim();
|
||||||
|
if (trimmed) origins.push(`${forwardedProto}://${trimmed}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return origins;
|
||||||
|
},
|
||||||
...(authentikEnabled && {
|
...(authentikEnabled && {
|
||||||
accountLinking: {
|
accountLinking: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** Parse env vars that Docker Compose passes as strings ("true" / "false"). */
|
||||||
|
export function envBoolean(value: string | undefined): boolean {
|
||||||
|
if (!value) return false;
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
return normalized === "true" || normalized === "1";
|
||||||
|
}
|
||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
|
import { envBoolean } from "~/lib/env-boolean";
|
||||||
|
|
||||||
export function proxy(request: NextRequest) {
|
export function proxy(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
if (pathname === "/auth/register" && process.env.DISABLE_SIGNUPS === "true") {
|
if (pathname === "/auth/register" && envBoolean(process.env.DISABLE_SIGNUPS)) {
|
||||||
const signInUrl = new URL("/auth/signin", request.url);
|
const signInUrl = new URL("/auth/signin", request.url);
|
||||||
signInUrl.searchParams.set("signup", "disabled");
|
signInUrl.searchParams.set("signup", "disabled");
|
||||||
return NextResponse.redirect(signInUrl);
|
return NextResponse.redirect(signInUrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user