@podosoft/podokit 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -2
- package/dist/add.d.ts +2 -0
- package/dist/add.js +5 -1
- package/dist/assemble.d.ts +14 -0
- package/dist/assemble.js +60 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.js +7 -0
- package/dist/eject.d.ts +11 -0
- package/dist/eject.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -1
- package/dist/inspect.d.ts +43 -0
- package/dist/inspect.js +82 -0
- package/dist/lockfile.d.ts +80 -0
- package/dist/lockfile.js +193 -0
- package/dist/templates/base/dot-env.example +11 -6
- package/dist/templates/fullstack-nest-svelte/README.md +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +6 -3
- package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +4 -12
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/dot-env.example +10 -5
- package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +37 -0
- package/dist/templates/fullstack-nest-svelte/infra/docker/sms-sink.mjs +40 -0
- package/dist/templates/fullstack-nest-svelte/tests/README.md +1 -1
- package/dist/templates/fullstack-nest-svelte/tests/package.json +2 -1
- package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +7 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +149 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-pagination.svelte +21 -22
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +93 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +224 -8
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +19 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/accept-invitation/[id]/+page.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/+page.svelte +21 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +360 -49
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +82 -37
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.server.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +264 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +3 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +79 -45
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +322 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +3 -5
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +254 -108
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/forgot-password/+page.svelte +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +14 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +127 -5
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +18 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/verify-email/+page.svelte +63 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +54 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +51 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +80 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +264 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +120 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +10 -10
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +49 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/totp.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +12 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +77 -6
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +42 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +57 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +36 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +9 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +28 -12
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +35 -3
- package/dist/templates/modules/admin-dashboard/module.manifest.json +13 -7
- package/dist/templates/modules/api-key-auth/module.manifest.json +3 -3
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +16 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +4 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +2 -0
- package/dist/templates/modules/audit-log/module.manifest.json +15 -9
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +78 -13
- package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +73 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +197 -52
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +12 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/org-permissions.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/permissions.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.controller.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.entity.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.module.ts +12 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +118 -0
- package/dist/templates/modules/auth/files/apps/api/src/mail/mailer.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +27 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-setting.entity.ts +16 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +25 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.module.ts +11 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +57 -0
- package/dist/templates/modules/auth/files/apps/api/src/sms/sms.ts +23 -0
- package/dist/templates/modules/auth/module.manifest.json +54 -20
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +6 -3
- package/dist/templates/modules/bullmq/module.manifest.json +4 -4
- package/dist/templates/modules/file-upload/module.manifest.json +3 -3
- package/dist/templates/modules/job-progress/module.manifest.json +9 -9
- package/dist/templates/modules/logging/module.manifest.json +3 -3
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -3
- package/dist/templates/modules/object-storage-s3/module.manifest.json +5 -5
- package/dist/templates/modules/rate-limit/module.manifest.json +2 -2
- package/dist/templates/modules/redis/module.manifest.json +4 -4
- package/dist/templates/modules/sse/module.manifest.json +4 -4
- package/dist/templates/todo/README.md +2 -2
- package/dist/templates/todo/apps/api/package.json +1 -0
- package/dist/templates/todo/apps/api/src/app.module.ts +6 -3
- package/dist/templates/todo/apps/api/src/common/app-exception.ts +4 -12
- package/dist/templates/todo/apps/api/src/config/env.validation.ts +1 -1
- package/dist/templates/todo/apps/api/src/main.ts +1 -1
- package/dist/templates/todo/apps/web/package.json +2 -2
- package/dist/templates/todo/apps/web/src/lib/components/ui/switch/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +1 -1
- package/dist/templates/todo/dot-env.example +10 -5
- package/dist/templates/todo/infra/docker/docker-compose.yml +4 -0
- package/dist/templates/todo/tests/README.md +1 -1
- package/dist/templates/todo/tests/package.json +2 -1
- package/dist/templates/todo/tests/playwright.config.ts +1 -1
- package/dist/update.d.ts +49 -0
- package/dist/update.js +158 -0
- package/package.json +2 -2
package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class InitAppSettings1720400000000 implements MigrationInterface {
|
|
4
|
+
name = "InitAppSettings1720400000000";
|
|
5
|
+
|
|
6
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "app_setting" (
|
|
9
|
+
"key" text NOT NULL,
|
|
10
|
+
"value" text NOT NULL,
|
|
11
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
12
|
+
CONSTRAINT "PK_app_setting_key" PRIMARY KEY ("key")
|
|
13
|
+
)
|
|
14
|
+
`);
|
|
15
|
+
// Shipped defaults for the admin-managed auth feature flags (single source of
|
|
16
|
+
// truth is this table; toggle them on the admin Settings page). Must match
|
|
17
|
+
// FLAG_DEFAULTS in settings/settings.service.ts. phoneNumber ships off —
|
|
18
|
+
// real SMS delivery needs a provider wired into sendOTP.
|
|
19
|
+
await queryRunner.query(`
|
|
20
|
+
INSERT INTO "app_setting" ("key", "value") VALUES
|
|
21
|
+
('twoFactor', 'true'),
|
|
22
|
+
('magicLink', 'true'),
|
|
23
|
+
('emailOtp', 'true'),
|
|
24
|
+
('username', 'true'),
|
|
25
|
+
('multiSession', 'true'),
|
|
26
|
+
('phoneNumber', 'false'),
|
|
27
|
+
('apiKey', 'true'),
|
|
28
|
+
('passkey', 'true'),
|
|
29
|
+
('organization', 'true'),
|
|
30
|
+
('oidcProvider', 'false')
|
|
31
|
+
ON CONFLICT ("key") DO NOTHING
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
36
|
+
await queryRunner.query(`DROP TABLE "app_setting"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class InitAuthConfig1720500000000 implements MigrationInterface {
|
|
4
|
+
name = "InitAuthConfig1720500000000";
|
|
5
|
+
|
|
6
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
// Admin-managed auth configuration (OAuth providers, SMTP, server-enforced
|
|
8
|
+
// toggles). Secrets (OAuth client secret, SMTP password) are stored encrypted
|
|
9
|
+
// in "secret" (AES-256-GCM envelope; key derived from BETTER_AUTH_SECRET —
|
|
10
|
+
// never in this DB). Non-secret fields live in "config" (jsonb). No rows are
|
|
11
|
+
// seeded: an empty table means "use environment variables" (legacy behavior).
|
|
12
|
+
await queryRunner.query(`
|
|
13
|
+
CREATE TABLE "auth_config" (
|
|
14
|
+
"key" text NOT NULL,
|
|
15
|
+
"enabled" boolean NOT NULL DEFAULT false,
|
|
16
|
+
"config" jsonb NOT NULL DEFAULT '{}',
|
|
17
|
+
"secret" text,
|
|
18
|
+
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
19
|
+
CONSTRAINT "PK_auth_config_key" PRIMARY KEY ("key")
|
|
20
|
+
)
|
|
21
|
+
`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
25
|
+
await queryRunner.query(`DROP TABLE "auth_config"`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Column, Entity, PrimaryColumn, UpdateDateColumn } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/** A single admin-editable application setting, stored as a string key/value.
|
|
4
|
+
* Feature flags (twoFactor, magicLink, ...) live here so an admin can change
|
|
5
|
+
* them from the UI instead of editing environment variables. */
|
|
6
|
+
@Entity({ name: "app_setting" })
|
|
7
|
+
export class AppSetting {
|
|
8
|
+
@PrimaryColumn({ type: "text" })
|
|
9
|
+
key!: string;
|
|
10
|
+
|
|
11
|
+
@Column({ type: "text" })
|
|
12
|
+
value!: string;
|
|
13
|
+
|
|
14
|
+
@UpdateDateColumn()
|
|
15
|
+
updatedAt!: Date;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Admin-managed auth feature flags and their shipped defaults.
|
|
2
|
+
*
|
|
3
|
+
* Kept free of Nest/TypeORM imports so both the DI-managed SettingsService and
|
|
4
|
+
* the better-auth feature gate (loaded by auth.ts, outside DI — including when
|
|
5
|
+
* the better-auth CLI evaluates auth.ts for migrations) can import it safely.
|
|
6
|
+
*
|
|
7
|
+
* Must match the rows seeded by the InitAppSettings migration. phoneNumber is
|
|
8
|
+
* off by default because real delivery needs an SMS provider. */
|
|
9
|
+
export type FeatureFlag = "twoFactor" | "magicLink" | "emailOtp" | "username" | "multiSession" | "phoneNumber" | "apiKey" | "passkey" | "organization" | "oidcProvider";
|
|
10
|
+
|
|
11
|
+
export const FLAG_DEFAULTS: Record<FeatureFlag, boolean> = {
|
|
12
|
+
twoFactor: true,
|
|
13
|
+
magicLink: true,
|
|
14
|
+
emailOtp: true,
|
|
15
|
+
username: true,
|
|
16
|
+
multiSession: true,
|
|
17
|
+
phoneNumber: false,
|
|
18
|
+
apiKey: true,
|
|
19
|
+
passkey: true,
|
|
20
|
+
organization: true,
|
|
21
|
+
// Acting as an OIDC identity provider is an enterprise opt-in; off by default.
|
|
22
|
+
oidcProvider: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const FEATURE_FLAGS = Object.keys(FLAG_DEFAULTS) as FeatureFlag[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
3
|
+
import { AppSetting } from "./app-setting.entity";
|
|
4
|
+
import { SettingsService } from "./settings.service";
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [TypeOrmModule.forFeature([AppSetting])],
|
|
8
|
+
providers: [SettingsService],
|
|
9
|
+
exports: [SettingsService],
|
|
10
|
+
})
|
|
11
|
+
export class SettingsModule {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Injectable, type OnModuleInit } from "@nestjs/common";
|
|
2
|
+
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
|
+
import { Repository } from "typeorm";
|
|
4
|
+
import { AppSetting } from "./app-setting.entity";
|
|
5
|
+
|
|
6
|
+
import { FLAG_DEFAULTS, FEATURE_FLAGS, type FeatureFlag } from "./flag-defaults";
|
|
7
|
+
|
|
8
|
+
/** Admin-editable feature flags — the DB (app_setting) is the single source of
|
|
9
|
+
* truth. Defaults are seeded by the InitAppSettings migration and changed live
|
|
10
|
+
* from the admin Settings page. Only features whose plugin can be mounted
|
|
11
|
+
* unconditionally live here; their endpoints are blocked server-side when off
|
|
12
|
+
* (see auth/feature-gate.ts). Server-enforced flags (email verification, breach
|
|
13
|
+
* check, self-deletion) and secrets/infra stay in the environment. */
|
|
14
|
+
export { FEATURE_FLAGS, type FeatureFlag };
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class SettingsService implements OnModuleInit {
|
|
18
|
+
private cache = new Map<string, string>();
|
|
19
|
+
|
|
20
|
+
constructor(@InjectRepository(AppSetting) private readonly repo: Repository<AppSetting>) {}
|
|
21
|
+
|
|
22
|
+
async onModuleInit(): Promise<void> {
|
|
23
|
+
await this.refresh();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async refresh(): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
const rows = await this.repo.find();
|
|
29
|
+
this.cache = new Map(rows.map((r) => [r.key, r.value]));
|
|
30
|
+
} catch {
|
|
31
|
+
// Table not migrated yet — fall back to the shipped defaults until it exists.
|
|
32
|
+
this.cache = new Map();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** DB value (migration-seeded or admin-set); shipped default if the row is missing. */
|
|
37
|
+
getBool(flag: FeatureFlag): boolean {
|
|
38
|
+
const stored = this.cache.get(flag);
|
|
39
|
+
if (stored !== undefined) return stored === "true";
|
|
40
|
+
return FLAG_DEFAULTS[flag];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
flags(): Record<FeatureFlag, boolean> {
|
|
44
|
+
return Object.fromEntries(FEATURE_FLAGS.map((f) => [f, this.getBool(f)])) as Record<FeatureFlag, boolean>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async setMany(update: Partial<Record<FeatureFlag, boolean>>): Promise<Record<FeatureFlag, boolean>> {
|
|
48
|
+
for (const flag of FEATURE_FLAGS) {
|
|
49
|
+
const next = update[flag];
|
|
50
|
+
if (typeof next === "boolean") {
|
|
51
|
+
await this.repo.save({ key: flag, value: String(next) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await this.refresh();
|
|
55
|
+
return this.flags();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Outgoing SMS transport. Point SMS_WEBHOOK_URL at your provider's HTTP API in
|
|
2
|
+
// production (or adapt this to a vendor SDK); in local dev it targets the SMS
|
|
3
|
+
// sink (docker-compose `dev` profile), which captures every message for tests to
|
|
4
|
+
// read back. With no SMS_WEBHOOK_URL set, fall back to logging so nothing crashes.
|
|
5
|
+
export type Sms = { to: string; body: string };
|
|
6
|
+
|
|
7
|
+
export async function sendSms(sms: Sms): Promise<void> {
|
|
8
|
+
const url = process.env.SMS_WEBHOOK_URL;
|
|
9
|
+
if (!url) {
|
|
10
|
+
console.warn(`[sms] no SMS_WEBHOOK_URL configured; not delivered — to ${sms.to}: ${sms.body}`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "content-type": "application/json" },
|
|
17
|
+
body: JSON.stringify(sms),
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) console.warn(`[sms] delivery returned ${res.status} for ${sms.to}`);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.warn(`[sms] delivery failed for ${sms.to}:`, err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -4,20 +4,43 @@
|
|
|
4
4
|
"requires": [],
|
|
5
5
|
"targetApp": "api",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"@podosoft/podokit-auth": "^0.1.0",
|
|
8
|
+
"@podosoft/podokit-contracts": "^0.1.0",
|
|
7
9
|
"better-auth": "^1.6.23",
|
|
8
10
|
"@thallesp/nestjs-better-auth": "^2.7.0",
|
|
9
|
-
"pg": "^8.12.0"
|
|
11
|
+
"pg": "^8.12.0",
|
|
12
|
+
"nodemailer": "^7.0.9",
|
|
13
|
+
"@better-auth/api-key": "^1.6.23",
|
|
14
|
+
"@better-auth/passkey": "^1.6.23",
|
|
15
|
+
"@better-auth/oauth-provider": "^1.6.23"
|
|
10
16
|
},
|
|
11
17
|
"devDependencies": {
|
|
12
|
-
"@types/pg": "^8.11.10"
|
|
18
|
+
"@types/pg": "^8.11.10",
|
|
19
|
+
"@types/nodemailer": "^7.0.4"
|
|
13
20
|
},
|
|
14
21
|
"env": [
|
|
15
|
-
"#
|
|
22
|
+
"# \u2500\u2500 Auth (better-auth) \u2014 the only required secrets \u2500\u2500",
|
|
23
|
+
"# BETTER_AUTH_SECRET also derives the key that encrypts config secrets stored in the",
|
|
24
|
+
"# DB (OAuth client secrets, SMTP password). Keep it stable and secret; rotating it",
|
|
25
|
+
"# makes previously-saved DB secrets undecryptable.",
|
|
16
26
|
"BETTER_AUTH_SECRET=change-me-in-production-min-32-characters",
|
|
17
|
-
"BETTER_AUTH_URL=http://localhost:
|
|
18
|
-
"#
|
|
19
|
-
"
|
|
20
|
-
"#
|
|
27
|
+
"BETTER_AUTH_URL=http://localhost:5002",
|
|
28
|
+
"# Everything below is OPTIONAL. OAuth providers, SMTP, and the server-enforced",
|
|
29
|
+
"# toggles (email verification, breached-password check, self-delete) \u2014 plus the",
|
|
30
|
+
"# feature flags \u2014 are configured on the admin Settings page, stored encrypted in",
|
|
31
|
+
"# the DB and applied live (no restart). These env vars still work as a fallback/",
|
|
32
|
+
"# override (e.g. for secrets-manager injection in production); the DB takes",
|
|
33
|
+
"# precedence when set.",
|
|
34
|
+
"# --- Optional overrides / local development ---",
|
|
35
|
+
"# Email (Mailpit in local dev: docker compose --profile dev up \u2192 http://localhost:8025)",
|
|
36
|
+
"# SMTP_HOST=localhost",
|
|
37
|
+
"# SMTP_PORT=1025",
|
|
38
|
+
"# MAIL_FROM=PodoKit <no-reply@example.com>",
|
|
39
|
+
"# Phone-number SMS webhook (dev: SMS sink on port 8095)",
|
|
40
|
+
"# SMS_WEBHOOK_URL=http://localhost:8095/sms",
|
|
41
|
+
"# AUTH_HIBP=false",
|
|
42
|
+
"# AUTH_EMAIL_VERIFICATION=false",
|
|
43
|
+
"# AUTH_ALLOW_DELETE=false",
|
|
21
44
|
"# GOOGLE_CLIENT_ID=",
|
|
22
45
|
"# GOOGLE_CLIENT_SECRET=",
|
|
23
46
|
"# GITHUB_CLIENT_ID=",
|
|
@@ -26,45 +49,56 @@
|
|
|
26
49
|
"inject": [
|
|
27
50
|
{
|
|
28
51
|
"file": "apps/api/src/app.module.ts",
|
|
29
|
-
"marker": "// podokit:imports",
|
|
52
|
+
"marker": "// podokit:end:imports",
|
|
30
53
|
"text": "import { APP_GUARD } from \"@nestjs/core\";"
|
|
31
54
|
},
|
|
32
55
|
{
|
|
33
56
|
"file": "apps/api/src/app.module.ts",
|
|
34
|
-
"marker": "// podokit:imports",
|
|
57
|
+
"marker": "// podokit:end:imports",
|
|
35
58
|
"text": "import { AuthModule, AuthGuard } from \"@thallesp/nestjs-better-auth\";"
|
|
36
59
|
},
|
|
37
60
|
{
|
|
38
61
|
"file": "apps/api/src/app.module.ts",
|
|
39
|
-
"marker": "// podokit:imports",
|
|
40
|
-
"text": "import {
|
|
62
|
+
"marker": "// podokit:end:imports",
|
|
63
|
+
"text": "import { authRuntime } from \"./auth/auth-provider\";"
|
|
41
64
|
},
|
|
42
65
|
{
|
|
43
66
|
"file": "apps/api/src/app.module.ts",
|
|
44
|
-
"marker": "// podokit:imports",
|
|
67
|
+
"marker": "// podokit:end:imports",
|
|
45
68
|
"text": "import { AccountModule } from \"./account/account.module\";"
|
|
46
69
|
},
|
|
47
70
|
{
|
|
48
71
|
"file": "apps/api/src/app.module.ts",
|
|
49
|
-
"marker": "// podokit:
|
|
50
|
-
"text": "
|
|
72
|
+
"marker": "// podokit:end:imports",
|
|
73
|
+
"text": "import { AuthConfigModule } from \"./auth-config/auth-config.module\";"
|
|
51
74
|
},
|
|
52
75
|
{
|
|
53
76
|
"file": "apps/api/src/app.module.ts",
|
|
54
|
-
"marker": "// podokit:module-imports",
|
|
77
|
+
"marker": "// podokit:end:module-imports",
|
|
78
|
+
"text": "AuthModule.forRoot(authRuntime),"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"file": "apps/api/src/app.module.ts",
|
|
82
|
+
"marker": "// podokit:end:module-imports",
|
|
55
83
|
"text": "AccountModule,"
|
|
56
84
|
},
|
|
57
85
|
{
|
|
58
86
|
"file": "apps/api/src/app.module.ts",
|
|
59
|
-
"marker": "// podokit:
|
|
87
|
+
"marker": "// podokit:end:module-imports",
|
|
88
|
+
"text": "AuthConfigModule,"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"file": "apps/api/src/app.module.ts",
|
|
92
|
+
"marker": "// podokit:end:providers",
|
|
60
93
|
"text": "{ provide: APP_GUARD, useClass: AuthGuard },"
|
|
61
94
|
}
|
|
62
95
|
],
|
|
63
96
|
"instructions": [
|
|
64
|
-
"Set BETTER_AUTH_SECRET in .env (>= 32 chars).",
|
|
97
|
+
"Set BETTER_AUTH_SECRET in .env (>= 32 chars). This also derives the key that encrypts config secrets stored in the DB, so keep it stable and secret.",
|
|
65
98
|
"Create the auth tables: npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
99
|
+
"Create the app tables (auth_config, app_setting): npm run migration:run",
|
|
100
|
+
"Sign up: curl -c cookies.txt -XPOST localhost:5002/api/auth/sign-up/email -H 'content-type: application/json' -d '{\"email\":\"a@example.com\",\"password\":\"password123\",\"name\":\"A\"}'",
|
|
101
|
+
"Call a protected route with the session: curl -b cookies.txt localhost:5002/account/me",
|
|
102
|
+
"Everything is protected by default; mark public routes with @Public(). Configure OAuth (Google/GitHub), SMTP, and server-enforced toggles on the admin Settings page \u2014 stored encrypted in the DB and applied live (no restart). Environment variables (GOOGLE_CLIENT_*, SMTP_*, AUTH_*) still work as an optional fallback/override."
|
|
69
103
|
]
|
|
70
104
|
}
|
|
@@ -2,7 +2,8 @@ import { Module } from "@nestjs/common";
|
|
|
2
2
|
import { BullModule } from "@nestjs/bullmq";
|
|
3
3
|
import { DemoProcessor } from "./demo.processor";
|
|
4
4
|
import { DEMO_QUEUE, redisConnection } from "./queue";
|
|
5
|
-
// podokit:worker-imports
|
|
5
|
+
// podokit:begin:worker-imports
|
|
6
|
+
// podokit:end:worker-imports
|
|
6
7
|
|
|
7
8
|
// Consumer side: runs BullMQ processors. Bootstrapped by main-worker.ts as a
|
|
8
9
|
// separate process so workers scale independently of the API.
|
|
@@ -10,11 +11,13 @@ import { DEMO_QUEUE, redisConnection } from "./queue";
|
|
|
10
11
|
imports: [
|
|
11
12
|
BullModule.forRoot({ connection: redisConnection() }),
|
|
12
13
|
BullModule.registerQueue({ name: DEMO_QUEUE }),
|
|
13
|
-
// podokit:worker-queues
|
|
14
|
+
// podokit:begin:worker-queues
|
|
15
|
+
// podokit:end:worker-queues
|
|
14
16
|
],
|
|
15
17
|
providers: [
|
|
16
18
|
DemoProcessor,
|
|
17
|
-
// podokit:worker-providers
|
|
19
|
+
// podokit:begin:worker-providers
|
|
20
|
+
// podokit:end:worker-providers
|
|
18
21
|
],
|
|
19
22
|
})
|
|
20
23
|
export class WorkerModule {}
|
|
@@ -17,20 +17,20 @@
|
|
|
17
17
|
"inject": [
|
|
18
18
|
{
|
|
19
19
|
"file": "apps/api/src/app.module.ts",
|
|
20
|
-
"marker": "// podokit:imports",
|
|
20
|
+
"marker": "// podokit:end:imports",
|
|
21
21
|
"text": "import { JobsModule } from \"./jobs/jobs.module\";"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"file": "apps/api/src/app.module.ts",
|
|
25
|
-
"marker": "// podokit:module-imports",
|
|
25
|
+
"marker": "// podokit:end:module-imports",
|
|
26
26
|
"text": "JobsModule,"
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
29
|
"instructions": [
|
|
30
30
|
"Ensure Redis is running: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
31
31
|
"Run the worker in a separate terminal: npm run dev:worker -w <app>-api",
|
|
32
|
-
"Enqueue a job: curl -XPOST localhost:
|
|
33
|
-
"Check status: curl localhost:
|
|
32
|
+
"Enqueue a job: curl -XPOST localhost:5002/jobs -H 'content-type: application/json' -d '{\"text\":\"hello\"}'",
|
|
33
|
+
"Check status: curl localhost:5002/jobs/<id> (state goes waiting -> active -> completed)",
|
|
34
34
|
"Deploy the worker as its own process: k3s manifest added at infra/k3s/worker-deployment.yaml; Docker Compose example at infra/docker/worker.compose.example.yml"
|
|
35
35
|
]
|
|
36
36
|
}
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"env": [],
|
|
10
10
|
"inject": [
|
|
11
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { FilesModule } from \"./files/files.module\";" },
|
|
12
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "FilesModule," }
|
|
11
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { FilesModule } from \"./files/files.module\";" },
|
|
12
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "FilesModule," }
|
|
13
13
|
],
|
|
14
14
|
"instructions": [
|
|
15
15
|
"Start MinIO (or configure AWS S3): docker compose -f infra/docker/docker-compose.yml -f infra/docker/minio.compose.yml up -d",
|
|
16
|
-
"Upload a file: curl -F 'file=@./some.png' localhost:
|
|
16
|
+
"Upload a file: curl -F 'file=@./some.png' localhost:5002/files",
|
|
17
17
|
"The response includes { key, url } — a presigned download URL."
|
|
18
18
|
]
|
|
19
19
|
}
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
"targetApp": "api",
|
|
6
6
|
"env": [],
|
|
7
7
|
"inject": [
|
|
8
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { JobProgressModule } from \"./progress/job-progress.module\";" },
|
|
9
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "JobProgressModule," },
|
|
10
|
-
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:worker-imports", "text": "import { ProgressProcessor } from \"../progress/progress.processor\";" },
|
|
11
|
-
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:worker-imports", "text": "import { RedisModule } from \"../redis/redis.module\";" },
|
|
12
|
-
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:worker-queues", "text": "RedisModule," },
|
|
13
|
-
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:worker-queues", "text": "BullModule.registerQueue({ name: \"progress\" })," },
|
|
14
|
-
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:worker-providers", "text": "ProgressProcessor," }
|
|
8
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { JobProgressModule } from \"./progress/job-progress.module\";" },
|
|
9
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "JobProgressModule," },
|
|
10
|
+
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:end:worker-imports", "text": "import { ProgressProcessor } from \"../progress/progress.processor\";" },
|
|
11
|
+
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:end:worker-imports", "text": "import { RedisModule } from \"../redis/redis.module\";" },
|
|
12
|
+
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:end:worker-queues", "text": "RedisModule," },
|
|
13
|
+
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:end:worker-queues", "text": "BullModule.registerQueue({ name: \"progress\" })," },
|
|
14
|
+
{ "file": "apps/api/src/jobs/worker.module.ts", "marker": "// podokit:end:worker-providers", "text": "ProgressProcessor," }
|
|
15
15
|
],
|
|
16
16
|
"instructions": [
|
|
17
17
|
"Start Redis + Postgres: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
18
18
|
"Run API and worker in separate terminals: npm run dev / npm run dev:worker -w <app>-api",
|
|
19
|
-
"Watch progress: curl -N localhost:
|
|
20
|
-
"Start a job: curl -XPOST localhost:
|
|
19
|
+
"Watch progress: curl -N localhost:5002/events/stream",
|
|
20
|
+
"Start a job: curl -XPOST localhost:5002/progress -H 'content-type: application/json' -d '{\"steps\":5}'",
|
|
21
21
|
"The stream shows job-progress events 20 -> 40 -> ... -> 100 pushed from the worker via Redis."
|
|
22
22
|
]
|
|
23
23
|
}
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"LOG_LEVEL=info"
|
|
16
16
|
],
|
|
17
17
|
"inject": [
|
|
18
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { LoggingModule } from \"./logging/logging.module\";" },
|
|
19
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "LoggingModule," }
|
|
18
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { LoggingModule } from \"./logging/logging.module\";" },
|
|
19
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "LoggingModule," }
|
|
20
20
|
],
|
|
21
21
|
"instructions": [
|
|
22
22
|
"Every HTTP request is now logged as structured JSON with a correlation id.",
|
|
23
|
-
"Make a request and watch the API logs: curl localhost:
|
|
23
|
+
"Make a request and watch the API logs: curl localhost:5002/health",
|
|
24
24
|
"Set LOG_LEVEL (debug|info|warn|error) in .env. Logs are pretty in dev and JSON when NODE_ENV=production.",
|
|
25
25
|
"To route Nest's own logs through pino too, add to main.ts: NestFactory.create(AppModule, { bufferLogs: true }) then app.useLogger(app.get(Logger))."
|
|
26
26
|
]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
# MinIO for local S3-compatible object storage (
|
|
2
|
-
#
|
|
1
|
+
# MinIO for local S3-compatible object storage — development only (in production
|
|
2
|
+
# point S3_* at a real S3-compatible service). Part of the `dev` profile; add it with:
|
|
3
3
|
#
|
|
4
|
-
# docker compose -f infra/docker/docker-compose.yml -f infra/docker/minio.compose.yml up -d
|
|
4
|
+
# docker compose -f infra/docker/docker-compose.yml -f infra/docker/minio.compose.yml --profile dev up -d
|
|
5
5
|
#
|
|
6
6
|
# Console: http://localhost:9001 (S3 API: http://localhost:9000)
|
|
7
7
|
services:
|
|
8
8
|
minio:
|
|
9
9
|
image: minio/minio:latest
|
|
10
|
+
profiles: ["dev"]
|
|
10
11
|
command: server /data --console-address ":9001"
|
|
11
12
|
environment:
|
|
12
13
|
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-podokit}
|
|
@@ -20,6 +21,7 @@ services:
|
|
|
20
21
|
# Waits for MinIO, then creates the bucket and exits.
|
|
21
22
|
minio-init:
|
|
22
23
|
image: minio/mc:latest
|
|
24
|
+
profiles: ["dev"]
|
|
23
25
|
depends_on:
|
|
24
26
|
- minio
|
|
25
27
|
entrypoint: >
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"S3_FORCE_PATH_STYLE=true"
|
|
19
19
|
],
|
|
20
20
|
"inject": [
|
|
21
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { StorageModule } from \"./storage/storage.module\";" },
|
|
22
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "StorageModule," }
|
|
21
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { StorageModule } from \"./storage/storage.module\";" },
|
|
22
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "StorageModule," }
|
|
23
23
|
],
|
|
24
24
|
"instructions": [
|
|
25
25
|
"Start MinIO for local dev: docker compose -f infra/docker/docker-compose.yml -f infra/docker/minio.compose.yml up -d",
|
|
26
|
-
"Store an object: curl -XPUT localhost:
|
|
27
|
-
"Read it back: curl localhost:
|
|
28
|
-
"Presigned URL: curl localhost:
|
|
26
|
+
"Store an object: curl -XPUT localhost:5002/storage/hello -H 'content-type: application/json' -d '{\"content\":\"hi\"}'",
|
|
27
|
+
"Read it back: curl localhost:5002/storage/hello",
|
|
28
|
+
"Presigned URL: curl localhost:5002/storage/hello/presigned",
|
|
29
29
|
"For AWS S3: set STORAGE_PROVIDER=aws, remove S3_ENDPOINT, set S3_FORCE_PATH_STYLE=false and real credentials/bucket."
|
|
30
30
|
]
|
|
31
31
|
}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"RATE_LIMIT_MAX=100"
|
|
14
14
|
],
|
|
15
15
|
"inject": [
|
|
16
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { RateLimitModule } from \"./rate-limit/rate-limit.module\";" },
|
|
17
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "RateLimitModule," }
|
|
16
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { RateLimitModule } from \"./rate-limit/rate-limit.module\";" },
|
|
17
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "RateLimitModule," }
|
|
18
18
|
],
|
|
19
19
|
"instructions": [
|
|
20
20
|
"Ensure Redis is running: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"# redis (REDIS_HOST / REDIS_PORT already defined above)"
|
|
11
11
|
],
|
|
12
12
|
"inject": [
|
|
13
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { RedisModule } from \"./redis/redis.module\";" },
|
|
14
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "RedisModule," }
|
|
13
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { RedisModule } from \"./redis/redis.module\";" },
|
|
14
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "RedisModule," }
|
|
15
15
|
],
|
|
16
16
|
"instructions": [
|
|
17
17
|
"Start Redis: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
18
|
-
"Set a value: curl -XPUT localhost:
|
|
19
|
-
"Read it back: curl localhost:
|
|
18
|
+
"Set a value: curl -XPUT localhost:5002/cache/greeting -H 'content-type: application/json' -d '{\"value\":\"hi\",\"ttl\":60}'",
|
|
19
|
+
"Read it back: curl localhost:5002/cache/greeting"
|
|
20
20
|
]
|
|
21
21
|
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"targetApp": "api",
|
|
6
6
|
"env": [],
|
|
7
7
|
"inject": [
|
|
8
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { EventsModule } from \"./events/events.module\";" },
|
|
9
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "EventsModule," }
|
|
8
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { EventsModule } from \"./events/events.module\";" },
|
|
9
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "EventsModule," }
|
|
10
10
|
],
|
|
11
11
|
"instructions": [
|
|
12
|
-
"Stream events: curl -N localhost:
|
|
13
|
-
"Publish a message (in another terminal): curl -XPOST localhost:
|
|
12
|
+
"Stream events: curl -N localhost:5002/events/stream",
|
|
13
|
+
"Publish a message (in another terminal): curl -XPOST localhost:5002/events -H 'content-type: application/json' -d '{\"message\":\"hello\"}'",
|
|
14
14
|
"Inject EventsService anywhere to push updates (e.g. job progress)."
|
|
15
15
|
]
|
|
16
16
|
}
|
|
@@ -22,8 +22,8 @@ docker compose -f infra/docker/docker-compose.yml up -d
|
|
|
22
22
|
{{packageManager}} run dev
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
- API: http://localhost:
|
|
26
|
-
- Web: http://localhost:
|
|
25
|
+
- API: http://localhost:5002 — health at `/health`, docs at `/api-docs`
|
|
26
|
+
- Web: http://localhost:5001
|
|
27
27
|
|
|
28
28
|
## Database & migrations
|
|
29
29
|
|
|
@@ -5,7 +5,8 @@ import { validateEnv } from "./config/env.validation";
|
|
|
5
5
|
import { dataSourceOptions } from "./database/data-source";
|
|
6
6
|
import { HealthModule } from "./health/health.module";
|
|
7
7
|
import { TodosModule } from "./todos/todos.module";
|
|
8
|
-
// podokit:imports
|
|
8
|
+
// podokit:begin:imports
|
|
9
|
+
// podokit:end:imports
|
|
9
10
|
|
|
10
11
|
@Module({
|
|
11
12
|
imports: [
|
|
@@ -13,10 +14,12 @@ import { TodosModule } from "./todos/todos.module";
|
|
|
13
14
|
TypeOrmModule.forRoot(dataSourceOptions),
|
|
14
15
|
HealthModule,
|
|
15
16
|
TodosModule,
|
|
16
|
-
// podokit:module-imports
|
|
17
|
+
// podokit:begin:module-imports
|
|
18
|
+
// podokit:end:module-imports
|
|
17
19
|
],
|
|
18
20
|
providers: [
|
|
19
|
-
// podokit:providers
|
|
21
|
+
// podokit:begin:providers
|
|
22
|
+
// podokit:end:providers
|
|
20
23
|
],
|
|
21
24
|
})
|
|
22
25
|
export class AppModule {}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readonly code: string,
|
|
6
|
-
message: string,
|
|
7
|
-
readonly statusCode = 400,
|
|
8
|
-
) {
|
|
9
|
-
super(message);
|
|
10
|
-
this.name = "AppException";
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
// AppException is a shared contract (stable, language-independent error `code`
|
|
2
|
+
// the frontend branches on). Re-exported from @podosoft/podokit-contracts so the
|
|
3
|
+
// backend and any other layer reference one definition.
|
|
4
|
+
export { AppException } from "@podosoft/podokit-contracts";
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
// Schema-validated environment. Fails fast at boot if something is wrong.
|
|
4
4
|
const schema = z.object({
|
|
5
5
|
NODE_ENV: z.string().default("development"),
|
|
6
|
-
PORT: z.coerce.number().default(
|
|
6
|
+
PORT: z.coerce.number().default(5002),
|
|
7
7
|
CORS_ORIGIN: z.string().optional(),
|
|
8
8
|
POSTGRES_HOST: z.string().default("localhost"),
|
|
9
9
|
POSTGRES_PORT: z.coerce.number().default(5432),
|