@podosoft/podokit 0.12.4 → 0.13.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/templates/fullstack-nest-svelte/apps/api/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +1 -1
- 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/site-runtime.svelte +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +73 -2
- 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/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/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 +6 -6
- 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 +2 -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 +2 -2
- 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 +32 -2
- 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/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 +4 -1
- 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/apps/api/package.json +1 -1
- package/dist/templates/todo/apps/web/package.json +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { sendSms } from "../sms/sms";
|
|
|
6
6
|
import { assertUserCreationAllowed, createFeatureGate, createSignupOpenCheck } from "./feature-gate";
|
|
7
7
|
import { orgAc, orgRoles } from "./org-permissions";
|
|
8
8
|
import { pool } from "./db";
|
|
9
|
-
import { type AuthConfig, envAuthConfig, SUPPORTED_PROVIDER_IDS } from "@podosoft/podokit-auth";
|
|
9
|
+
import { type AuthConfig, envAuthConfig, sessionIdleOptions, SUPPORTED_PROVIDER_IDS } from "@podosoft/podokit-auth";
|
|
10
10
|
import { apiKey } from "@better-auth/api-key";
|
|
11
11
|
import { passkey } from "@better-auth/passkey";
|
|
12
12
|
import { oauthProvider } from "@better-auth/oauth-provider";
|
|
@@ -176,6 +176,8 @@ export function buildAuth(config: AuthConfig) {
|
|
|
176
176
|
// podokit:begin:auth-hooks
|
|
177
177
|
// podokit:end:auth-hooks
|
|
178
178
|
|
|
179
|
+
const session = sessionIdleOptions(config.sessionIdleTimeoutMinutes);
|
|
180
|
+
|
|
179
181
|
return betterAuth({
|
|
180
182
|
database: pool,
|
|
181
183
|
emailAndPassword: {
|
|
@@ -208,6 +210,7 @@ export function buildAuth(config: AuthConfig) {
|
|
|
208
210
|
},
|
|
209
211
|
},
|
|
210
212
|
socialProviders: buildSocial(config),
|
|
213
|
+
...(session ? { session } : {}),
|
|
211
214
|
plugins,
|
|
212
215
|
hooks,
|
|
213
216
|
secret: process.env.BETTER_AUTH_SECRET ?? "change-me-in-production-min-32-characters",
|
|
@@ -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.3.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();
|
|
@@ -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.3.0",
|
|
18
18
|
"@nestjs/common": "^11.1.27",
|
|
19
19
|
"@nestjs/config": "^4.0.4",
|
|
20
20
|
"@nestjs/core": "^11.1.27",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podosoft/podokit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": {
|