Add 'apps/web/' from commit '1e7174fa604b11e7c3983cd8ad01c596f6e77e96'

git-subtree-dir: apps/web
git-subtree-mainline: 068a51b46b
git-subtree-split: 1e7174fa60
This commit is contained in:
2026-08-16 21:42:59 -04:00
350 changed files with 62192 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
# Function to read a variable from a specific env file
read_env_var() {
local file="$1"
local var="$2"
if [ -f "$file" ]; then
grep "^$var=" "$file" | cut -d '=' -f2- | tr -d '"' | tr -d "'"
fi
}
# 1. Get Production URL
# Priority: Argument > .env.production > .env
PROD_DB_URL="$1"
if [ -z "$PROD_DB_URL" ]; then
echo "Checking .env.production for DATABASE_URL..."
PROD_DB_URL=$(read_env_var ".env.production" "DATABASE_URL")
fi
if [ -z "$PROD_DB_URL" ]; then
echo "Checking .env for PROD_DATABASE_URL..."
PROD_DB_URL=$(read_env_var ".env" "PROD_DATABASE_URL")
fi
if [ -z "$PROD_DB_URL" ]; then
echo "Error: Could not find production database URL."
echo "Please provide it as an argument, or set DATABASE_URL in .env.production, or PROD_DATABASE_URL in .env"
echo "Usage: $0 <PROD_DATABASE_URL>"
exit 1
fi
# 2. Get Target URL
# Priority: .env.local > .env
TARGET_DB_URL=$(read_env_var ".env.local" "DATABASE_URL")
if [ -z "$TARGET_DB_URL" ]; then TARGET_DB_URL=$(read_env_var ".env" "DATABASE_URL"); fi
if [ -z "$TARGET_DB_URL" ]; then
echo "Error: Could not find target DATABASE_URL in .env.local or .env"
exit 1
fi
echo "Configuration:"
echo " Source: $PROD_DB_URL"
echo " Target: $TARGET_DB_URL"
echo
echo "⚠️ WARNING: This will OVERWRITE the target database at the above URL."
echo "This is a one-time migration script."
read -p "Are you sure you want to continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
echo "Cloning database..."
# Use local pg_dump and psql directly
# This assumes pg_dump and psql are installed on the host machine
pg_dump "$PROD_DB_URL" \
--clean --if-exists \
--no-owner --no-privileges \
--format=plain \
| psql "$TARGET_DB_URL"
if [ $? -eq 0 ]; then
echo "✅ Database cloned successfully!"
else
echo "❌ Database clone failed."
exit 1
fi
+32
View File
@@ -0,0 +1,32 @@
#!/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
# (app, db, garage). Receipt storage uses in-stack Garage unless S3_* are
# overridden in .env. Garage S3 API: localhost:${GARAGE_API_PORT:-3900}.
#
# 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 "$@"
@@ -0,0 +1,61 @@
import "dotenv/config";
import bcrypt from "bcryptjs";
import { Pool } from "pg";
const DEMO_USER_ID = "a0000000-0000-4000-8000-000000000001";
const password = process.env.DEMO_ACCOUNT_PASSWORD?.trim();
const databaseUrl = process.env.DATABASE_URL?.trim();
if (!databaseUrl) {
throw new Error("DATABASE_URL is required");
}
if (!password || password.length < 12) {
throw new Error("DEMO_ACCOUNT_PASSWORD must be at least 12 characters");
}
const pool = new Pool({ connectionString: databaseUrl, ssl: false });
try {
const passwordHash = await bcrypt.hash(password, 12);
const client = await pool.connect();
try {
await client.query("BEGIN");
const userResult = await client.query(
`UPDATE "beenvoice_user"
SET "password" = $1, "updatedAt" = NOW()
WHERE "id" = $2`,
[passwordHash, DEMO_USER_ID],
);
const accountResult = await client.query(
`UPDATE "beenvoice_account"
SET "password" = $1, "updatedAt" = NOW()
WHERE "userId" = $2 AND "providerId" = 'credential'`,
[passwordHash, DEMO_USER_ID],
);
if (userResult.rowCount !== 1 || accountResult.rowCount !== 1) {
throw new Error(
"Demo account is missing. Apply database migrations before provisioning it.",
);
}
await client.query(`DELETE FROM "beenvoice_session" WHERE "userId" = $1`, [
DEMO_USER_ID,
]);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
console.log(
"Demo review account provisioned; previous sessions were invalidated.",
);
} finally {
await pool.end();
}
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." &>/dev/null && pwd)"
cd "${PROJECT_ROOT}"
echo "[setup-env] Project root: ${PROJECT_ROOT}"
ENV_EXAMPLE_FILE="${PROJECT_ROOT}/env.example"
ENV_FILE="${PROJECT_ROOT}/.env"
FORCE=${FORCE:-false}
if [[ ! -f "${ENV_EXAMPLE_FILE}" ]]; then
echo "[setup-env] ERROR: env.example not found at ${ENV_EXAMPLE_FILE}" >&2
exit 1
fi
if [[ -f "${ENV_FILE}" && "${FORCE}" != "true" ]]; then
echo "[setup-env] .env already exists. Set FORCE=true to overwrite. Skipping."
exit 0
fi
echo "[setup-env] Generating secrets for .env"
GEN_AUTH_SECRET=$(openssl rand -hex 32 2>/dev/null || cat /proc/sys/kernel/random/uuid)
GEN_DB_PASSWORD=$(openssl rand -hex 16 2>/dev/null || cat /proc/sys/kernel/random/uuid)
TMP_FILE=$(mktemp)
sed \
-e "s/^AUTH_SECRET=__GENERATE__/AUTH_SECRET=${GEN_AUTH_SECRET}/" \
-e "s/^POSTGRES_PASSWORD=__GENERATE__/POSTGRES_PASSWORD=${GEN_DB_PASSWORD}/" \
"${ENV_EXAMPLE_FILE}" > "${TMP_FILE}"
mv "${TMP_FILE}" "${ENV_FILE}"
echo "[setup-env] Wrote ${ENV_FILE} with generated AUTH_SECRET and POSTGRES_PASSWORD"
echo "[setup-env] You can edit ${ENV_FILE} to adjust PORT, RESEND_* and other values."
exit 0
#!/usr/bin/env bash
set -euo pipefail
# Resolve project root (directory containing this script's parent)
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." &>/dev/null && pwd)"
cd "${PROJECT_ROOT}"
echo "[setup-env] Project root: ${PROJECT_ROOT}"
ENV_EXAMPLE_FILE="${PROJECT_ROOT}/env.example"
ENV_FILE="${PROJECT_ROOT}/.env"
FORCE=${FORCE:-false}
if [[ ! -f "${ENV_EXAMPLE_FILE}" ]]; then
echo "[setup-env] ERROR: env.example not found at ${ENV_EXAMPLE_FILE}" >&2
exit 1
fi
if [[ -f "${ENV_FILE}" && "${FORCE}" != "true" ]]; then
echo "[setup-env] .env already exists. Set FORCE=true to overwrite. Skipping."
exit 0
fi
echo "[setup-env] Generating secrets for .env"
# Generate secrets
GEN_AUTH_SECRET=$(openssl rand -hex 32 2>/dev/null || cat /proc/sys/kernel/random/uuid)
GEN_DB_PASSWORD=$(openssl rand -hex 16 2>/dev/null || cat /proc/sys/kernel/random/uuid)
TMP_FILE=$(mktemp)
# Perform replacements
sed \
-e "s/^AUTH_SECRET=__GENERATE__/AUTH_SECRET=${GEN_AUTH_SECRET}/" \
-e "s/^POSTGRES_PASSWORD=__GENERATE__/POSTGRES_PASSWORD=${GEN_DB_PASSWORD}/" \
"${ENV_EXAMPLE_FILE}" > "${TMP_FILE}"
mv "${TMP_FILE}" "${ENV_FILE}"
echo "[setup-env] Wrote ${ENV_FILE} with generated AUTH_SECRET and POSTGRES_PASSWORD"
echo "[setup-env] You can edit ${ENV_FILE} to adjust PORT, RESEND_* and other values."
exit 0
@@ -0,0 +1,85 @@
/**
* Ensures every drizzle/*.sql migration file has a matching entry in meta/_journal.json.
* Run: bun scripts/verify-drizzle-journal.ts
*/
import { readdirSync, readFileSync } from "fs";
import path from "path";
const drizzleDir = path.resolve(import.meta.dir, "../drizzle");
const journalPath = path.join(drizzleDir, "meta/_journal.json");
const journal = JSON.parse(readFileSync(journalPath, "utf8")) as {
entries: Array<{ idx: number; tag: string; when: number }>;
};
const sqlTags = readdirSync(drizzleDir)
.filter((name) => /^\d+_.+\.sql$/.test(name))
.map((name) => name.replace(/\.sql$/, ""))
.sort();
const journalTags = journal.entries.map((entry) => entry.tag).sort();
const missingFromJournal = sqlTags.filter((tag) => !journalTags.includes(tag));
const missingSql = journalTags.filter((tag) => !sqlTags.includes(tag));
const idxSequence = journal.entries.map((entry) => entry.idx);
const expectedIdx = journal.entries.map((_, i) => i);
const badIdx = idxSequence.some((idx, i) => idx !== expectedIdx[i]);
// drizzle's migrator gates on the single highest `when` already recorded in
// the target DB — it doesn't check hashes per-migration. If `when` values
// ever go non-increasing (e.g. a migration got deleted/renumbered after
// being applied to some environment), later entries can silently be skipped
// forever even though drizzle reports success. Keep this strictly increasing.
//
// 0008 and 0011 are known pre-existing exceptions from before this check
// existed (see git log on those files) — both already guard every statement
// with IF NOT EXISTS specifically because of this, so a skip is harmless.
// Don't add new exceptions here; fix the timestamp instead.
const KNOWN_NON_MONOTONIC_TAGS = new Set([
"0008_payments_recurring_public_links",
"0011_time_entry_invoice_id",
]);
const nonMonotonic = journal.entries.some(
(entry, i) =>
i > 0 &&
entry.when <= journal.entries[i - 1]!.when &&
!KNOWN_NON_MONOTONIC_TAGS.has(entry.tag),
);
let failed = false;
if (missingFromJournal.length > 0) {
console.error("[verify-drizzle-journal] SQL files missing from journal:");
for (const tag of missingFromJournal) console.error(` - ${tag}`);
failed = true;
}
if (missingSql.length > 0) {
console.error("[verify-drizzle-journal] Journal entries without SQL files:");
for (const tag of missingSql) console.error(` - ${tag}`);
failed = true;
}
if (badIdx) {
console.error("[verify-drizzle-journal] Journal idx values are not sequential from 0");
failed = true;
}
if (nonMonotonic) {
console.error(
"[verify-drizzle-journal] Journal `when` timestamps are not strictly increasing. " +
"drizzle-orm's migrator only compares against the single highest `when` already " +
"applied in the target DB, so a lower or equal value here can cause migrations to " +
"be silently skipped on databases that already ran a later timestamp.",
);
failed = true;
}
if (failed) {
process.exit(1);
}
console.log(
`[verify-drizzle-journal] OK — ${sqlTags.length} migrations match journal entries`,
);