Default signups off, improve Docker deploy, fix onboarding step UI.
Show a disabled-registration state on the register page when DISABLE_SIGNUPS is true (default), document docker-deploy.sh with git-SHA image tags, and align onboarding progress circles and labels on a shared grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+10
-4
@@ -11,7 +11,7 @@
|
|||||||
# Quick start (Docker app + Postgres):
|
# Quick start (Docker app + Postgres):
|
||||||
# cp .env.example .env
|
# cp .env.example .env
|
||||||
# # edit AUTH_SECRET + public URLs below
|
# # edit AUTH_SECRET + public URLs below
|
||||||
# docker compose up -d --build
|
# ./scripts/docker-deploy.sh
|
||||||
#
|
#
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Build-time vs runtime (Docker)
|
# Build-time vs runtime (Docker)
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
# emails, and MCP links. In the browser, sign-in uses the current page origin
|
# emails, and MCP links. In the browser, sign-in uses the current page origin
|
||||||
# automatically so dev works when Next picks another port (e.g. 3002).
|
# automatically so dev works when Next picks another port (e.g. 3002).
|
||||||
#
|
#
|
||||||
# Updating production: git pull && docker compose up -d --build
|
# Updating production: git pull && ./scripts/docker-deploy.sh
|
||||||
|
# (or: docker compose up -d --build). Plain `docker compose up -d` does NOT rebuild.
|
||||||
# Migrations run on every app start (idempotent — only pending SQL is applied).
|
# Migrations run on every app start (idempotent — only pending SQL is applied).
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -75,6 +76,10 @@ POSTGRES_PORT=5432
|
|||||||
# Host port mapped to container :3000 (WEB_PORT, then PORT, then 3000).
|
# Host port mapped to container :3000 (WEB_PORT, then PORT, then 3000).
|
||||||
WEB_PORT=3000
|
WEB_PORT=3000
|
||||||
|
|
||||||
|
# App image tag for docker-compose.yml (optional). docker-deploy.sh sets
|
||||||
|
# beenvoice:<git-sha> automatically; default without it is beenvoice:local.
|
||||||
|
# BEENVOICE_IMAGE=beenvoice:local
|
||||||
|
|
||||||
# Postgres credentials for docker-compose.yml `db` service.
|
# Postgres credentials for docker-compose.yml `db` service.
|
||||||
# DATABASE_URL inside the app container is set by compose (host `db`, not localhost).
|
# DATABASE_URL inside the app container is set by compose (host `db`, not localhost).
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
@@ -112,8 +117,9 @@ NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js
|
|||||||
# Access control (optional)
|
# Access control (optional)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Block new email/password registrations. Use literal true or false.
|
# Block new email/password registrations (default: true / signups off).
|
||||||
# DISABLE_SIGNUPS=true
|
# Set DISABLE_SIGNUPS=false to allow new email/password signups.
|
||||||
|
# DISABLE_SIGNUPS=false
|
||||||
|
|
||||||
# Bearer token for POST /api/cron/generate-recurring (recurring invoice cron).
|
# Bearer token for POST /api/cron/generate-recurring (recurring invoice cron).
|
||||||
# CRON_SECRET=
|
# CRON_SECRET=
|
||||||
|
|||||||
@@ -132,10 +132,12 @@ docker compose build --no-cache app
|
|||||||
### 2. First start (or after code changes)
|
### 2. First start (or after code changes)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
./scripts/docker-deploy.sh
|
||||||
|
# or: bun run docker:deploy
|
||||||
|
# or: 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`.
|
`--build` is required after code changes. A plain `docker compose up -d` reuses the existing `beenvoice:local` image and **does not** pick up new code from `git pull`. The deploy script tags the image with the current git SHA (`beenvoice:<sha>`) so each deploy gets a distinct image.
|
||||||
|
|
||||||
App listens on `${WEB_PORT:-${PORT:-3000}}` on the host (container port is always 3000). Postgres stays on the internal compose network.
|
App listens on `${WEB_PORT:-${PORT:-3000}}` on the host (container port is always 3000). Postgres stays on the internal compose network.
|
||||||
|
|
||||||
@@ -143,16 +145,19 @@ App listens on `${WEB_PORT:-${PORT:-3000}}` on the host (container port is alway
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
docker compose up -d --build # rebuild image, restart app, run any new migrations
|
./scripts/docker-deploy.sh # recommended: rebuild + tag with git SHA + restart
|
||||||
|
# or: docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
| Command | New code? | Migrations run? |
|
| Command | New code? | Migrations run? |
|
||||||
|---------|-----------|-----------------|
|
|---------|-----------|-----------------|
|
||||||
| `git pull` only | No | No |
|
| `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` (no `--build`) | No — reuses `beenvoice:local` | Only if the app container restarts (same image) |
|
||||||
| `docker compose up -d --build` | Yes | Yes — on app container start |
|
| `./scripts/docker-deploy.sh` or `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) |
|
| `docker compose restart app` | No | Yes — migrate runs again (no-op if up to date) |
|
||||||
|
|
||||||
|
Prune old app images occasionally: `docker image prune -f` (or remove specific `beenvoice:*` tags).
|
||||||
|
|
||||||
To verify migration files match the journal before deploy: `bun run db:verify-journal`.
|
To verify migration files match the journal before deploy: `bun run db:verify-journal`.
|
||||||
|
|
||||||
### 4. Sign-ups
|
### 4. Sign-ups
|
||||||
@@ -213,12 +218,13 @@ bun run lint:fix
|
|||||||
bun run format:write
|
bun run format:write
|
||||||
bun run typecheck
|
bun run typecheck
|
||||||
|
|
||||||
# Docker helpers (Postgres only — uses Colima on macOS)
|
# Docker helpers
|
||||||
bun run docker:up # colima start + docker-compose.dev.yml up -d
|
bun run docker:up # dev Postgres only (Colima + docker-compose.dev.yml)
|
||||||
bun run docker:down # stop dev Postgres + colima
|
bun run docker:down # stop dev Postgres + colima
|
||||||
|
bun run docker:deploy # production: rebuild app image + docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Full-stack deploy uses `docker compose up` (see [Docker deployment](#docker-deployment-app--database)), not `bun run docker:up`.
|
Full-stack deploy uses `bun run docker:deploy` or `./scripts/docker-deploy.sh` (see [Docker deployment](#docker-deployment-app--database)), not `bun run docker:up`.
|
||||||
|
|
||||||
## API surface
|
## API surface
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -1,3 +1,9 @@
|
|||||||
|
# Production stack (app + Postgres). Local dev Postgres-only: docker-compose.dev.yml
|
||||||
|
#
|
||||||
|
# After git pull, rebuild before starting — a plain `docker compose up -d` reuses
|
||||||
|
# the existing local image and will NOT include new code. Use:
|
||||||
|
# ./scripts/docker-deploy.sh
|
||||||
|
# docker compose up -d --build
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
@@ -5,7 +11,9 @@ services:
|
|||||||
args:
|
args:
|
||||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
||||||
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
|
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
|
||||||
image: beenvoice:local
|
# Fixed default tag (beenvoice:local) is reused until you --build. docker-deploy.sh
|
||||||
|
# sets BEENVOICE_IMAGE=beenvoice:<git-sha> so each deploy gets a fresh tag.
|
||||||
|
image: ${BEENVOICE_IMAGE:-beenvoice:local}
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in .env}
|
AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in .env}
|
||||||
@@ -18,7 +26,7 @@ services:
|
|||||||
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-}
|
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-}
|
||||||
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.umami.is/script.js}
|
NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.umami.is/script.js}
|
||||||
NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED:-false}
|
NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED:-false}
|
||||||
DISABLE_SIGNUPS: ${DISABLE_SIGNUPS:-false}
|
DISABLE_SIGNUPS: ${DISABLE_SIGNUPS:-true}
|
||||||
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER:-}
|
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER:-}
|
||||||
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-}
|
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-}
|
||||||
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-}
|
AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-}
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ App image built from `Dockerfile`. Container `CMD`: `bun migrate.ts && bun run s
|
|||||||
|
|
||||||
Set `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` to the public hostname before deploy. Rebuild the image when changing `NEXT_PUBLIC_*` build-time vars.
|
Set `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` to the public hostname before deploy. Rebuild the image when changing `NEXT_PUBLIC_*` build-time vars.
|
||||||
|
|
||||||
|
**Deploy / update:** `git pull && ./scripts/docker-deploy.sh` (or `docker compose up -d --build`). Plain `docker compose up -d` reuses the local `beenvoice:local` image and does not include pulled code. The deploy script tags images as `beenvoice:<git-sha>`.
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"docker:up": "colima start && docker compose -f docker-compose.dev.yml up -d",
|
"docker:up": "colima start && docker compose -f docker-compose.dev.yml up -d",
|
||||||
"docker:down": "docker compose -f docker-compose.dev.yml down && colima stop",
|
"docker:down": "docker compose -f docker-compose.dev.yml down && colima stop",
|
||||||
"docker:dev:down": "docker compose -f docker-compose.dev.yml down && colima stop",
|
"docker:dev:down": "docker compose -f docker-compose.dev.yml down && colima stop",
|
||||||
|
"docker:deploy": "./scripts/docker-deploy.sh",
|
||||||
"deploy": "drizzle-kit push && next build",
|
"deploy": "drizzle-kit push && next build",
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||||
|
|||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Production deploy helper for docker-compose.yml (not docker-compose.dev.yml).
|
||||||
|
# Rebuilds the app image from the current working tree, then starts/restarts services.
|
||||||
|
#
|
||||||
|
# Plain `docker compose up -d` reuses the local image tag and does NOT pick up
|
||||||
|
# changes from `git pull`. Always pass --build or use this script after pulling.
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
if [[ -f .env ]]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${BEENVOICE_IMAGE:-}" ]] && command -v git >/dev/null 2>&1; then
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
BEENVOICE_IMAGE="beenvoice:$(git rev-parse --short HEAD)"
|
||||||
|
export BEENVOICE_IMAGE
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
BEENVOICE_IMAGE="${BEENVOICE_IMAGE:-beenvoice:local}"
|
||||||
|
export BEENVOICE_IMAGE
|
||||||
|
|
||||||
|
echo "Deploying ${BEENVOICE_IMAGE} (docker compose up -d --build)..."
|
||||||
|
exec docker compose up -d --build "$@"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { env } from "~/env";
|
||||||
import { RegisterForm } from "./register-form";
|
import { RegisterForm } from "./register-form";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
return <RegisterForm />;
|
return <RegisterForm signupsDisabled={env.DISABLE_SIGNUPS === true} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ArrowRight, Lock, Mail, User } from "lucide-react";
|
import { ArrowRight, Lock, Mail, User, UserX } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
AuthCard,
|
AuthCard,
|
||||||
AuthCardHeader,
|
AuthCardHeader,
|
||||||
@@ -22,7 +22,11 @@ function formatAuthError(message: string | undefined, fallback: string): string
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegisterForm() {
|
interface RegisterFormProps {
|
||||||
|
signupsDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterForm({ signupsDisabled = false }: RegisterFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
@@ -92,6 +96,35 @@ export function RegisterForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signupsDisabled) {
|
||||||
|
return (
|
||||||
|
<AuthPageShell>
|
||||||
|
<AuthCard>
|
||||||
|
<AuthCardHeader
|
||||||
|
title="Registration closed"
|
||||||
|
description="New account sign-ups are not available right now"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="bg-muted/50 text-muted-foreground mb-6 flex gap-3 rounded-xl border px-4 py-3 text-sm">
|
||||||
|
<UserX className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0" />
|
||||||
|
<p>
|
||||||
|
This workspace is not accepting new registrations. If you already
|
||||||
|
have an account, sign in below. Contact your administrator if you
|
||||||
|
need access.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button asChild className="h-11 w-full">
|
||||||
|
<Link href="/auth/signin">
|
||||||
|
Sign in to your account
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</AuthCard>
|
||||||
|
</AuthPageShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthPageShell>
|
<AuthPageShell>
|
||||||
<AuthCard>
|
<AuthCard>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
||||||
const signupDisabled = searchParams.get("signup") === "disabled";
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -74,7 +73,7 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
|
|||||||
description="Sign in to your workspace"
|
description="Sign in to your workspace"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{signupDisabled && (
|
{!allowRegistration && (
|
||||||
<p className="bg-muted/50 text-muted-foreground mb-5 rounded-xl border px-3 py-2.5 text-sm">
|
<p className="bg-muted/50 text-muted-foreground mb-5 rounded-xl border px-3 py-2.5 text-sm">
|
||||||
New account registration is currently disabled.
|
New account registration is currently disabled.
|
||||||
</p>
|
</p>
|
||||||
@@ -155,7 +154,7 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{allowRegistration && !signupDisabled && (
|
{allowRegistration && (
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -14,72 +14,86 @@ function stepIndex(step: OnboardingStepId) {
|
|||||||
return ONBOARDING_STEPS.findIndex((item) => item.id === step);
|
return ONBOARDING_STEPS.findIndex((item) => item.id === step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TRACK_GRID_COLUMNS = ONBOARDING_STEPS.map((_, index) =>
|
||||||
|
index < ONBOARDING_STEPS.length - 1 ? "auto 1fr" : "auto",
|
||||||
|
).join(" ");
|
||||||
|
|
||||||
export function OnboardingStepIndicator({ step }: { step: OnboardingStepId }) {
|
export function OnboardingStepIndicator({ step }: { step: OnboardingStepId }) {
|
||||||
const currentIndex = stepIndex(step);
|
const currentIndex = stepIndex(step);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav aria-label="Setup progress" className="mb-8">
|
<nav aria-label="Setup progress" className="mb-8">
|
||||||
<ol className="mx-auto flex w-full max-w-md">
|
<ol className="sr-only">
|
||||||
|
{ONBOARDING_STEPS.map((item, index) => {
|
||||||
|
const isCurrent = currentIndex === index;
|
||||||
|
return (
|
||||||
|
<li key={item.id} aria-current={isCurrent ? "step" : undefined}>
|
||||||
|
{item.label}
|
||||||
|
{isCurrent ? " (current)" : ""}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{/* Row 1: circles + connectors. Row 2: labels (same columns as circles). */}
|
||||||
|
<div
|
||||||
|
className="mx-auto grid w-full max-w-md items-center gap-y-2"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: TRACK_GRID_COLUMNS,
|
||||||
|
gridTemplateRows: "auto auto",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
{ONBOARDING_STEPS.map((item, index) => {
|
{ONBOARDING_STEPS.map((item, index) => {
|
||||||
const isComplete = currentIndex > index;
|
const isComplete = currentIndex > index;
|
||||||
const isCurrent = currentIndex === index;
|
const isCurrent = currentIndex === index;
|
||||||
const isUpcoming = currentIndex < index;
|
const isUpcoming = currentIndex < index;
|
||||||
const connectorComplete = currentIndex > index;
|
const connectorComplete = currentIndex > index;
|
||||||
|
const circleCol = index * 2 + 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={item.id} className="flex flex-1 flex-col items-center">
|
<div key={item.id} className="contents">
|
||||||
<div className="flex w-full items-center">
|
{index > 0 && (
|
||||||
{index > 0 && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-0.5 flex-1 rounded-full transition-colors",
|
|
||||||
connectorComplete || isCurrent
|
|
||||||
? "bg-primary"
|
|
||||||
: "bg-border/80",
|
|
||||||
)}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 text-sm font-medium transition-colors",
|
"h-0.5 self-center rounded-full transition-colors",
|
||||||
isComplete &&
|
connectorComplete ? "bg-primary" : "bg-border/80",
|
||||||
"border-primary bg-primary text-primary-foreground",
|
|
||||||
isCurrent &&
|
|
||||||
"border-primary bg-primary/10 text-primary ring-primary/20 ring-4",
|
|
||||||
isUpcoming &&
|
|
||||||
"border-border/80 bg-background/60 text-muted-foreground",
|
|
||||||
)}
|
)}
|
||||||
aria-current={isCurrent ? "step" : undefined}
|
style={{ gridColumn: index * 2, gridRow: 1 }}
|
||||||
>
|
/>
|
||||||
{isComplete ? (
|
)}
|
||||||
<Check className="h-4 w-4" aria-hidden />
|
<div
|
||||||
) : (
|
className={cn(
|
||||||
<span>{index + 1}</span>
|
"flex h-9 w-9 items-center justify-center justify-self-center rounded-full border-2 text-sm font-medium transition-colors",
|
||||||
)}
|
isComplete &&
|
||||||
</div>
|
"border-primary bg-primary text-primary-foreground",
|
||||||
{index < ONBOARDING_STEPS.length - 1 && (
|
isCurrent &&
|
||||||
<div
|
"border-primary bg-primary/10 text-primary ring-primary/20 ring-4",
|
||||||
className={cn(
|
isUpcoming &&
|
||||||
"h-0.5 flex-1 rounded-full transition-colors",
|
"border-border/80 bg-background/60 text-muted-foreground",
|
||||||
connectorComplete ? "bg-primary" : "bg-border/80",
|
)}
|
||||||
)}
|
style={{ gridColumn: circleCol, gridRow: 1 }}
|
||||||
aria-hidden
|
>
|
||||||
/>
|
{isComplete ? (
|
||||||
|
<Check className="h-4 w-4" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<span>{index + 1}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 hidden text-xs font-medium sm:block",
|
"hidden min-w-0 justify-self-center text-center text-xs leading-tight font-medium sm:block",
|
||||||
isCurrent ? "text-foreground" : "text-muted-foreground",
|
isCurrent ? "text-foreground" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
|
style={{ gridColumn: circleCol, gridRow: 2 }}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ol>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-4 text-center text-sm sm:hidden">
|
<p className="text-muted-foreground mt-4 text-center text-sm sm:hidden">
|
||||||
Step {Math.min(currentIndex + 1, ONBOARDING_STEPS.length)} of{" "}
|
Step {Math.min(currentIndex + 1, ONBOARDING_STEPS.length)} of{" "}
|
||||||
{ONBOARDING_STEPS.length}
|
{ONBOARDING_STEPS.length}
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ export const env = createEnv({
|
|||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
DB_DISABLE_SSL: optionalEnvBoolean(),
|
DB_DISABLE_SSL: optionalEnvBoolean(),
|
||||||
DISABLE_SIGNUPS: optionalEnvBoolean(),
|
DISABLE_SIGNUPS: optionalEnvBoolean().default(true),
|
||||||
CRON_SECRET: z.string().optional(),
|
CRON_SECRET: z.string().optional(),
|
||||||
// SSO / Authentik (optional)
|
// SSO / Authentik (optional)
|
||||||
AUTHENTIK_ISSUER: z.string().url().optional(),
|
AUTHENTIK_ISSUER: z.string().url().optional(),
|
||||||
|
|||||||
+2
-2
@@ -3,7 +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 { env } from "~/env";
|
||||||
import { isDemoUser, promoteFirstRealUserIfNeeded } from "~/lib/first-admin";
|
import { isDemoUser, promoteFirstRealUserIfNeeded } from "~/lib/first-admin";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import * as schema from "~/server/db/schema";
|
import * as schema from "~/server/db/schema";
|
||||||
@@ -13,7 +13,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 = envBoolean(process.env.DISABLE_SIGNUPS);
|
const signupsDisabled = 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.
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
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";
|
|
||||||
import { isPublicRoute } from "~/lib/public-routes";
|
import { isPublicRoute } from "~/lib/public-routes";
|
||||||
|
|
||||||
export function proxy(request: NextRequest) {
|
export function proxy(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
if (pathname === "/auth/register" && envBoolean(process.env.DISABLE_SIGNUPS)) {
|
|
||||||
const signInUrl = new URL("/auth/signin", request.url);
|
|
||||||
signInUrl.searchParams.set("signup", "disabled");
|
|
||||||
return NextResponse.redirect(signInUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define API routes that should be handled separately
|
// Define API routes that should be handled separately
|
||||||
const apiRoutes = ["/api/auth", "/api/trpc", "/api/mcp", "/api/i"];
|
const apiRoutes = ["/api/auth", "/api/trpc", "/api/mcp", "/api/i"];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user