@podosoft/podokit 0.4.0 → 0.5.0
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 +2 -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 +52 -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/lockfile.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_OWNED_GLOBS = exports.LOCK_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.manifestPath = manifestPath;
|
|
5
|
+
exports.filesLockPath = filesLockPath;
|
|
6
|
+
exports.readManifest = readManifest;
|
|
7
|
+
exports.writeManifest = writeManifest;
|
|
8
|
+
exports.writeFilesLock = writeFilesLock;
|
|
9
|
+
exports.podokitVersion = podokitVersion;
|
|
10
|
+
exports.matchGlob = matchGlob;
|
|
11
|
+
exports.classifyTier = classifyTier;
|
|
12
|
+
exports.walkFiles = walkFiles;
|
|
13
|
+
exports.computeFilesLock = computeFilesLock;
|
|
14
|
+
exports.readFilesLock = readFilesLock;
|
|
15
|
+
exports.computeDrift = computeDrift;
|
|
16
|
+
exports.initLockfile = initLockfile;
|
|
17
|
+
exports.recordModules = recordModules;
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = require("node:path");
|
|
20
|
+
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
21
|
+
/**
|
|
22
|
+
* The generation lockfile (`.podokit/`) records how a project was assembled so
|
|
23
|
+
* `podo update` can rebuild the same inputs and detect user edits. See
|
|
24
|
+
* ADR-0009 (update mechanism) and ADR-0010 (ownership model).
|
|
25
|
+
*/
|
|
26
|
+
exports.LOCK_SCHEMA_VERSION = 1;
|
|
27
|
+
const PODOKIT_DIR = ".podokit";
|
|
28
|
+
const MANIFEST_FILE = "manifest.json";
|
|
29
|
+
const FILES_LOCK_FILE = "files.lock";
|
|
30
|
+
/** Marker that flags a file as assembled from base + module injections. */
|
|
31
|
+
const INJECTION_MARKER = "// podokit:";
|
|
32
|
+
/** Directories that never belong in the lockfile (generated / vendored). */
|
|
33
|
+
const WALK_IGNORE = new Set([
|
|
34
|
+
"node_modules",
|
|
35
|
+
".git",
|
|
36
|
+
PODOKIT_DIR,
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
".svelte-kit",
|
|
40
|
+
".turbo",
|
|
41
|
+
"coverage",
|
|
42
|
+
]);
|
|
43
|
+
/** Default user-owned globs seeded into a new project. See ADR-0010. */
|
|
44
|
+
exports.DEFAULT_OWNED_GLOBS = [
|
|
45
|
+
"apps/web/src/routes/**",
|
|
46
|
+
"apps/web/src/lib/components/ui/**",
|
|
47
|
+
];
|
|
48
|
+
const TEXT_EXT = /\.(json|md|ts|tsx|js|mjs|cjs|css|html|yml|yaml|txt|env|svelte|gitignore|example)$/i;
|
|
49
|
+
function podokitDir(projectRoot) {
|
|
50
|
+
return (0, node_path_1.join)(projectRoot, PODOKIT_DIR);
|
|
51
|
+
}
|
|
52
|
+
function manifestPath(projectRoot) {
|
|
53
|
+
return (0, node_path_1.join)(podokitDir(projectRoot), MANIFEST_FILE);
|
|
54
|
+
}
|
|
55
|
+
function filesLockPath(projectRoot) {
|
|
56
|
+
return (0, node_path_1.join)(podokitDir(projectRoot), FILES_LOCK_FILE);
|
|
57
|
+
}
|
|
58
|
+
function readManifest(projectRoot) {
|
|
59
|
+
const file = manifestPath(projectRoot);
|
|
60
|
+
return (0, node_fs_1.existsSync)(file) ? JSON.parse((0, node_fs_1.readFileSync)(file, "utf8")) : null;
|
|
61
|
+
}
|
|
62
|
+
function writeManifest(projectRoot, manifest) {
|
|
63
|
+
(0, node_fs_1.mkdirSync)(podokitDir(projectRoot), { recursive: true });
|
|
64
|
+
(0, node_fs_1.writeFileSync)(manifestPath(projectRoot), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
65
|
+
}
|
|
66
|
+
function writeFilesLock(projectRoot, lock) {
|
|
67
|
+
(0, node_fs_1.mkdirSync)(podokitDir(projectRoot), { recursive: true });
|
|
68
|
+
(0, node_fs_1.writeFileSync)(filesLockPath(projectRoot), `${JSON.stringify(lock, null, 2)}\n`);
|
|
69
|
+
}
|
|
70
|
+
/** Read the PodoKit CLI's own version to stamp into the manifest. */
|
|
71
|
+
function podokitVersion() {
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf8"));
|
|
74
|
+
return pkg.version ?? "0.0.0";
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return "0.0.0";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Match a POSIX relative path against a simple glob supporting `*` and `**`. */
|
|
81
|
+
function matchGlob(path, glob) {
|
|
82
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
// Single pass so replacement output is never re-scanned.
|
|
84
|
+
const pattern = escaped.replace(/\/\*\*$|\*\*\/|\*\*|\*/g, (token) => {
|
|
85
|
+
if (token === "/**")
|
|
86
|
+
return "(?:/.*)?"; // trailing `/**`: the dir itself or anything under it
|
|
87
|
+
if (token === "**/")
|
|
88
|
+
return "(?:.*/)?"; // `**/`: zero or more leading segments
|
|
89
|
+
if (token === "**")
|
|
90
|
+
return ".*"; // `**`: any characters
|
|
91
|
+
return "[^/]*"; // `*`: within a single segment
|
|
92
|
+
});
|
|
93
|
+
return new RegExp(`^${pattern}$`).test(path);
|
|
94
|
+
}
|
|
95
|
+
function isTextPath(relPath) {
|
|
96
|
+
return relPath.startsWith("dot-") || TEXT_EXT.test(relPath);
|
|
97
|
+
}
|
|
98
|
+
/** Classify a file's ownership tier. See ADR-0010 priority order. */
|
|
99
|
+
function classifyTier(relPath, content, ownedGlobs) {
|
|
100
|
+
if (ownedGlobs.some((glob) => matchGlob(relPath, glob)))
|
|
101
|
+
return "owned";
|
|
102
|
+
if (isTextPath(relPath) && content.toString("utf8").includes(INJECTION_MARKER))
|
|
103
|
+
return "assembled";
|
|
104
|
+
return "managed";
|
|
105
|
+
}
|
|
106
|
+
/** List project files as POSIX-relative paths, skipping generated/vendored dirs. */
|
|
107
|
+
function walkFiles(projectRoot, prefix = "") {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const entry of (0, node_fs_1.readdirSync)((0, node_path_1.join)(projectRoot, prefix))) {
|
|
110
|
+
if (WALK_IGNORE.has(entry))
|
|
111
|
+
continue;
|
|
112
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
113
|
+
if ((0, node_fs_1.statSync)((0, node_path_1.join)(projectRoot, rel)).isDirectory()) {
|
|
114
|
+
out.push(...walkFiles(projectRoot, rel));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
out.push(rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** Recompute the files.lock by hashing and classifying every project file. */
|
|
123
|
+
function computeFilesLock(projectRoot, ownedGlobs) {
|
|
124
|
+
const files = {};
|
|
125
|
+
for (const rel of walkFiles(projectRoot).sort()) {
|
|
126
|
+
const content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, rel));
|
|
127
|
+
files[rel] = { tier: classifyTier(rel, content, ownedGlobs), outHash: (0, podokit_template_engine_1.hashContent)(content) };
|
|
128
|
+
}
|
|
129
|
+
return { schemaVersion: exports.LOCK_SCHEMA_VERSION, files };
|
|
130
|
+
}
|
|
131
|
+
function readFilesLock(projectRoot) {
|
|
132
|
+
const file = filesLockPath(projectRoot);
|
|
133
|
+
return (0, node_fs_1.existsSync)(file) ? JSON.parse((0, node_fs_1.readFileSync)(file, "utf8")) : null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Compare the current disk against the lock: report managed/assembled files the
|
|
137
|
+
* user has edited (hash mismatch) or deleted. Owned files are never reported —
|
|
138
|
+
* they belong to the user. Returns empty drift when there is no lock.
|
|
139
|
+
*/
|
|
140
|
+
function computeDrift(projectRoot) {
|
|
141
|
+
const lock = readFilesLock(projectRoot);
|
|
142
|
+
const drifted = [];
|
|
143
|
+
const missing = [];
|
|
144
|
+
if (!lock)
|
|
145
|
+
return { drifted, missing };
|
|
146
|
+
for (const [rel, entry] of Object.entries(lock.files)) {
|
|
147
|
+
if (entry.tier === "owned")
|
|
148
|
+
continue;
|
|
149
|
+
const abs = (0, node_path_1.join)(projectRoot, rel);
|
|
150
|
+
if (!(0, node_fs_1.existsSync)(abs)) {
|
|
151
|
+
missing.push(rel);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if ((0, podokit_template_engine_1.hashContent)((0, node_fs_1.readFileSync)(abs)) !== entry.outHash)
|
|
155
|
+
drifted.push(rel);
|
|
156
|
+
}
|
|
157
|
+
return { drifted: drifted.sort(), missing: missing.sort() };
|
|
158
|
+
}
|
|
159
|
+
/** Write the initial `.podokit/` lockfile for a freshly created project. */
|
|
160
|
+
function initLockfile(projectRoot, options) {
|
|
161
|
+
const ownedGlobs = options.ownedGlobs ?? exports.DEFAULT_OWNED_GLOBS;
|
|
162
|
+
const manifest = {
|
|
163
|
+
schemaVersion: exports.LOCK_SCHEMA_VERSION,
|
|
164
|
+
podokitVersion: options.version ?? podokitVersion(),
|
|
165
|
+
template: options.template,
|
|
166
|
+
packageManager: options.packageManager,
|
|
167
|
+
answers: options.answers,
|
|
168
|
+
modules: [],
|
|
169
|
+
ownedGlobs,
|
|
170
|
+
};
|
|
171
|
+
writeManifest(projectRoot, manifest);
|
|
172
|
+
writeFilesLock(projectRoot, computeFilesLock(projectRoot, ownedGlobs));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Record applied modules in the manifest and recompute the files.lock. Modules
|
|
176
|
+
* already present are left in place (idempotent); the lock always reflects disk.
|
|
177
|
+
* No-op on projects without a manifest (pre-lockfile projects; see `podo adopt`).
|
|
178
|
+
*/
|
|
179
|
+
function recordModules(projectRoot, moduleNames, version) {
|
|
180
|
+
const manifest = readManifest(projectRoot);
|
|
181
|
+
if (!manifest)
|
|
182
|
+
return;
|
|
183
|
+
const stampedWith = version ?? podokitVersion();
|
|
184
|
+
const known = new Set(manifest.modules.map((m) => m.name));
|
|
185
|
+
for (const name of moduleNames) {
|
|
186
|
+
if (known.has(name))
|
|
187
|
+
continue;
|
|
188
|
+
manifest.modules.push({ name, order: manifest.modules.length, addedWith: stampedWith });
|
|
189
|
+
known.add(name);
|
|
190
|
+
}
|
|
191
|
+
writeManifest(projectRoot, manifest);
|
|
192
|
+
writeFilesLock(projectRoot, computeFilesLock(projectRoot, manifest.ownedGlobs));
|
|
193
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# Copy to .env and adjust. Never commit real secrets.
|
|
2
|
+
|
|
3
|
+
# ── Required (runtime) ───────────────────────────────────────────────
|
|
2
4
|
POSTGRES_HOST=localhost
|
|
3
5
|
POSTGRES_PORT=5432
|
|
4
6
|
POSTGRES_USER=podokit
|
|
5
7
|
POSTGRES_PASSWORD=podokit
|
|
6
8
|
POSTGRES_DB=podokit
|
|
7
9
|
|
|
10
|
+
APP_BASE_URL=http://localhost:5001
|
|
11
|
+
API_BASE_URL=http://localhost:5002
|
|
12
|
+
BACKEND_INTERNAL_URL=http://localhost:5002
|
|
13
|
+
CORS_ORIGIN=http://localhost:5001
|
|
14
|
+
PORT=5002
|
|
15
|
+
|
|
16
|
+
# ── Optional / development ───────────────────────────────────────────
|
|
17
|
+
# Redis — only needed by cache/queue modules. Start it with:
|
|
18
|
+
# docker compose --profile cache up
|
|
8
19
|
REDIS_HOST=localhost
|
|
9
20
|
REDIS_PORT=6379
|
|
10
|
-
|
|
11
|
-
APP_BASE_URL=http://localhost:5173
|
|
12
|
-
API_BASE_URL=http://localhost:3000
|
|
13
|
-
BACKEND_INTERNAL_URL=http://localhost:3000
|
|
14
|
-
CORS_ORIGIN=http://localhost:5173
|
|
15
|
-
PORT=3000
|
|
@@ -18,8 +18,8 @@ docker compose -f infra/docker/docker-compose.yml up -d
|
|
|
18
18
|
{{packageManager}} run dev
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
- API: http://localhost:
|
|
22
|
-
- Web: http://localhost:
|
|
21
|
+
- API: http://localhost:5002 — health at `/health`, docs at `/api-docs`
|
|
22
|
+
- Web: http://localhost:5001
|
|
23
23
|
|
|
24
24
|
## Database & migrations
|
|
25
25
|
|
|
@@ -4,17 +4,20 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
|
|
4
4
|
import { validateEnv } from "./config/env.validation";
|
|
5
5
|
import { dataSourceOptions } from "./database/data-source";
|
|
6
6
|
import { HealthModule } from "./health/health.module";
|
|
7
|
-
// podokit:imports
|
|
7
|
+
// podokit:begin:imports
|
|
8
|
+
// podokit:end:imports
|
|
8
9
|
|
|
9
10
|
@Module({
|
|
10
11
|
imports: [
|
|
11
12
|
ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }),
|
|
12
13
|
TypeOrmModule.forRoot(dataSourceOptions),
|
|
13
14
|
HealthModule,
|
|
14
|
-
// podokit:module-imports
|
|
15
|
+
// podokit:begin:module-imports
|
|
16
|
+
// podokit:end:module-imports
|
|
15
17
|
],
|
|
16
18
|
providers: [
|
|
17
|
-
// podokit:providers
|
|
19
|
+
// podokit:begin:providers
|
|
20
|
+
// podokit:end:providers
|
|
18
21
|
],
|
|
19
22
|
})
|
|
20
23
|
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),
|
|
@@ -22,7 +22,7 @@ async function bootstrap(): Promise<void> {
|
|
|
22
22
|
.build();
|
|
23
23
|
SwaggerModule.setup("api-docs", app, SwaggerModule.createDocument(app, config));
|
|
24
24
|
|
|
25
|
-
const port = Number(process.env.PORT ??
|
|
25
|
+
const port = Number(process.env.PORT ?? 5002);
|
|
26
26
|
await app.listen(port);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "vite dev --port
|
|
7
|
+
"dev": "vite dev --port 5001",
|
|
8
8
|
"build": "vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
10
|
"lint": "svelte-check --tsconfig ./tsconfig.json",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@internationalized/date": "^3.12.2",
|
|
15
15
|
"@lucide/svelte": "^1.23.0",
|
|
16
|
-
"@podosoft/podokit-api-client": "^0.
|
|
16
|
+
"@podosoft/podokit-api-client": "^0.5.0",
|
|
17
17
|
"bits-ui": "^2.18.1",
|
|
18
18
|
"clsx": "^2.1.1",
|
|
19
19
|
"mode-watcher": "^1.1.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{preventScroll}
|
|
28
28
|
data-slot="select-content"
|
|
29
29
|
class={cn(
|
|
30
|
-
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto",
|
|
30
|
+
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-96 overflow-x-hidden overflow-y-auto",
|
|
31
31
|
className
|
|
32
32
|
)}
|
|
33
33
|
{...restProps}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{#snippet children({ selected, highlighted })}
|
|
27
27
|
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
|
28
28
|
{#if selected}
|
|
29
|
-
<CheckIcon class="
|
|
29
|
+
<CheckIcon class="size-3.5" />
|
|
30
30
|
{/if}
|
|
31
31
|
</span>
|
|
32
32
|
<span class="flex flex-1 gap-2 shrink-0 whitespace-nowrap">
|
package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Switch as SwitchPrimitive } from "bits-ui";
|
|
3
|
+
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
checked = $bindable(false),
|
|
8
|
+
class: className,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<SwitchPrimitive.Root
|
|
14
|
+
bind:ref
|
|
15
|
+
data-slot="switch"
|
|
16
|
+
class={cn(
|
|
17
|
+
"peer focus-visible:border-ring focus-visible:ring-ring/50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
bind:checked
|
|
21
|
+
{...restProps}
|
|
22
|
+
>
|
|
23
|
+
<SwitchPrimitive.Thumb
|
|
24
|
+
data-slot="switch-thumb"
|
|
25
|
+
class={cn(
|
|
26
|
+
"bg-background pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
</SwitchPrimitive.Root>
|
|
@@ -5,7 +5,7 @@ const FORWARDED_HEADERS = ["authorization", "cookie", "content-type", "accept",
|
|
|
5
5
|
const RELAYED_RESPONSE_HEADERS = ["content-type", "location", "cache-control"];
|
|
6
6
|
|
|
7
7
|
export function backendBaseUrl(): string {
|
|
8
|
-
return process.env.BACKEND_INTERNAL_URL ?? "http://localhost:
|
|
8
|
+
return process.env.BACKEND_INTERNAL_URL ?? "http://localhost:5002";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// Normalize the resolved client IP for the forwarded header. better-auth stores
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Copy to .env and adjust. Never commit real secrets.
|
|
2
|
+
|
|
3
|
+
# ── Required (runtime) ───────────────────────────────────────────────
|
|
2
4
|
NODE_ENV=development
|
|
3
|
-
PORT=
|
|
5
|
+
PORT=5002
|
|
4
6
|
|
|
5
7
|
POSTGRES_HOST=localhost
|
|
6
8
|
POSTGRES_PORT=5432
|
|
@@ -8,9 +10,12 @@ POSTGRES_USER=podokit
|
|
|
8
10
|
POSTGRES_PASSWORD=podokit
|
|
9
11
|
POSTGRES_DB=podokit
|
|
10
12
|
|
|
13
|
+
# Web -> API boundary (server-side only; not exposed to the browser)
|
|
14
|
+
BACKEND_INTERNAL_URL=http://localhost:5002
|
|
15
|
+
CORS_ORIGIN=http://localhost:5001
|
|
16
|
+
|
|
17
|
+
# ── Optional / development ───────────────────────────────────────────
|
|
18
|
+
# Redis — only needed by cache/queue modules (redis, bullmq, rate-limit,
|
|
19
|
+
# job-progress, sse). Start it with: docker compose --profile cache up
|
|
11
20
|
REDIS_HOST=localhost
|
|
12
21
|
REDIS_PORT=6379
|
|
13
|
-
|
|
14
|
-
# Web -> API boundary (server-side only; not exposed to the browser)
|
|
15
|
-
BACKEND_INTERNAL_URL=http://localhost:3000
|
|
16
|
-
CORS_ORIGIN=http://localhost:5173
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
services:
|
|
2
|
+
# --- Runtime (always started by `docker compose up`) ---
|
|
2
3
|
postgres:
|
|
3
4
|
image: postgres:16-alpine
|
|
4
5
|
environment:
|
|
@@ -15,8 +16,11 @@ services:
|
|
|
15
16
|
timeout: 5s
|
|
16
17
|
retries: 5
|
|
17
18
|
|
|
19
|
+
# --- Runtime, but only needed by cache/queue modules (redis, bullmq,
|
|
20
|
+
# rate-limit, job-progress, sse). Start with: docker compose --profile cache up
|
|
18
21
|
redis:
|
|
19
22
|
image: redis:7-alpine
|
|
23
|
+
profiles: ["cache"]
|
|
20
24
|
ports:
|
|
21
25
|
- "${REDIS_PORT:-6379}:6379"
|
|
22
26
|
healthcheck:
|
|
@@ -25,5 +29,38 @@ services:
|
|
|
25
29
|
timeout: 5s
|
|
26
30
|
retries: 5
|
|
27
31
|
|
|
32
|
+
# --- Development / testing tools (NOT for production) ---
|
|
33
|
+
# Start with: docker compose --profile dev up
|
|
34
|
+
# Local email catcher: SMTP on 1025, web UI + REST API on 8025 (http://localhost:8025).
|
|
35
|
+
mailpit:
|
|
36
|
+
image: axllent/mailpit:latest
|
|
37
|
+
profiles: ["dev"]
|
|
38
|
+
ports:
|
|
39
|
+
- "${SMTP_PORT:-1025}:1025"
|
|
40
|
+
- "${MAILPIT_UI_PORT:-8025}:8025"
|
|
41
|
+
healthcheck:
|
|
42
|
+
test: ["CMD", "wget", "-qO-", "http://localhost:8025/readyz"]
|
|
43
|
+
interval: 5s
|
|
44
|
+
timeout: 5s
|
|
45
|
+
retries: 5
|
|
46
|
+
|
|
47
|
+
# Local SMS catcher (the "Mailpit for SMS"): the app posts outgoing SMS here via
|
|
48
|
+
# SMS_WEBHOOK_URL and tests read them over REST. Development only.
|
|
49
|
+
sms-sink:
|
|
50
|
+
image: node:20-alpine
|
|
51
|
+
profiles: ["dev"]
|
|
52
|
+
command: ["node", "/app/sms-sink.mjs"]
|
|
53
|
+
environment:
|
|
54
|
+
PORT: "8095"
|
|
55
|
+
ports:
|
|
56
|
+
- "${SMS_SINK_PORT:-8095}:8095"
|
|
57
|
+
volumes:
|
|
58
|
+
- ./sms-sink.mjs:/app/sms-sink.mjs:ro
|
|
59
|
+
healthcheck:
|
|
60
|
+
test: ["CMD", "wget", "-qO-", "http://localhost:8095/readyz"]
|
|
61
|
+
interval: 5s
|
|
62
|
+
timeout: 5s
|
|
63
|
+
retries: 5
|
|
64
|
+
|
|
28
65
|
volumes:
|
|
29
66
|
pgdata:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Dev/test SMS sink — the "Mailpit for SMS". The app posts outgoing SMS here
|
|
2
|
+
// (via SMS_WEBHOOK_URL) instead of a real provider, and tests read them back
|
|
3
|
+
// over REST. Development only; never runs in production (see the `dev` profile
|
|
4
|
+
// in docker-compose.yml). Uses only Node built-ins, run on a stock node image.
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
|
|
7
|
+
const PORT = Number(process.env.PORT ?? 8095);
|
|
8
|
+
/** @type {{to: string, body: string, receivedAt: string}[]} */
|
|
9
|
+
let messages = [];
|
|
10
|
+
|
|
11
|
+
const send = (res, status, body) => {
|
|
12
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
13
|
+
res.end(JSON.stringify(body));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
createServer((req, res) => {
|
|
17
|
+
const { method, url } = req;
|
|
18
|
+
if (method === "GET" && url === "/readyz") return send(res, 200, { status: "ok" });
|
|
19
|
+
if (method === "GET" && url === "/messages") return send(res, 200, messages);
|
|
20
|
+
if (method === "DELETE" && url === "/messages") {
|
|
21
|
+
messages = [];
|
|
22
|
+
return send(res, 200, { cleared: true });
|
|
23
|
+
}
|
|
24
|
+
if (method === "POST" && url === "/sms") {
|
|
25
|
+
let raw = "";
|
|
26
|
+
req.on("data", (c) => (raw += c));
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
let payload = {};
|
|
29
|
+
try {
|
|
30
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
31
|
+
} catch {
|
|
32
|
+
payload = { body: raw };
|
|
33
|
+
}
|
|
34
|
+
messages.unshift({ to: payload.to ?? payload.phoneNumber ?? "", body: payload.body ?? payload.code ?? raw, receivedAt: new Date().toISOString() });
|
|
35
|
+
send(res, 202, { accepted: true });
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
send(res, 404, { error: "not found" });
|
|
40
|
+
}).listen(PORT, () => console.log(`[sms-sink] listening on ${PORT}`));
|
|
@@ -10,6 +10,6 @@ npm run test:e2e:report # open the HTML report
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
The suite runs against a live stack (web on `E2E_BASE_URL`, default
|
|
13
|
-
`http://localhost:
|
|
13
|
+
`http://localhost:5001`, proxying `/api/*` to the API). Start PostgreSQL, run the
|
|
14
14
|
auth migration, and start the API + web first. Files ending in `*.api.spec.ts` run
|
|
15
15
|
in the request-only `api` project; `*.ui.spec.ts` run in the chromium `ui` project.
|
|
@@ -4,7 +4,7 @@ import { defineConfig, devices } from "@playwright/test";
|
|
|
4
4
|
// the API). Start it first (see docs/testing.md) — locally via the dev harness,
|
|
5
5
|
// in CI via scripts/e2e-ci.mjs. Two projects: `api` (request-only, backend e2e)
|
|
6
6
|
// and `ui` (chromium, pages). Files are routed by suffix (*.api/*.ui.spec.ts).
|
|
7
|
-
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:
|
|
7
|
+
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
8
8
|
|
|
9
9
|
export default defineConfig({
|
|
10
10
|
testDir: ".",
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
import UsersIcon from "@lucide/svelte/icons/users";
|
|
7
7
|
import MonitorSmartphoneIcon from "@lucide/svelte/icons/monitor-smartphone";
|
|
8
8
|
import ScrollTextIcon from "@lucide/svelte/icons/scroll-text";
|
|
9
|
+
import Building2Icon from "@lucide/svelte/icons/building-2";
|
|
10
|
+
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
9
11
|
import { getI18n } from "$lib/i18n";
|
|
10
12
|
import type { SessionUser } from "../../app.d.ts";
|
|
11
13
|
import type { Messages } from "$lib/i18n/messages";
|
|
12
14
|
import type { Component } from "svelte";
|
|
13
15
|
|
|
14
|
-
let { user, capabilities }: { user: SessionUser; capabilities?: { auditLog?: boolean } } = $props();
|
|
16
|
+
let { user, capabilities }: { user: SessionUser; capabilities?: { auditLog?: boolean; organization?: boolean } } = $props();
|
|
15
17
|
const i18n = getI18n();
|
|
16
18
|
|
|
17
19
|
type NavItem = { href: string; key: keyof Messages["nav"]; icon: Component; adminOnly?: boolean };
|
|
@@ -19,9 +21,13 @@
|
|
|
19
21
|
{ href: "/admin", key: "overview", icon: LayoutDashboardIcon },
|
|
20
22
|
{ href: "/admin/users", key: "users", icon: UsersIcon, adminOnly: true },
|
|
21
23
|
{ href: "/admin/sessions", key: "sessions", icon: MonitorSmartphoneIcon, adminOnly: true },
|
|
24
|
+
...(capabilities?.organization
|
|
25
|
+
? [{ href: "/admin/organizations", key: "organizations", icon: Building2Icon, adminOnly: true } as NavItem]
|
|
26
|
+
: []),
|
|
22
27
|
...(capabilities?.auditLog
|
|
23
28
|
? [{ href: "/admin/audit", key: "audit", icon: ScrollTextIcon, adminOnly: true } as NavItem]
|
|
24
29
|
: []),
|
|
30
|
+
{ href: "/admin/settings", key: "settings", icon: SettingsIcon, adminOnly: true },
|
|
25
31
|
]);
|
|
26
32
|
const visible = $derived(items.filter((item) => !item.adminOnly || user.role === "admin"));
|
|
27
33
|
</script>
|