@podosoft/podokit 0.12.4 → 0.14.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/dist/add.d.ts +27 -1
- package/dist/add.js +3 -1
- package/dist/index.js +6 -3
- package/dist/lib.d.ts +1 -1
- package/dist/lockfile.d.ts +4 -1
- package/dist/lockfile.js +4 -3
- package/dist/module-migrations.d.ts +25 -0
- package/dist/module-migrations.js +233 -0
- package/dist/templates/fullstack-nest-svelte/Dockerfile.dev +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +3 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +5 -0
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +6 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +2 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +1 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image-upload.interceptor.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.controller.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.module.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +43 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +110 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.spec.ts +79 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.validation.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +155 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +109 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +21 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +21 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +78 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/+layout.server.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/+layout.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.server.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/+page.svelte +73 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +2 -47
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +15 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +4 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +44 -4
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +10 -10
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +96 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +70 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +18 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -9
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +36 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +77 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sidebar.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +3 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +4 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/verify-email.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/module.manifest.json +74 -11
- package/dist/templates/modules/audit-log/module.manifest.json +1 -1
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +3 -6
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +6 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +12 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.spec.ts +42 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/user-delete-handlers.ts +27 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +55 -3
- package/dist/templates/modules/auth/module.manifest.json +5 -4
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +1 -1
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/Dockerfile.dev +3 -1
- package/dist/templates/todo/apps/api/package.json +1 -1
- package/dist/templates/todo/apps/web/package.json +1 -1
- package/dist/templates/todo/compose.dev.yaml +6 -0
- package/dist/update.d.ts +8 -1
- package/dist/update.js +68 -12
- package/package.json +1 -1
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/{app-sidebar.svelte → admin-sidebar.svelte} +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/account/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/audit/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/organizations/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/sessions/+page.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/settings/general-settings.svelte +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.server.ts +0 -0
- /package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/{(app) → (admin)}/admin/users/+page.svelte +0 -0
|
@@ -2,7 +2,17 @@ import { BadRequestException, Injectable } from "@nestjs/common";
|
|
|
2
2
|
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
3
|
import { Repository } from "typeorm";
|
|
4
4
|
import { AuthConfigRow } from "./auth-config.entity";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SESSION_LIFETIME_SECONDS,
|
|
7
|
+
encryptSecret,
|
|
8
|
+
envAuthConfig,
|
|
9
|
+
isSessionIdleTimeoutMinutes,
|
|
10
|
+
resolveSessionIdleTimeoutMinutes,
|
|
11
|
+
socialKey,
|
|
12
|
+
SUPPORTED_PROVIDER_IDS,
|
|
13
|
+
SUPPORTED_SOCIAL_PROVIDERS,
|
|
14
|
+
} from "@podosoft/podokit-auth";
|
|
15
|
+
import { refreshAuthNow } from "../auth/auth-provider";
|
|
6
16
|
|
|
7
17
|
/** One configured social provider as shown to the admin (no secret — just a
|
|
8
18
|
* `hasSecret` flag). */
|
|
@@ -16,7 +26,14 @@ export type AuthConfigView = {
|
|
|
16
26
|
/** Catalog of addable providers (id + display label) for the "add provider" picker. */
|
|
17
27
|
catalog: ReadonlyArray<{ id: string; label: string }>;
|
|
18
28
|
smtp: { enabled: boolean; host: string; port: number; secure: boolean; user: string; from: string; hasSecret: boolean };
|
|
19
|
-
server: {
|
|
29
|
+
server: {
|
|
30
|
+
requireEmailVerification: boolean;
|
|
31
|
+
requireSignupApproval: boolean;
|
|
32
|
+
allowDelete: boolean;
|
|
33
|
+
hibp: boolean;
|
|
34
|
+
auditLog: boolean;
|
|
35
|
+
sessionIdleTimeoutMinutes: number | null;
|
|
36
|
+
};
|
|
20
37
|
};
|
|
21
38
|
|
|
22
39
|
type ProviderUpdate = { enabled?: boolean; clientId?: string; clientSecret?: string; redirectURI?: string; delete?: boolean };
|
|
@@ -24,7 +41,14 @@ export type AuthConfigUpdate = {
|
|
|
24
41
|
/** Per-provider upsert (keyed by provider id); `{ delete: true }` removes it. */
|
|
25
42
|
social?: Record<string, ProviderUpdate>;
|
|
26
43
|
smtp?: { enabled?: boolean; host?: string; port?: number; secure?: boolean; user?: string; pass?: string; from?: string };
|
|
27
|
-
server?: {
|
|
44
|
+
server?: {
|
|
45
|
+
requireEmailVerification?: boolean;
|
|
46
|
+
requireSignupApproval?: boolean;
|
|
47
|
+
allowDelete?: boolean;
|
|
48
|
+
hibp?: boolean;
|
|
49
|
+
auditLog?: boolean;
|
|
50
|
+
sessionIdleTimeoutMinutes?: number | null;
|
|
51
|
+
};
|
|
28
52
|
};
|
|
29
53
|
|
|
30
54
|
@Injectable()
|
|
@@ -59,6 +83,7 @@ export class AuthConfigService {
|
|
|
59
83
|
allowDelete?: boolean;
|
|
60
84
|
hibp?: boolean;
|
|
61
85
|
auditLog?: boolean;
|
|
86
|
+
sessionIdleTimeoutMinutes?: number | null;
|
|
62
87
|
};
|
|
63
88
|
return {
|
|
64
89
|
social,
|
|
@@ -78,6 +103,10 @@ export class AuthConfigService {
|
|
|
78
103
|
allowDelete: serverC.allowDelete ?? env.allowDelete,
|
|
79
104
|
hibp: serverC.hibp ?? env.hibp,
|
|
80
105
|
auditLog: serverC.auditLog ?? env.auditLog,
|
|
106
|
+
sessionIdleTimeoutMinutes: resolveSessionIdleTimeoutMinutes(
|
|
107
|
+
serverC.sessionIdleTimeoutMinutes,
|
|
108
|
+
env.sessionIdleTimeoutMinutes,
|
|
109
|
+
),
|
|
81
110
|
},
|
|
82
111
|
};
|
|
83
112
|
}
|
|
@@ -90,6 +119,10 @@ export class AuthConfigService {
|
|
|
90
119
|
}
|
|
91
120
|
if (dto.smtp) await this.upsertSmtp(dto.smtp);
|
|
92
121
|
if (dto.server) await this.upsertServer(dto.server);
|
|
122
|
+
await refreshAuthNow();
|
|
123
|
+
if (dto.server?.sessionIdleTimeoutMinutes !== undefined) {
|
|
124
|
+
await this.resetSessionExpirations(dto.server.sessionIdleTimeoutMinutes);
|
|
125
|
+
}
|
|
93
126
|
return this.describe();
|
|
94
127
|
}
|
|
95
128
|
|
|
@@ -116,12 +149,31 @@ export class AuthConfigService {
|
|
|
116
149
|
}
|
|
117
150
|
|
|
118
151
|
private async upsertServer(u: NonNullable<AuthConfigUpdate["server"]>): Promise<void> {
|
|
152
|
+
if (u.sessionIdleTimeoutMinutes !== undefined && !isSessionIdleTimeoutMinutes(u.sessionIdleTimeoutMinutes)) {
|
|
153
|
+
throw new BadRequestException("Session idle timeout must be null or an integer from 5 to 10080 minutes");
|
|
154
|
+
}
|
|
119
155
|
const row = (await this.repo.findOneBy({ key: "server" })) ?? this.repo.create({ key: "server", enabled: true, config: {}, secret: null });
|
|
120
156
|
const config = { ...(row.config as Record<string, unknown>) };
|
|
121
157
|
for (const f of ["requireEmailVerification", "requireSignupApproval", "allowDelete", "hibp", "auditLog"] as const) {
|
|
122
158
|
if (u[f] !== undefined) config[f] = u[f];
|
|
123
159
|
}
|
|
160
|
+
if (u.sessionIdleTimeoutMinutes !== undefined) {
|
|
161
|
+
config.sessionIdleTimeoutMinutes = u.sessionIdleTimeoutMinutes;
|
|
162
|
+
}
|
|
124
163
|
row.config = config;
|
|
125
164
|
await this.repo.save(row);
|
|
126
165
|
}
|
|
166
|
+
|
|
167
|
+
private async resetSessionExpirations(minutes: number | null): Promise<void> {
|
|
168
|
+
const lifetimeSeconds = minutes === null
|
|
169
|
+
? DEFAULT_SESSION_LIFETIME_SECONDS
|
|
170
|
+
: minutes * 60;
|
|
171
|
+
await this.repo.manager.query(
|
|
172
|
+
`UPDATE "session"
|
|
173
|
+
SET "expiresAt" = CURRENT_TIMESTAMP + ($1 * INTERVAL '1 second'),
|
|
174
|
+
"updatedAt" = CURRENT_TIMESTAMP
|
|
175
|
+
WHERE "expiresAt" > CURRENT_TIMESTAMP`,
|
|
176
|
+
[lifetimeSeconds],
|
|
177
|
+
);
|
|
178
|
+
}
|
|
127
179
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
".claude/skills/podokit-configure-auth/**"
|
|
8
8
|
],
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@podosoft/podokit-auth": "^0.
|
|
11
|
-
"@podosoft/podokit-contracts": "^0.
|
|
10
|
+
"@podosoft/podokit-auth": "^0.3.0",
|
|
11
|
+
"@podosoft/podokit-contracts": "^0.4.0",
|
|
12
12
|
"better-auth": "^1.6.23",
|
|
13
13
|
"@thallesp/nestjs-better-auth": "^2.7.0",
|
|
14
14
|
"pg": "^8.12.0",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"# AUTH_EMAIL_VERIFICATION=false",
|
|
45
45
|
"# AUTH_REQUIRE_SIGNUP_APPROVAL=false",
|
|
46
46
|
"# AUTH_ALLOW_DELETE=false",
|
|
47
|
+
"# AUTH_SESSION_IDLE_TIMEOUT_MINUTES=30",
|
|
47
48
|
"# GOOGLE_CLIENT_ID=",
|
|
48
49
|
"# GOOGLE_CLIENT_SECRET=",
|
|
49
50
|
"# GITHUB_CLIENT_ID=",
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
"file": "AGENTS.md",
|
|
110
111
|
"marker": "<!-- podokit:end:agents-modules -->",
|
|
111
112
|
"optional": true,
|
|
112
|
-
"text": "### auth (better-auth)\n- The API is **secure by default**: every route needs a session except `/health` and `/api/auth/*`. Opt a route out with `@Public()`; read the user with `@Session()`.\n- Configure OAuth providers, SMTP, sign-up approval, and the other server toggles on the admin **Settings** page (DB-backed, applied live \u2014 not via env). Only `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are required env vars.\n- Use `npm run auth:configure -w <app>-api -- ...` and `.claude/skills/podokit-configure-auth` to apply OAuth/SMTP credentials without committing secrets. The sign-up approval gate is provider-independent; do not add provider-specific approval checks.\n- Development setup: create auth tables with `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`, then run TypeORM migrations. Production images can run both from compiled output with `npm run migrate:all`.\n"
|
|
113
|
+
"text": "### auth (better-auth)\n- The API is **secure by default**: every route needs a session except `/health` and `/api/auth/*`. Opt a route out with `@Public()`; read the user with `@Session()`.\n- Configure OAuth providers, SMTP, sign-up approval, automatic logout, and the other server toggles on the admin **Settings** page (DB-backed, applied live \u2014 not via env). Only `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are required env vars.\n- Use `npm run auth:configure -w <app>-api -- ...` and `.claude/skills/podokit-configure-auth` to apply OAuth/SMTP credentials without committing secrets. The sign-up approval gate is provider-independent; do not add provider-specific approval checks.\n- Development setup: create auth tables with `npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts`, then run TypeORM migrations. Production images can run both from compiled output with `npm run migrate:all`.\n"
|
|
113
114
|
}
|
|
114
115
|
],
|
|
115
116
|
"instructions": [
|
|
@@ -120,6 +121,6 @@
|
|
|
120
121
|
"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\"}'",
|
|
121
122
|
"Call a protected route with the session: curl -b cookies.txt localhost:5002/account/me",
|
|
122
123
|
"Use npm run auth:configure -w <app>-api -- --help for redacted OAuth, SMTP, and sign-up approval automation.",
|
|
123
|
-
"Everything is protected by default; mark public routes with @Public(). Configure OAuth providers, SMTP, and server-enforced toggles on the admin Settings page \u2014 stored
|
|
124
|
+
"Everything is protected by default; mark public routes with @Public(). Configure OAuth providers, SMTP, automatic logout, and server-enforced toggles on the admin Settings page \u2014 stored in the DB and applied live (no restart). Environment variables (GOOGLE_CLIENT_*, SMTP_*, AUTH_*) still work as an optional fallback/override."
|
|
124
125
|
]
|
|
125
126
|
}
|
|
@@ -23,7 +23,7 @@ test("file upload: multipart upload returns a key and url @smoke", async ({ play
|
|
|
23
23
|
await ctx.dispose();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
test("file upload: a request with no file is a 400
|
|
26
|
+
test("file upload: a request with no file is a 400", async ({ playwright }) => {
|
|
27
27
|
const ctx = await session(playwright);
|
|
28
28
|
const res = await ctx.post("/api/files", { multipart: { notfile: "x" } });
|
|
29
29
|
expect(res.status()).toBe(400);
|
|
@@ -10,7 +10,7 @@ async function session(playwright: import("@playwright/test").PlaywrightWorkerAr
|
|
|
10
10
|
return ctx;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
test("sse: publish accepts a message
|
|
13
|
+
test("sse: publish accepts a message", async ({ playwright }) => {
|
|
14
14
|
const ctx = await session(playwright);
|
|
15
15
|
const pub = await ctx.post("/api/events", { data: { message: `hi-${Date.now()}` } });
|
|
16
16
|
expect(pub.ok()).toBeTruthy();
|
|
@@ -17,7 +17,9 @@ WORKDIR /app
|
|
|
17
17
|
COPY package.json ./
|
|
18
18
|
COPY apps/api/package.json ./apps/api/package.json
|
|
19
19
|
COPY apps/web/package.json ./apps/web/package.json
|
|
20
|
-
|
|
20
|
+
ARG PODOKIT_NPM_REGISTRY=""
|
|
21
|
+
RUN if [ -n "$PODOKIT_NPM_REGISTRY" ]; then npm config set registry "$PODOKIT_NPM_REGISTRY"; fi \
|
|
22
|
+
&& npm install
|
|
21
23
|
|
|
22
24
|
# Baked source (kept in sync during development by `docker compose watch`).
|
|
23
25
|
COPY . .
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"migration:generate": "npm run typeorm -- migration:generate"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@podosoft/podokit-contracts": "^0.
|
|
17
|
+
"@podosoft/podokit-contracts": "^0.4.0",
|
|
18
18
|
"@nestjs/common": "^11.1.27",
|
|
19
19
|
"@nestjs/config": "^4.0.4",
|
|
20
20
|
"@nestjs/core": "^11.1.27",
|
|
@@ -41,6 +41,8 @@ services:
|
|
|
41
41
|
build:
|
|
42
42
|
context: .
|
|
43
43
|
dockerfile: Dockerfile.dev
|
|
44
|
+
args:
|
|
45
|
+
PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
|
|
44
46
|
command: npm run dev -w {{projectName}}-web -- --host 0.0.0.0
|
|
45
47
|
working_dir: /app
|
|
46
48
|
env_file: .env.docker
|
|
@@ -64,6 +66,8 @@ services:
|
|
|
64
66
|
build:
|
|
65
67
|
context: .
|
|
66
68
|
dockerfile: Dockerfile.dev
|
|
69
|
+
args:
|
|
70
|
+
PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
|
|
67
71
|
init: true
|
|
68
72
|
command: npm run dev -w {{projectName}}-api
|
|
69
73
|
working_dir: /app
|
|
@@ -87,6 +91,8 @@ services:
|
|
|
87
91
|
build:
|
|
88
92
|
context: .
|
|
89
93
|
dockerfile: Dockerfile.dev
|
|
94
|
+
args:
|
|
95
|
+
PODOKIT_NPM_REGISTRY: ${PODOKIT_NPM_REGISTRY:-}
|
|
90
96
|
init: true
|
|
91
97
|
command: npm run dev:worker -w {{projectName}}-api
|
|
92
98
|
working_dir: /app
|
package/dist/update.d.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { type Tier } from "./lockfile";
|
|
|
5
5
|
* working copy, classifying each file. Applying the plan (writing files, 3-way
|
|
6
6
|
* merging) is a later step (see ADR-0009).
|
|
7
7
|
*/
|
|
8
|
-
export type Action = "update" | "merge" | "conflict" | "add" | "remove" | "skip" | "up-to-date";
|
|
8
|
+
export type Action = "update" | "merge" | "conflict" | "add" | "move" | "remove" | "skip" | "up-to-date";
|
|
9
9
|
export interface FileChange {
|
|
10
10
|
path: string;
|
|
11
|
+
/** Previous path when this change is an update migration move. */
|
|
12
|
+
fromPath?: string;
|
|
11
13
|
tier: Tier;
|
|
12
14
|
action: Action;
|
|
13
15
|
/** Why the action was chosen (for the report). */
|
|
@@ -33,6 +35,11 @@ export interface ApplyOptions {
|
|
|
33
35
|
}
|
|
34
36
|
export interface ApplyResult {
|
|
35
37
|
written: string[];
|
|
38
|
+
/** Files relocated by module update migrations. */
|
|
39
|
+
moved: {
|
|
40
|
+
from: string;
|
|
41
|
+
to: string;
|
|
42
|
+
}[];
|
|
36
43
|
removed: string[];
|
|
37
44
|
/** Files 3-way merged cleanly. */
|
|
38
45
|
merged: string[];
|
package/dist/update.js
CHANGED
|
@@ -12,6 +12,7 @@ const assemble_1 = require("./assemble");
|
|
|
12
12
|
const lockfile_1 = require("./lockfile");
|
|
13
13
|
const inspect_1 = require("./inspect");
|
|
14
14
|
const add_1 = require("./add");
|
|
15
|
+
const module_migrations_1 = require("./module-migrations");
|
|
15
16
|
function treeText(tree, path) {
|
|
16
17
|
const file = tree.get(path);
|
|
17
18
|
if (!file)
|
|
@@ -64,9 +65,29 @@ function planUpdate(projectRoot, templatesDir) {
|
|
|
64
65
|
});
|
|
65
66
|
const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, manifest.modules, manifest.managedOverrides);
|
|
66
67
|
const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, manifest.modules, manifest.ownedGlobs);
|
|
67
|
-
const
|
|
68
|
+
const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, manifest.modules);
|
|
69
|
+
const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, lock, pendingMigrations);
|
|
70
|
+
const changes = migrationPlan.moves.map((move) => ({
|
|
71
|
+
path: move.to,
|
|
72
|
+
fromPath: move.from,
|
|
73
|
+
tier: move.tier,
|
|
74
|
+
action: "move",
|
|
75
|
+
note: "module path migration",
|
|
76
|
+
}));
|
|
77
|
+
for (const conflict of migrationPlan.conflicts) {
|
|
78
|
+
changes.push({
|
|
79
|
+
path: "module migration",
|
|
80
|
+
tier: "managed",
|
|
81
|
+
action: "conflict",
|
|
82
|
+
note: conflict,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const movedSources = new Set(migrationPlan.moves.map((move) => move.from));
|
|
86
|
+
const movedTargets = new Set(migrationPlan.moves.map((move) => move.to));
|
|
68
87
|
const paths = new Set([...newTree.keys(), ...Object.keys(lock.files)]);
|
|
69
88
|
for (const path of [...paths].sort()) {
|
|
89
|
+
if (movedSources.has(path) || movedTargets.has(path))
|
|
90
|
+
continue;
|
|
70
91
|
const locked = lock.files[path];
|
|
71
92
|
const newText = treeText(newTree, path);
|
|
72
93
|
const disk = diskContent(projectRoot, path);
|
|
@@ -232,6 +253,19 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
232
253
|
});
|
|
233
254
|
const managedOverrides = targetManagedOverrides(projectRoot, templatesDir, modules, manifest.managedOverrides);
|
|
234
255
|
const ownedGlobs = targetOwnedGlobs(projectRoot, templatesDir, modules, manifest.ownedGlobs);
|
|
256
|
+
const pendingMigrations = (0, module_migrations_1.collectPendingMigrations)(projectRoot, templatesDir, modules);
|
|
257
|
+
const migrationPlan = (0, module_migrations_1.planModuleMigrations)(projectRoot, newTree, previousLock, pendingMigrations);
|
|
258
|
+
if (migrationPlan.conflicts.length) {
|
|
259
|
+
throw new Error(`Cannot apply module path migration:\n${migrationPlan.conflicts
|
|
260
|
+
.map((conflict) => ` - ${conflict}`)
|
|
261
|
+
.join("\n")}`);
|
|
262
|
+
}
|
|
263
|
+
const migratedOwnedGlobs = [
|
|
264
|
+
...new Set(ownedGlobs.map((glob) => (0, module_migrations_1.migrateConfiguredPath)(glob, pendingMigrations))),
|
|
265
|
+
];
|
|
266
|
+
const migratedManagedOverrides = [
|
|
267
|
+
...new Set(managedOverrides.map((glob) => (0, module_migrations_1.migrateConfiguredPath)(glob, pendingMigrations))),
|
|
268
|
+
];
|
|
235
269
|
const needsMergeBase = plan.changes.some((change) => change.action === "conflict");
|
|
236
270
|
let oldTree = null;
|
|
237
271
|
if (options.oldTemplatesDir && needsMergeBase) {
|
|
@@ -250,7 +284,13 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
250
284
|
(0, node_fs_1.rmSync)(previousModules.root, { recursive: true, force: true });
|
|
251
285
|
}
|
|
252
286
|
}
|
|
253
|
-
const result = {
|
|
287
|
+
const result = {
|
|
288
|
+
written: [],
|
|
289
|
+
moved: (0, module_migrations_1.applyModuleMigrations)(projectRoot, migrationPlan),
|
|
290
|
+
removed: [],
|
|
291
|
+
merged: [],
|
|
292
|
+
conflicts: [],
|
|
293
|
+
};
|
|
254
294
|
for (const change of plan.changes) {
|
|
255
295
|
const newText = treeText(newTree, change.path);
|
|
256
296
|
if (change.action === "update" || change.action === "add") {
|
|
@@ -280,22 +320,37 @@ function applyUpdate(projectRoot, templatesDir, options = {}) {
|
|
|
280
320
|
// Keep the assembled target as the managed baseline. A merged working file
|
|
281
321
|
// intentionally remains drifted so a future update performs another 3-way
|
|
282
322
|
// merge instead of treating the user's lines as clean generated output.
|
|
283
|
-
(0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock,
|
|
323
|
+
(0, lockfile_1.writeFilesLock)(projectRoot, updatedFilesLock(projectRoot, newTree, previousLock, migratedOwnedGlobs, migratedManagedOverrides));
|
|
284
324
|
const modulesDir = (0, node_path_1.join)(templatesDir, "modules");
|
|
325
|
+
const appliedMigrations = new Map();
|
|
326
|
+
for (const pending of pendingMigrations) {
|
|
327
|
+
const ids = appliedMigrations.get(pending.module) ?? [];
|
|
328
|
+
ids.push(pending.migration.id);
|
|
329
|
+
appliedMigrations.set(pending.module, ids);
|
|
330
|
+
}
|
|
285
331
|
const refreshedModules = manifest.modules.map((module) => {
|
|
286
332
|
const resolved = (0, add_1.resolveModule)(module.packageName ?? module.name, modulesDir, projectRoot);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
...module,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
333
|
+
const applied = [
|
|
334
|
+
...new Set([
|
|
335
|
+
...(module.appliedMigrations ?? []),
|
|
336
|
+
...(appliedMigrations.get(module.name) ?? []),
|
|
337
|
+
]),
|
|
338
|
+
];
|
|
339
|
+
return {
|
|
340
|
+
...module,
|
|
341
|
+
...(resolved?.packageName
|
|
342
|
+
? {
|
|
343
|
+
packageName: resolved.packageName,
|
|
344
|
+
moduleVersion: resolved.moduleVersion,
|
|
345
|
+
}
|
|
346
|
+
: {}),
|
|
347
|
+
...(applied.length ? { appliedMigrations: applied } : {}),
|
|
348
|
+
};
|
|
294
349
|
});
|
|
295
350
|
(0, lockfile_1.writeManifest)(projectRoot, {
|
|
296
351
|
...manifest,
|
|
297
|
-
ownedGlobs,
|
|
298
|
-
managedOverrides,
|
|
352
|
+
ownedGlobs: migratedOwnedGlobs,
|
|
353
|
+
managedOverrides: migratedManagedOverrides,
|
|
299
354
|
modules: refreshedModules,
|
|
300
355
|
podokitVersion: (0, lockfile_1.podokitVersion)(),
|
|
301
356
|
});
|
|
@@ -308,6 +363,7 @@ function summarize(plan) {
|
|
|
308
363
|
merge: 0,
|
|
309
364
|
conflict: 0,
|
|
310
365
|
add: 0,
|
|
366
|
+
move: 0,
|
|
311
367
|
remove: 0,
|
|
312
368
|
skip: 0,
|
|
313
369
|
"up-to-date": 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "An opinionated but extensible starter toolkit and CLI for full-stack TypeScript apps with NestJS, SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|