also looking at this
fix(dashboard): stop remote commands importing an absent config module
#59
5 files
+114
-0
| @@ -0,0 +1,21 @@ | ||
| 1 | import { Action } from '@stacksjs/actions' | |
| 2 | import { response } from '@stacksjs/router' | |
| 3 | ||
| 4 | /** | |
| 5 | * AnalyticsHQ does not configure dashboard-managed SSH hosts or commands. | |
| 6 | * | |
| 7 | * Keeping this application override prevents the framework default from | |
| 8 | * importing the optional `~/config/remote` module while still giving the | |
| 9 | * dashboard a valid, empty registry. | |
| 10 | */ | |
| 11 | export default new Action({ | |
| 12 | name: 'RemoteCommandIndexAction', | |
| 13 | description: 'List the remote commands configured for AnalyticsHQ', | |
| 14 | method: 'GET', | |
| 15 | async handle() { | |
| 16 | return response.json({ | |
| 17 | hosts: [], | |
| 18 | commands: [], | |
| 19 | }) | |
| 20 | }, | |
| 21 | }) | |
| @@ -0,0 +1,16 @@ | ||
| 1 | import { Action } from '@stacksjs/actions' | |
| 2 | import { response } from '@stacksjs/router' | |
| 3 | ||
| 4 | /** | |
| 5 | * Fail closed because AnalyticsHQ has no remote-command registry or gate. | |
| 6 | */ | |
| 7 | export default new Action({ | |
| 8 | name: 'RemoteCommandRunAction', | |
| 9 | description: 'Reject remote commands because AnalyticsHQ does not configure them', | |
| 10 | method: 'POST', | |
| 11 | async handle() { | |
| 12 | return response.json({ | |
| 13 | message: 'Remote commands are not configured for AnalyticsHQ.', | |
| 14 | }, 403) | |
| 15 | }, | |
| 16 | }) | |
| @@ -0,0 +1,27 @@ | ||
| 1 | -- AnalyticsHQ enables the framework newsletter routes, but its application | |
| 2 | -- migration corpus predates the framework Subscriber models. Keep the schema | |
| 3 | -- here so a clean PostgreSQL database can serve the public signup action. | |
| 4 | CREATE TABLE IF NOT EXISTS "subscribers" ( | |
| 5 | "id" BIGSERIAL PRIMARY KEY, | |
| 6 | "email" varchar(255) NOT NULL, | |
| 7 | "status" varchar(32) NOT NULL DEFAULT 'subscribed' CHECK ("status" IN ('subscribed', 'unsubscribed', 'pending', 'bounced')), | |
| 8 | "source" varchar(100) DEFAULT 'homepage', | |
| 9 | "unsubscribed_at" timestamp, | |
| 10 | "user_id" bigint REFERENCES "users"("id"), | |
| 11 | "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 12 | "updated_at" timestamp, | |
| 13 | "uuid" varchar(255) | |
| 14 | ); | |
| 15 | CREATE UNIQUE INDEX IF NOT EXISTS "subscribers_email_unique" ON "subscribers" ("email"); | |
| 16 | CREATE UNIQUE INDEX IF NOT EXISTS "subscribers_uuid_unique" ON "subscribers" ("uuid"); | |
| 17 | ||
| 18 | CREATE TABLE IF NOT EXISTS "subscriber_emails" ( | |
| 19 | "id" BIGSERIAL PRIMARY KEY, | |
| 20 | "email" varchar(255) NOT NULL, | |
| 21 | "source" varchar(100) DEFAULT 'homepage', | |
| 22 | "subscriber_id" bigint REFERENCES "subscribers"("id"), | |
| 23 | "created_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| 24 | "updated_at" timestamp, | |
| 25 | "uuid" varchar(255) | |
| 26 | ); | |
| 27 | CREATE UNIQUE INDEX IF NOT EXISTS "subscriber_emails_uuid_unique" ON "subscriber_emails" ("uuid"); | |
| @@ -0,0 +1,24 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import RemoteCommandIndexAction from '../../app/Actions/Dashboard/Remote/RemoteCommandIndexAction' | |
| 3 | import RemoteCommandRunAction from '../../app/Actions/Dashboard/Remote/RemoteCommandRunAction' | |
| 4 | ||
| 5 | describe('dashboard remote commands', () => { | |
| 6 | it('loads without the optional framework remote config alias', async () => { | |
| 7 | const result = await RemoteCommandIndexAction.handle() | |
| 8 | ||
| 9 | expect(result.status).toBe(200) | |
| 10 | expect(await result.json()).toEqual({ | |
| 11 | hosts: [], | |
| 12 | commands: [], | |
| 13 | }) | |
| 14 | }) | |
| 15 | ||
| 16 | it('fails closed when a remote command run is requested', async () => { | |
| 17 | const result = await RemoteCommandRunAction.handle() | |
| 18 | ||
| 19 | expect(result.status).toBe(403) | |
| 20 | expect(await result.json()).toEqual({ | |
| 21 | message: 'Remote commands are not configured for AnalyticsHQ.', | |
| 22 | }) | |
| 23 | }) | |
| 24 | }) | |
| @@ -0,0 +1,26 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { readFileSync } from 'node:fs' | |
| 3 | import { join } from 'node:path' | |
| 4 | ||
| 5 | const migration = readFileSync( | |
| 6 | join(import.meta.dir, '../../database/migrations/0000000053-create-email-subscriber-tables.sql'), | |
| 7 | 'utf8', | |
| 8 | ) | |
| 9 | ||
| 10 | describe('email subscriber migration', () => { | |
| 11 | it('creates both tables needed by SubscriberEmailAction', () => { | |
| 12 | expect(migration).toContain('CREATE TABLE IF NOT EXISTS "subscribers"') | |
| 13 | expect(migration).toContain('CREATE TABLE IF NOT EXISTS "subscriber_emails"') | |
| 14 | expect(migration.indexOf('"subscribers"')).toBeLessThan(migration.indexOf('"subscriber_emails"')) | |
| 15 | }) | |
| 16 | ||
| 17 | it('keeps subscriber identity and lookup fields unique', () => { | |
| 18 | expect(migration).toContain('"subscribers_email_unique"') | |
| 19 | expect(migration).toContain('"subscribers_uuid_unique"') | |
| 20 | expect(migration).toContain('"subscriber_emails_uuid_unique"') | |
| 21 | }) | |
| 22 | ||
| 23 | it('connects subscriber emails to their subscriber', () => { | |
| 24 | expect(migration).toContain('"subscriber_id" bigint REFERENCES "subscribers"("id")') | |
| 25 | }) | |
| 26 | }) | |