lux-bank runs bankd v1.0.18, and Lux ID is the one sign-in the suite proves
bankd v1.0.18 takes only a lux.id access token issued to lux-financial for
the lux org, maps only a member of the admin org to _superusers, and listens
on HTTP alone. Its pod reads none of the bank-secrets keys, so the five env
references to them are gone.
bank-docs states the amd64 node selector its pod already runs with.
e2e/40-lux-financial proves the one way in: neither console serves a password
input or ships a credential in its JS; every sign-in screen hands off to lux.id
with client lux-financial, PKCE S256 and the console's /auth/callback; bankd
answers 404 on every password and Base sign-in route, the code exchange takes
only lux-financial by code or refresh, and a token lux.id did not issue to
lux-financial reads as anonymous. The password-form sign-in and the seeded
ledger totals it asserted are deleted: neither exists.
k8s/lux-k8s/{bank-api,bank-dash,bank-docs,bank-sandbox,bankd,financial} and
scripts/seed-bank-admin.ts are deleted. deploy/lux-bank declares the bank,
lux.financial is a static site, and no host those files name resolves.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -8,6 +8,9 @@ image:
|
||||
repository: ghcr.io/luxbank/financial-docs
|
||||
tag: 1.1.2
|
||||
pullPolicy: Always
|
||||
# The image is built for linux/amd64 only.
|
||||
nodeSelector:
|
||||
kubernetes.io/arch: amd64
|
||||
partOf: lux-financial
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -6,8 +6,8 @@ selectorLabels:
|
||||
app: bankd
|
||||
image:
|
||||
repository: ghcr.io/luxfi/bank
|
||||
tag: v1.0.16
|
||||
digest: sha256:81fad895e3fc60b044cc2ab34ece5352f334f7bef4af101a3a1688eca1e42521
|
||||
tag: v1.0.18
|
||||
digest: sha256:069f200ee39f8a7b167bd6cb30b55b9fafbf7ed208e64d7b0000390a0ec47092
|
||||
pullPolicy: IfNotPresent
|
||||
# One manifest under that tag, linux/amd64 — stated beside the image because it
|
||||
# describes the bytes. A cluster that is all amd64 is unaffected; a mixed one
|
||||
@@ -23,33 +23,6 @@ args:
|
||||
- --dir
|
||||
- /data
|
||||
env:
|
||||
- name: BANKD_DIR
|
||||
value: /data
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: jwt-secret
|
||||
name: bank-secrets
|
||||
- name: ENCRYPT_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: encrypt-key
|
||||
name: bank-secrets
|
||||
- name: SENDGRID_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: sendgrid-key
|
||||
name: bank-secrets
|
||||
- name: SENTRY_DSN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: sentry-dsn
|
||||
name: bank-secrets
|
||||
- name: GOOGLE_RECAPTCHA_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: recaptcha-secret-key
|
||||
name: bank-secrets
|
||||
- name: BANK_SANDBOX
|
||||
value: 'true'
|
||||
resources:
|
||||
@@ -70,8 +43,8 @@ ports:
|
||||
# could not see — it reads the Deployment, and a claim a Deployment merely MOUNTS
|
||||
# is not part of it — so the values described a pod that names a volume nobody
|
||||
# declares, and the scheduler refuses such a pod outright: no node is available,
|
||||
# forever, with no image ever pulled. 20Gi RWO is what k8s/lux-k8s/bankd carried
|
||||
# before that directory was superseded.
|
||||
# forever, with no image ever pulled. 20Gi RWO is the size bankd's ledger has
|
||||
# always been given.
|
||||
#
|
||||
# No storageClass on purpose. Naming one binds this file to one cluster's
|
||||
# provisioner; omitting it takes the cluster's default, which is the block class
|
||||
|
||||
+98
-175
@@ -2,18 +2,18 @@ import { test, expect, Page, APIRequestContext } from '@playwright/test'
|
||||
import { httpStatus, snap } from './helpers'
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Lux Financial — authenticated bank flow proof suite
|
||||
//
|
||||
// Unlike 18/19-broker (unauth UI + responsive audits), this suite logs in with
|
||||
// the sandbox superuser and ASSERTS real post-login state: the customer's total
|
||||
// balance, the balances list, and the admin control-plane nav + pages. It also
|
||||
// proves the acm white-label (?brand=acm → light theme + "acm" wordmark).
|
||||
// Lux Financial — one way in, and the surfaces around it
|
||||
//
|
||||
// Customer : https://app.lux.financial (Vite SPA → bankd @ api.lux.financial)
|
||||
// Admin : https://admin.lux.financial (Vite SPA → bankd, testnet custody)
|
||||
// Admin : https://admin.lux.financial (Vite SPA → bankd)
|
||||
// Docs : https://docs.lux.financial (Docusaurus)
|
||||
// Backend : api.lux.financial (Hanzo Base app "bankd", /v1/*)
|
||||
//
|
||||
// Both consoles sign in only with Lux ID: the public client lux-financial, PKCE
|
||||
// S256, back to https://<console>/auth/callback. No console serves a password
|
||||
// input or ships a credential in its JS, and bankd serves no password route and
|
||||
// takes no token but a lux.id access token issued to lux-financial.
|
||||
//
|
||||
// Resilient by design: unreachable surfaces (HTTP 0 / ≥500) skip-with-note
|
||||
// rather than hard-failing the run.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -21,23 +21,9 @@ import { httpStatus, snap } from './helpers'
|
||||
const APP = 'https://app.lux.financial'
|
||||
const ADMIN = 'https://admin.lux.financial'
|
||||
const DOCS = 'https://docs.lux.financial'
|
||||
// Credentials come from the environment, never from this file — a password in
|
||||
// a tracked file is a plaintext password however "sandbox" the account is.
|
||||
// This suite's contract is skip-with-note for anything it cannot reach, so an
|
||||
// unset password skips the authenticated tests rather than reddening the run;
|
||||
// the dedicated sign-in proof (50-login.spec.ts) is the one that must FAIL.
|
||||
const CREDS = {
|
||||
email: process.env.LUX_FINANCIAL_USER || '[email protected]',
|
||||
password: process.env.LUX_FINANCIAL_PASSWORD || '',
|
||||
}
|
||||
|
||||
function haveCreds(): boolean {
|
||||
if (!CREDS.password) {
|
||||
console.log(' WARN: LUX_FINANCIAL_PASSWORD unset — skipping the authenticated tests')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const API = 'https://api.lux.financial'
|
||||
const IDP_HOST = 'lux.id'
|
||||
const CLIENT = 'lux-financial'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reachability cache
|
||||
@@ -74,79 +60,6 @@ async function bodyLightness(page: Page): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
async function loginCustomer(page: Page) {
|
||||
test.skip(!haveCreds(), 'LUX_FINANCIAL_PASSWORD is not set')
|
||||
await page.goto(`${APP}/login`, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
await page.locator('input[type=email]').first().fill(CREDS.email)
|
||||
await page.locator('input[type=password]').first().fill(CREDS.password)
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click()
|
||||
await page.waitForURL(/\/app/, { timeout: 30_000 })
|
||||
await expect(page.locator('body')).toContainText('21,818.45', { timeout: 20_000 })
|
||||
}
|
||||
|
||||
async function loginAdmin(page: Page) {
|
||||
test.skip(!haveCreds(), 'LUX_FINANCIAL_PASSWORD is not set')
|
||||
await page.goto(`${ADMIN}/login`, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
await page.locator('input[type=email]').first().fill(CREDS.email)
|
||||
await page.locator('input[type=password]').first().fill(CREDS.password)
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click()
|
||||
// Admin lands on "/" — wait for the Overview shell (nav) to hydrate.
|
||||
await expect(page.getByRole('link', { name: 'Overview', exact: true })).toBeVisible({ timeout: 30_000 })
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Bank Customer — app.lux.financial (authenticated)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
test.describe('Lux Financial — Bank Customer', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let page: Page
|
||||
let down = false
|
||||
|
||||
test.beforeAll(async ({ browser, request }) => {
|
||||
down = !(await reachable(request, 'app', APP))
|
||||
if (down) return
|
||||
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 } })
|
||||
page = await ctx.newPage()
|
||||
await loginCustomer(page)
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page?.context().close().catch(() => {})
|
||||
})
|
||||
|
||||
test('logs in → dashboard shows total balance $21,818.45 + Welcome back', async () => {
|
||||
test.skip(down, 'app.lux.financial unreachable')
|
||||
expect(page.url()).toContain('/app')
|
||||
await expect(page.locator('body')).toContainText('Welcome back')
|
||||
await expect(page.locator('body')).toContainText('21,818.45')
|
||||
await expect(page.locator('body')).toContainText(/Across 5 balances/i)
|
||||
await snap(page, 'luxfin-customer-dashboard')
|
||||
})
|
||||
|
||||
test('dashboard lists the 5 currency balances (USD/EUR/GBP/LUX/DAI)', async () => {
|
||||
test.skip(down, 'app.lux.financial unreachable')
|
||||
const body = page.locator('body')
|
||||
await expect(body).toContainText(/Balances/i)
|
||||
for (const code of ['USD', 'EUR', 'GBP', 'LUX', 'DAI']) {
|
||||
await expect(body).toContainText(code)
|
||||
}
|
||||
// Concrete per-currency figures rendered from bankd, not placeholders.
|
||||
await expect(body).toContainText('$12,500.00') // USD
|
||||
await expect(body).toContainText('€3,200.00') // EUR
|
||||
await expect(body).toContainText('250 LUX') // LUX
|
||||
})
|
||||
|
||||
test('dashboard renders cards + recent activity (real app, not marketing shell)', async () => {
|
||||
test.skip(down, 'app.lux.financial unreachable')
|
||||
const body = page.locator('body')
|
||||
await expect(body).toContainText(/CARDS/i)
|
||||
await expect(body).toContainText(/RECENT ACTIVITY/i)
|
||||
// A known seeded transaction proves the activity feed reads bankd.
|
||||
await expect(body).toContainText(/Payroll — Acme Corp/i)
|
||||
})
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Bank Customer — acm white-label (unauthenticated login screen)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -175,91 +88,57 @@ test.describe('Lux Financial — Bank Customer white-label', () => {
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Bank Admin — admin.lux.financial (authenticated control plane)
|
||||
// Bank Admin — acm white-label (unauthenticated login screen)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
test.describe('Lux Financial — Bank Admin', () => {
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
let page: Page
|
||||
let down = false
|
||||
|
||||
test.beforeAll(async ({ browser, request }) => {
|
||||
down = !(await reachable(request, 'admin', ADMIN))
|
||||
if (down) return
|
||||
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 } })
|
||||
page = await ctx.newPage()
|
||||
await loginAdmin(page)
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page?.context().close().catch(() => {})
|
||||
})
|
||||
|
||||
test('logs in → Overview shows portfolio metrics', async () => {
|
||||
test.skip(down, 'admin.lux.financial unreachable')
|
||||
const body = page.locator('body')
|
||||
await expect(body).toContainText('Overview')
|
||||
await expect(body).toContainText(/Total liquidity/i)
|
||||
await expect(body).toContainText(/Pending KYC/i)
|
||||
await expect(body).toContainText(/Recent activity/i)
|
||||
await snap(page, 'luxfin-admin-overview')
|
||||
})
|
||||
|
||||
test('control-plane nav exposes MPC, Safes, KMS, Nodes', async () => {
|
||||
test.skip(down, 'admin.lux.financial unreachable')
|
||||
for (const item of ['MPC', 'Safes', 'KMS', 'Nodes']) {
|
||||
await expect(page.getByRole('link', { name: item, exact: true }).first()).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('MPC / KMS / Nodes pages render their sections', async () => {
|
||||
test.skip(down, 'admin.lux.financial unreachable')
|
||||
const checks: Array<[string, string]> = [
|
||||
['/mpc', 'MPC'],
|
||||
['/kms', 'KMS'],
|
||||
['/nodes', 'Nodes'],
|
||||
]
|
||||
for (const [path, heading] of checks) {
|
||||
await page.goto(`${ADMIN}${path}`, { waitUntil: 'networkidle', timeout: 30_000 })
|
||||
await page.waitForTimeout(1500)
|
||||
const h = page.locator('main h1, main h2, h1, h2').first()
|
||||
await expect(h).toContainText(heading)
|
||||
const mainLen = (await page.locator('main').innerText().catch(() => '')).trim().length
|
||||
console.log(` admin ${path}: heading OK, main content ${mainLen} chars`)
|
||||
expect(mainLen).toBeGreaterThan(50)
|
||||
}
|
||||
})
|
||||
|
||||
// KNOWN DEFECT (documented, not hidden): the Safes route throws
|
||||
// "Invalid currency code : USDC" (USDC passed to Intl.NumberFormat, which
|
||||
// only accepts ISO-4217 codes) and renders a blank page. The nav item exists
|
||||
// (proven above) but the page does not render. Encoded as an expected failure
|
||||
// so the suite stays honest & green; when the USDC formatting bug is fixed
|
||||
// this test will pass unexpectedly and flag removal of the marker.
|
||||
test('Safes page renders (KNOWN BROKEN: Intl currency "USDC" crash)', async () => {
|
||||
test.skip(down, 'admin.lux.financial unreachable')
|
||||
test.fail(true, 'Safes route crashes: RangeError Invalid currency code : USDC')
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (e) => errors.push(e.message))
|
||||
await page.goto(`${ADMIN}/safes`, { waitUntil: 'networkidle', timeout: 30_000 })
|
||||
await page.waitForTimeout(2500)
|
||||
if (errors.length) console.log(` Safes pageerror: ${errors[0]}`)
|
||||
const h = page.locator('main h1, main h2, h1, h2').first()
|
||||
await expect(h).toContainText('Safes')
|
||||
})
|
||||
|
||||
test('?brand=acm renders LIGHT theme with "acm" wordmark (admin login)', async ({ page: fresh, request }) => {
|
||||
test.describe('Lux Financial — Bank Admin white-label', () => {
|
||||
test('?brand=acm renders LIGHT theme with "acm" wordmark (admin login)', async ({ page, request }) => {
|
||||
test.skip(!(await reachable(request, 'admin', ADMIN)), 'admin.lux.financial unreachable')
|
||||
await fresh.goto(`${ADMIN}/login?brand=acm`, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
await fresh.waitForTimeout(1500)
|
||||
const L = await bodyLightness(fresh)
|
||||
await page.goto(`${ADMIN}/login?brand=acm`, { waitUntil: 'domcontentloaded', timeout: 45_000 })
|
||||
await page.waitForTimeout(1500)
|
||||
const L = await bodyLightness(page)
|
||||
console.log(` admin acm background lightness: ${L.toFixed(3)}`)
|
||||
expect(L).toBeGreaterThan(0.6)
|
||||
await expect(fresh.locator('body')).toContainText('acm')
|
||||
await snap(fresh, 'luxfin-admin-acm-light')
|
||||
await expect(page.locator('body')).toContainText('acm')
|
||||
await snap(page, 'luxfin-admin-acm-light')
|
||||
})
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// One way in — Lux ID, from every sign-in screen
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
test.describe('Lux Financial — one way in', () => {
|
||||
const screens: Array<[string, string, RegExp]> = [
|
||||
[APP, '/login', /sign in with lux id/i],
|
||||
[APP, '/signup', /continue with lux id/i],
|
||||
[ADMIN, '/login', /sign in with lux id/i],
|
||||
]
|
||||
for (const [origin, path, button] of screens) {
|
||||
test(`${new URL(origin).host}${path}: no password, Lux ID with PKCE`, async ({ page, request }) => {
|
||||
test.skip(!(await reachable(request, origin, origin)), `${origin} unreachable`)
|
||||
const bundles: string[] = []
|
||||
page.on('response', async (r) => {
|
||||
if (/\.js(\?|$)/.test(r.url())) bundles.push(await r.text().catch(() => ''))
|
||||
})
|
||||
await page.goto(`${origin}${path}`, { waitUntil: 'networkidle', timeout: 45_000 })
|
||||
expect(await page.locator('input[type=password]').count(), 'a console never asks for a password').toBe(0)
|
||||
expect(bundles.length).toBeGreaterThan(0)
|
||||
for (const js of bundles) expect(js, 'a credential shipped in the JS').not.toMatch(/Ilove|DEMO_PASSWORD|demoLogin/)
|
||||
|
||||
await Promise.all([
|
||||
page.waitForURL((u) => u.host === IDP_HOST, { timeout: 30_000 }),
|
||||
page.getByRole('button', { name: button }).click(),
|
||||
])
|
||||
const q = new URL(page.url()).searchParams
|
||||
expect(q.get('client_id')).toBe(CLIENT)
|
||||
expect(q.get('response_type')).toBe('code')
|
||||
expect(q.get('code_challenge_method')).toBe('S256')
|
||||
expect(q.get('code_challenge') ?? '').toMatch(/^[A-Za-z0-9_-]{43,128}$/)
|
||||
expect(q.get('client_secret'), 'a browser client carries no secret').toBeNull()
|
||||
expect(q.get('redirect_uri')).toBe(`${origin}/auth/callback`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Docs — docs.lux.financial
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -287,6 +166,50 @@ test.describe('Lux Financial — Backend (bankd)', () => {
|
||||
const json = (await res.json()) as { code?: number }
|
||||
expect(json.code).toBe(200)
|
||||
})
|
||||
|
||||
test('bankd serves no password route and mints no token of its own', async ({ request }) => {
|
||||
test.skip(!(await reachable(request, 'api', `${API}/v1/health`)), 'api.lux.financial unreachable')
|
||||
for (const path of [
|
||||
'/v1/bank/login',
|
||||
'/v1/platform/auth/login',
|
||||
'/v1/collections/users/auth-with-password',
|
||||
'/v1/collections/_superusers/auth-with-password',
|
||||
'/v1/collections/users/auth-refresh',
|
||||
]) {
|
||||
const res = await request.post(`${API}${path}`, { data: {}, failOnStatusCode: false })
|
||||
expect(res.status(), path).toBe(404)
|
||||
}
|
||||
})
|
||||
|
||||
test('the code exchange takes only lux-financial, by code or refresh', async ({ request }) => {
|
||||
test.skip(!(await reachable(request, 'api', `${API}/v1/health`)), 'api.lux.financial unreachable')
|
||||
for (const form of [
|
||||
{ grant_type: 'password', client_id: CLIENT, username: 'x', password: 'x' },
|
||||
{ grant_type: 'authorization_code', client_id: 'hanzo-cli', code: 'x' },
|
||||
]) {
|
||||
const res = await request.post(`${API}/v1/iam/oauth/token`, { form, failOnStatusCode: false })
|
||||
expect(res.status(), form.grant_type).toBe(400)
|
||||
expect((await res.json()).error).toBe('unauthorized_client')
|
||||
}
|
||||
})
|
||||
|
||||
test('a token lux.id did not issue to lux-financial is anonymous', async ({ request }) => {
|
||||
test.skip(!(await reachable(request, 'api', `${API}/v1/health`)), 'api.lux.financial unreachable')
|
||||
// Unsigned, and naming another issuer, client and org: the shapes bankd refuses.
|
||||
const b64 = (o: object) => Buffer.from(JSON.stringify(o)).toString('base64url')
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600
|
||||
const forged = `${b64({ alg: 'none', typ: 'JWT' })}.${b64({
|
||||
iss: 'https://hanzo.id', aud: ['hanzo-cli'], owner: 'admin', sub: 'x', exp,
|
||||
tokenType: 'access-token', isAdmin: true, orgs: [{ org: 'admin' }],
|
||||
})}.`
|
||||
for (const auth of [undefined, `Bearer ${forged}`]) {
|
||||
const headers = auth ? { Authorization: auth } : undefined
|
||||
const res = await request.get(`${API}/v1/bank/overview`, { headers, failOnStatusCode: false })
|
||||
expect(res.status()).toBe(401)
|
||||
const su = await request.get(`${API}/v1/collections/_superusers/records`, { headers, failOnStatusCode: false })
|
||||
expect(su.status()).toBe(403)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bank-api
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bank-api
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bank-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bank-api
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
containers:
|
||||
- name: bank-api
|
||||
# TODO: bank-api has no semver tag in GHCR; pinned to verified live commit-sha.
|
||||
# Auto-bump should switch to semver once luxfi/bank publishes tagged releases.
|
||||
image: ghcr.io/luxfi/bank-api:291ba2151658f8729a6e32cf8cfdcf80ec17e170
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 4000
|
||||
name: http
|
||||
env:
|
||||
- name: PORT
|
||||
value: "4000"
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bank-secrets
|
||||
key: database-url
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bank-secrets
|
||||
key: jwt-secret
|
||||
- name: ENCRYPT_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bank-secrets
|
||||
key: encrypt-key
|
||||
- name: SENDGRID_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bank-secrets
|
||||
key: sendgrid-key
|
||||
- name: SENTRY_DSN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bank-secrets
|
||||
key: sentry-dsn
|
||||
- name: GOOGLE_RECAPTCHA_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bank-secrets
|
||||
key: recaptcha-secret-key
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: bank-config
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 4000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 4000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bank-api
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: bank-api
|
||||
ports:
|
||||
- name: http
|
||||
port: 4000
|
||||
targetPort: 4000
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: lux-bank
|
||||
resources:
|
||||
- deployment.yaml
|
||||
@@ -1,69 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bank-dash
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bank-dash
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bank-dash
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bank-dash
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
containers:
|
||||
- name: bank-dash
|
||||
# @luxfi/bank-dash — Vite SPA served by the stdlib dashd static+proxy
|
||||
# server (serves the SPA, proxies /v1 -> bankd for same-origin API + IAM).
|
||||
image: ghcr.io/luxfi/bank-dash:v1.6.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
env:
|
||||
- name: BANK_UPSTREAM
|
||||
value: http://bankd.lux-bank.svc.cluster.local
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: DIST_DIR
|
||||
value: /app/dist
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bank-dash
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: bank-dash
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
@@ -1,52 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: bank-dash
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bank-dash
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
ingressClassName: ingress
|
||||
tls:
|
||||
- hosts:
|
||||
- app.lux.financial
|
||||
- lux.finance
|
||||
- www.lux.finance
|
||||
secretName: bank-dash-tls
|
||||
rules:
|
||||
# app.lux.financial — platform / white-label surface.
|
||||
- host: app.lux.financial
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bank-dash
|
||||
port:
|
||||
number: 3000
|
||||
# lux.finance — the B2C home: landing, membership plans, login.
|
||||
- host: lux.finance
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bank-dash
|
||||
port:
|
||||
number: 3000
|
||||
- host: www.lux.finance
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bank-dash
|
||||
port:
|
||||
number: 3000
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: lux-bank
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- ingress.yaml
|
||||
@@ -1,59 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bank-docs
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bank-docs
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-financial
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bank-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bank-docs
|
||||
spec:
|
||||
containers:
|
||||
- name: bank-docs
|
||||
image: ghcr.io/luxbank/financial-docs:1.1.2
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bank-docs
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: bank-docs
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: lux-bank
|
||||
resources:
|
||||
- deployment.yaml
|
||||
@@ -1,191 +0,0 @@
|
||||
# sandbox.lux.financial — the seeded demo bank. Same images as production,
|
||||
# BANK_SANDBOX=true. app.lux.financial flips to real money (BANK_SANDBOX=false
|
||||
# + issuer credentials from KMS) once SF Private issues API access; this stack
|
||||
# keeps the demo alive either way.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bankd-sandbox-data
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bankd-sandbox
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: do-block-storage
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bankd-sandbox
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bankd-sandbox
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bankd-sandbox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bankd-sandbox
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
containers:
|
||||
- name: bankd
|
||||
image: ghcr.io/luxfi/bank:v1.6.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8070
|
||||
name: http
|
||||
env:
|
||||
- name: BANKD_DIR
|
||||
value: /data
|
||||
- name: BANK_SANDBOX
|
||||
value: "true"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /v1/bank/health
|
||||
port: 8070
|
||||
initialDelaySeconds: 8
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /v1/bank/health
|
||||
port: 8070
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bankd-sandbox-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bankd-sandbox
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: bankd-sandbox
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8070
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bank-dash-sandbox
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bank-dash-sandbox
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bank-dash-sandbox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bank-dash-sandbox
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
containers:
|
||||
- name: bank-dash
|
||||
image: ghcr.io/luxfi/bank-dash:v1.6.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
env:
|
||||
- name: BANK_UPSTREAM
|
||||
value: http://bankd-sandbox.lux-bank.svc.cluster.local
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: DIST_DIR
|
||||
value: /app/dist
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bank-dash-sandbox
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: bank-dash-sandbox
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: bank-sandbox
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bank-dash-sandbox
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
ingressClassName: ingress
|
||||
tls:
|
||||
- hosts:
|
||||
- sandbox.lux.financial
|
||||
secretName: bank-sandbox-tls
|
||||
rules:
|
||||
- host: sandbox.lux.financial
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bank-dash-sandbox
|
||||
port:
|
||||
number: 3000
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: lux-bank
|
||||
resources:
|
||||
- deployment.yaml
|
||||
@@ -1,97 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: bankd-data
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bankd
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: do-block-storage
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bankd
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: bankd
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-bank
|
||||
spec:
|
||||
replicas: 1
|
||||
# Single RWO PVC — Recreate avoids Multi-Attach deadlock on rollout.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bankd
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bankd
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
containers:
|
||||
- name: bankd
|
||||
# bankd — Go on hanzoai/base. BaaS API (IAM-native auth via lux.id).
|
||||
# This is the app.lux.financial backend. It runs sandbox until the
|
||||
# issuer credentials land in KMS; the real-money flip is
|
||||
# BANK_SANDBOX=false + SFPRIVATE_URL / SFPRIVATE_API_KEY (KMS:
|
||||
# providers/lux/sfprivate-api-key). The demo home is bankd-sandbox
|
||||
# at sandbox.lux.financial.
|
||||
image: ghcr.io/luxfi/bank:v1.6.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8070
|
||||
name: http
|
||||
env:
|
||||
- name: BANKD_DIR
|
||||
value: /data
|
||||
- name: BANK_SANDBOX
|
||||
value: "true"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /v1/bank/health
|
||||
port: 8070
|
||||
initialDelaySeconds: 8
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /v1/bank/health
|
||||
port: 8070
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: bankd-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bankd
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: bankd
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8070
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: lux-bank
|
||||
resources:
|
||||
- deployment.yaml
|
||||
@@ -1,64 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: lux-financial
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app: lux-financial
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-financial
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lux-financial
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: lux-financial
|
||||
spec:
|
||||
containers:
|
||||
- name: spa
|
||||
image: ghcr.io/luxfi/lux-financial:v1.4.0
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: http
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: ROOT
|
||||
value: "/public"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
imagePullSecrets:
|
||||
- name: ghcr-secret
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: lux-financial
|
||||
namespace: lux-bank
|
||||
spec:
|
||||
selector:
|
||||
app: lux-financial
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
@@ -1,38 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: lux-financial
|
||||
namespace: lux-bank
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: lux-universe
|
||||
app.kubernetes.io/part-of: lux-financial
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
ingressClassName: ingress
|
||||
tls:
|
||||
- hosts:
|
||||
- lux.financial
|
||||
- www.lux.financial
|
||||
secretName: lux-financial-tls
|
||||
rules:
|
||||
- host: lux.financial
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: lux-financial
|
||||
port:
|
||||
number: 80
|
||||
- host: www.lux.financial
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: lux-financial
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: lux-bank
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- ingress.yaml
|
||||
@@ -36,12 +36,6 @@ resources:
|
||||
- safe
|
||||
- vote
|
||||
- blog
|
||||
- bank-docs
|
||||
- bank-api
|
||||
- bank-dash
|
||||
- bank-sandbox
|
||||
- bankd
|
||||
- financial
|
||||
# luxd is deployed only from deploy/lux-{mainnet,testnet,devnet}/luxd.yaml.
|
||||
# Keeping a validator StatefulSet here would create a second, stale rollout path.
|
||||
- validators/crd-resources.yaml
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
/**
|
||||
* Seed Admin User for Lux Financial
|
||||
*
|
||||
* Creates a SuperAdmin user with a corresponding Account entity.
|
||||
* Password is hashed with bcrypt (cost factor 14) matching @luxbank/misc.
|
||||
*
|
||||
* Required env vars:
|
||||
* DATABASE_URL — PostgreSQL connection string
|
||||
* ADMIN_EMAIL — Admin email (e.g. [email protected])
|
||||
* ADMIN_PASSWORD — Admin password (from KMS, never hardcoded)
|
||||
* ADMIN_FIRSTNAME — First name
|
||||
* ADMIN_LASTNAME — Last name
|
||||
*
|
||||
* Usage:
|
||||
* DATABASE_URL="postgresql://..." ADMIN_EMAIL="[email protected]" ADMIN_PASSWORD="..." \
|
||||
* ADMIN_FIRSTNAME="Zach" ADMIN_LASTNAME="Kelling" \
|
||||
* npx --package=pg --package=bcryptjs --package=ts-node ts-node scripts/seed-bank-admin.ts
|
||||
*
|
||||
* Or via kubectl exec into the API pod:
|
||||
* kubectl exec -n lux-bank <pod> -- node -e '...'
|
||||
*/
|
||||
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { Client } = require("pg");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const bcrypt = require("bcryptjs");
|
||||
|
||||
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||
const ADMIN_FIRSTNAME = process.env.ADMIN_FIRSTNAME || "Admin";
|
||||
const ADMIN_LASTNAME = process.env.ADMIN_LASTNAME || "User";
|
||||
const ADMIN_ROLE = "admin:super";
|
||||
const BCRYPT_ROUNDS = 14;
|
||||
|
||||
async function main() {
|
||||
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
||||
console.error("ADMIN_EMAIL and ADMIN_PASSWORD environment variables are required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
console.error("DATABASE_URL environment variable is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new Client({ connectionString: databaseUrl });
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// Check if user already exists
|
||||
const existing = await client.query(
|
||||
'SELECT uuid FROM "user" WHERE username = $1',
|
||||
[ADMIN_EMAIL]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
// Update password
|
||||
const hash = await bcrypt.hash(ADMIN_PASSWORD, BCRYPT_ROUNDS);
|
||||
await client.query(
|
||||
'UPDATE "user" SET password = $1 WHERE username = $2',
|
||||
[hash, ADMIN_EMAIL]
|
||||
);
|
||||
console.log(`Updated password for ${ADMIN_EMAIL} (uuid: ${existing.rows[0].uuid})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(ADMIN_PASSWORD, BCRYPT_ROUNDS);
|
||||
const now = new Date();
|
||||
|
||||
// Create account
|
||||
const accountUuid = randomUUID();
|
||||
await client.query(
|
||||
'INSERT INTO account (uuid, created_at, updated_at) VALUES ($1, $2, $3)',
|
||||
[accountUuid, now, now]
|
||||
);
|
||||
|
||||
// Create contact
|
||||
const contactUuid = randomUUID();
|
||||
await client.query(
|
||||
'INSERT INTO contact (uuid, country, is_approved, is_sub_account, account_uuid, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)',
|
||||
[contactUuid, "US", true, false, accountUuid, now, now]
|
||||
);
|
||||
|
||||
// Create user
|
||||
const userUuid = randomUUID();
|
||||
await client.query(
|
||||
'INSERT INTO "user" (uuid, username, password, firstname, lastname, has_accepted_terms, verified_at, contact_uuid, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)',
|
||||
[userUuid, ADMIN_EMAIL, passwordHash, ADMIN_FIRSTNAME, ADMIN_LASTNAME, true, now, contactUuid, now, now]
|
||||
);
|
||||
|
||||
// Create client
|
||||
const clientUuid = randomUUID();
|
||||
await client.query(
|
||||
'INSERT INTO client (uuid, created_at, updated_at) VALUES ($1, $2, $3)',
|
||||
[clientUuid, now, now]
|
||||
);
|
||||
|
||||
// Link user to client
|
||||
const ucUuid = randomUUID();
|
||||
await client.query(
|
||||
'INSERT INTO user_clients (uuid, user_uuid, client_uuid) VALUES ($1, $2, $3)',
|
||||
[ucUuid, userUuid, clientUuid]
|
||||
);
|
||||
|
||||
// Set role
|
||||
const ucmUuid = randomUUID();
|
||||
await client.query(
|
||||
'INSERT INTO user_clients_metadata (uuid, user_client_uuid, role) VALUES ($1, $2, $3)',
|
||||
[ucmUuid, ucUuid, ADMIN_ROLE]
|
||||
);
|
||||
|
||||
console.log(`Seeded admin: ${ADMIN_EMAIL} (uuid: ${userUuid}, role: ${ADMIN_ROLE})`);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Seed failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user