Add 'apps/web/' from commit '1e7174fa604b11e7c3983cd8ad01c596f6e77e96'
git-subtree-dir: apps/web git-subtree-mainline:068a51b46bgit-subtree-split:1e7174fa60
This commit is contained in:
Executable
+71
@@ -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
|
||||
Executable
+32
@@ -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();
|
||||
}
|
||||
@@ -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`,
|
||||
);
|
||||
Reference in New Issue
Block a user