mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
173 lines
6.1 KiB
JavaScript
173 lines
6.1 KiB
JavaScript
/**
|
|
* Staging test account (OPS125) — the ONE source of the synthetic `campaignUser`
|
|
* used for manual testing at `https://staging.jorgesolla1313.com.br` (the
|
|
* mandatory `work-issue` post-deploy verification that used it was removed in
|
|
* OPS128; the infrastructure stays as reusable tooling — runbook §Staging).
|
|
*
|
|
* Why it exists: the minimal seed credentials never exist on staging (the
|
|
* staging DB is a copy of production, not a `db:seed:minimal` target), so
|
|
* testing there needs a dedicated account. This module owns the account
|
|
* identity and the fail-closed guard that keeps its creation restricted to
|
|
* staging.
|
|
*
|
|
* The password is NEVER in code — it comes from `STAGING_TEST_ACCOUNT_PASSWORD`
|
|
* in the homeserver env file (`~/stack/teqo-staging.env`, chmod 600), so the
|
|
* re-run is idempotent AND deterministic.
|
|
*
|
|
* Guard discriminators (the host alone cannot decide: the homeserver socat
|
|
* proxy rewrites the DB host to 127.0.0.1 and the env file sets
|
|
* `NODE_ENV=production`, so `assertLocalDatabase`/`requiresWriteConfirm` alone
|
|
* cannot tell staging from production):
|
|
* 1. the database NAME must be exactly `teqo_staging` (the honest signal);
|
|
* 2. the protocol must be `postgresql:`;
|
|
* 3. the host must still be in the local allowlist (defense in depth);
|
|
* 4. `ALLOW_REMOTE_DB` is refused (this script never needs it);
|
|
* 5. `TEQO_ENV`, when set, must be `staging`;
|
|
* 6. the explicit intent flag is ALWAYS required — the bootstrap is a
|
|
* deliberate act, never a side effect.
|
|
*/
|
|
|
|
import {
|
|
databaseHostname,
|
|
databaseName,
|
|
isLocalDatabaseUrl,
|
|
isRemoteDbOverrideSet,
|
|
isTruthyEnv,
|
|
} from './cli.mjs'
|
|
|
|
export const STAGING_TEST_ACCOUNT = Object.freeze({
|
|
name: 'Agente de Teste (staging)',
|
|
email: 'agente-teste@teqo.invalid',
|
|
role: 'coordinator',
|
|
})
|
|
|
|
export const STAGING_DATABASE_NAME = 'teqo_staging'
|
|
export const STAGING_TEST_ACCOUNT_CONFIRM_FLAG = 'STAGING_TEST_ACCOUNT_CONFIRM'
|
|
export const STAGING_TEST_ACCOUNT_PASSWORD_ENV = 'STAGING_TEST_ACCOUNT_PASSWORD'
|
|
export const MIN_STAGING_TEST_ACCOUNT_PASSWORD_LENGTH = 16
|
|
|
|
/**
|
|
* Fail-closed password contract: the CLI demands it before instantiating the
|
|
* Payload runtime. Pure so the unit pin covers the exact threshold.
|
|
*
|
|
* @param {unknown} password
|
|
*/
|
|
export const assertStagingTestAccountPassword = (password) => {
|
|
if (typeof password !== 'string' || password.length === 0) {
|
|
throw new Error(
|
|
`${STAGING_TEST_ACCOUNT_PASSWORD_ENV} não está definida — a senha vive no env file do homeserver (chmod 600).`,
|
|
)
|
|
}
|
|
if (password.length < MIN_STAGING_TEST_ACCOUNT_PASSWORD_LENGTH) {
|
|
throw new Error(
|
|
`${STAGING_TEST_ACCOUNT_PASSWORD_ENV} tem menos de ${MIN_STAGING_TEST_ACCOUNT_PASSWORD_LENGTH} caracteres — use uma senha sintética longa.`,
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fail-closed guard for the staging-only write. Pure and injectable (no env
|
|
* mutation) so the acceptance matrix is unit-pinned.
|
|
*
|
|
* @param {{
|
|
* databaseUrl?: string | null,
|
|
* allowRemoteDb?: boolean,
|
|
* teqoEnv?: string | null,
|
|
* confirm?: boolean,
|
|
* }} [options]
|
|
*/
|
|
export const assertStagingTestAccountTarget = ({
|
|
databaseUrl = process.env.DATABASE_URL,
|
|
allowRemoteDb = isRemoteDbOverrideSet(),
|
|
teqoEnv = process.env.TEQO_ENV,
|
|
confirm = isTruthyEnv(process.env[STAGING_TEST_ACCOUNT_CONFIRM_FLAG]),
|
|
} = {}) => {
|
|
const targetDatabaseName = databaseName(databaseUrl)
|
|
if (targetDatabaseName !== STAGING_DATABASE_NAME) {
|
|
throw new Error(
|
|
`banco-alvo "${targetDatabaseName ?? '(inválido)'}" ≠ "${STAGING_DATABASE_NAME}" — ` +
|
|
'este script só escreve na conta de teste do staging.',
|
|
)
|
|
}
|
|
|
|
let protocol = null
|
|
try {
|
|
protocol = new URL(String(databaseUrl ?? '')).protocol
|
|
} catch {
|
|
protocol = null
|
|
}
|
|
if (protocol !== 'postgresql:') {
|
|
throw new Error(
|
|
`protocolo "${protocol ?? '(inválido)'}" ≠ "postgresql:" — a conta de teste só é criada num alvo PostgreSQL.`,
|
|
)
|
|
}
|
|
|
|
if (!isLocalDatabaseUrl(databaseUrl)) {
|
|
// Never echo the URL — it carries the DB password (reviewer finding).
|
|
throw new Error(
|
|
`host "${databaseHostname(databaseUrl) ?? '(inválido)'}" fora do allowlist local — a conta de teste nunca é criada em alvo remoto.`,
|
|
)
|
|
}
|
|
|
|
if (allowRemoteDb) {
|
|
throw new Error(
|
|
'ALLOW_REMOTE_DB está setado — remova; este script nunca precisa do override remoto.',
|
|
)
|
|
}
|
|
|
|
if (teqoEnv !== undefined && teqoEnv !== null && teqoEnv !== '' && teqoEnv !== 'staging') {
|
|
throw new Error(
|
|
`TEQO_ENV="${teqoEnv}" ≠ "staging" — este script só roda no bootstrap do staging.`,
|
|
)
|
|
}
|
|
|
|
if (!confirm) {
|
|
throw new Error(
|
|
`confirme a intenção com ${STAGING_TEST_ACCOUNT_CONFIRM_FLAG}=1 — a conta é criada só no bootstrap deliberado do staging.`,
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Idempotent upsert of the staging test account, keyed by its unique `email`.
|
|
* Creates on first run, then syncs `name`/`role`/`password` on every re-run
|
|
* (the C155-style deterministic bootstrap). Never passes `contact`, so the
|
|
* `CampaignUser` hook leaves the linked ficha untouched on updates; the first
|
|
* `create` lets the hook materialize the synthetic `Contact`.
|
|
*
|
|
* Runs through the Local API without `user` — the default Payload bypass, the
|
|
* same precedent as `scripts/seed-minimal.mjs`.
|
|
*
|
|
* @param {import('payload').Payload} payload
|
|
* @param {{ password: string }} options
|
|
* @returns {Promise<{ operation: 'created' | 'updated', id: number }>}
|
|
*/
|
|
export const upsertStagingTestAccount = async (payload, { password }) => {
|
|
const data = {
|
|
name: STAGING_TEST_ACCOUNT.name,
|
|
email: STAGING_TEST_ACCOUNT.email,
|
|
role: STAGING_TEST_ACCOUNT.role,
|
|
password,
|
|
}
|
|
|
|
const existing = await payload.find({
|
|
collection: 'campaignUser',
|
|
where: { email: { equals: STAGING_TEST_ACCOUNT.email } },
|
|
depth: 0,
|
|
pagination: false,
|
|
})
|
|
|
|
const doc = existing.docs[0]
|
|
if (doc) {
|
|
const updated = await payload.update({
|
|
collection: 'campaignUser',
|
|
id: doc.id,
|
|
data,
|
|
depth: 0,
|
|
})
|
|
return { operation: 'updated', id: updated.id }
|
|
}
|
|
|
|
const created = await payload.create({ collection: 'campaignUser', data, depth: 0 })
|
|
return { operation: 'created', id: created.id }
|
|
}
|