@podosoft/podokit 1.2.0 → 1.4.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 +13 -2
- package/dist/add.js +135 -20
- package/dist/api-paths.d.ts +34 -0
- package/dist/api-paths.js +103 -0
- package/dist/deploy-compose-profile.d.ts +4 -0
- package/dist/deploy-compose-profile.js +13 -2
- package/dist/deploy-compose-render.d.ts +1 -1
- package/dist/deploy-compose-render.js +23 -7
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.d.ts +3 -0
- package/dist/deploy-profile.js +12 -4
- package/dist/deploy-render.js +54 -8
- package/dist/deploy.js +21 -1
- package/dist/dev.d.ts +14 -0
- package/dist/dev.js +152 -10
- package/dist/index.js +23 -2
- package/dist/lib.d.ts +4 -2
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +6 -1
- package/dist/lockfile.js +26 -6
- package/dist/module-api-paths.d.ts +52 -0
- package/dist/module-api-paths.js +189 -0
- package/dist/remove.d.ts +3 -0
- package/dist/remove.js +12 -3
- package/dist/templates/fullstack/AGENTS.md +17 -2
- package/dist/templates/fullstack/README.md +19 -5
- package/dist/templates/fullstack/apps/api/package.json +1 -1
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +20 -1
- package/dist/templates/fullstack/apps/api/src/app.ts +11 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.spec.ts +48 -2
- package/dist/templates/fullstack/apps/api/src/config/redis.connection.ts +45 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +46 -0
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +48 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +196 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +181 -2
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +8 -1
- package/dist/templates/fullstack/apps/api/src/database/database.ts +29 -10
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.spec.ts +188 -0
- package/dist/templates/fullstack/apps/api/src/database/pool-guard.ts +151 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +28 -4
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +10 -2
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -1
- package/dist/templates/fullstack/apps/web/components.json +3 -1
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/index.ts +7 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/ui/textarea/textarea.svelte +22 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +276 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +194 -9
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +15 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-headers.ts +12 -0
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-elysia-endpoint/SKILL.md +5 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +14 -1
- package/dist/templates/fullstack/dot-devcontainer/devcontainer.json +1 -0
- package/dist/templates/fullstack/{dot-env.docker → dot-env.docker.example} +7 -4
- package/dist/templates/fullstack/dot-env.example +10 -0
- package/dist/templates/fullstack/dot-gitignore +1 -0
- package/dist/templates/fullstack/dot-podokit/dev.json +2 -1
- package/dist/templates/fullstack/infra/k3s/README.md +3 -1
- package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +27 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +59 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +81 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/asset-paths.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.test.ts +125 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.ts +107 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +13 -19
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +15 -12
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/object-response.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +14 -15
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +13 -8
- 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 +14 -3
- package/dist/templates/modules/admin-dashboard/module.manifest.json +4 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +53 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.spec.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +17 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +8 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.spec.ts +114 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/database-outage.ts +50 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +4 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +11 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance-command.ts +5 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/maintenance.ts +14 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.spec.ts +136 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/postgres-pool.ts +40 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +16 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +121 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +108 -10
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +3 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +31 -3
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +2 -4
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.spec.ts +224 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +124 -35
- package/dist/templates/modules/events-redis/module.manifest.json +4 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.routes.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.spec.ts +636 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/chunked-uploads.ts +673 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +10 -4
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +37 -8
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-config.ts +99 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/migrations/1792100000000-InitFileUploadSessions.ts +49 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.test.ts +456 -0
- package/dist/templates/modules/file-upload/files/apps/web/src/lib/uploads.ts +356 -0
- package/dist/templates/modules/file-upload/files/tests/api/chunked-uploads.api.spec.ts +147 -0
- package/dist/templates/modules/file-upload/module.manifest.json +34 -6
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +1 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +23 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +30 -20
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +20 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +79 -12
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +16 -1
- package/dist/templates/modules/object-storage-local/module.manifest.json +11 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.spec.ts +115 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-multipart.ts +171 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +222 -9
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +55 -19
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.spec.ts +71 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-signer.ts +98 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +6 -3
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +7 -2
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +187 -44
- package/dist/templates/modules/object-storage-s3/module.manifest.json +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +11 -22
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +113 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +122 -0
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +24 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +7 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +14 -1
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +9 -12
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +16 -4
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +233 -5
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +134 -32
- package/dist/templates/modules/redis/module.manifest.json +4 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +11 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +48 -1
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +15 -2
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +68 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +25 -1
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/modules/sse/module.manifest.json +3 -1
- package/dist/templates/todo/AGENTS.md +9 -1
- package/dist/templates/todo/README.md +18 -5
- package/dist/update.d.ts +22 -0
- package/dist/update.js +68 -8
- package/package.json +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-validation.ts +0 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/skip-schema-validation.ts +0 -5
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Bundled entry for scripts/bootstrap-admin.mjs. The build bundles only its
|
|
2
|
+
// top-level targets, so the script cannot import auth modules from dist directly.
|
|
3
|
+
// The command runs against a migrated schema and reports a missing table from its
|
|
4
|
+
// own query. Better Auth's asynchronous startup check would only race the
|
|
5
|
+
// connection the command closes when it finishes.
|
|
6
|
+
import "./auth/maintenance-command";
|
|
7
|
+
import { verifyPassword } from "better-auth/crypto";
|
|
8
|
+
import { auth } from "./auth/auth";
|
|
9
|
+
import { closeAuthDatabase, postgresPool, sqliteDatabase } from "./auth/db";
|
|
10
|
+
|
|
11
|
+
export interface BootstrapUser {
|
|
12
|
+
id: string;
|
|
13
|
+
email: string;
|
|
14
|
+
role: string | null;
|
|
15
|
+
emailVerified: boolean;
|
|
16
|
+
signupApproved: boolean;
|
|
17
|
+
passwordHash: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BootstrapUserRow extends Omit<BootstrapUser, "emailVerified" | "signupApproved"> {
|
|
21
|
+
// SQLite stores booleans as 0 or 1.
|
|
22
|
+
emailVerified: boolean | number | null;
|
|
23
|
+
signupApproved: boolean | number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function userQuery(placeholder: string): string {
|
|
27
|
+
return `SELECT u.id,
|
|
28
|
+
u.email,
|
|
29
|
+
u.role,
|
|
30
|
+
u."emailVerified" AS "emailVerified",
|
|
31
|
+
u."signupApproved" AS "signupApproved",
|
|
32
|
+
a.password AS "passwordHash"
|
|
33
|
+
FROM "user" AS u
|
|
34
|
+
LEFT JOIN "account" AS a
|
|
35
|
+
ON a."userId" = u.id
|
|
36
|
+
AND a."providerId" = 'credential'
|
|
37
|
+
WHERE lower(u.email) = lower(${placeholder})
|
|
38
|
+
LIMIT 1`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function flag(value: boolean | number | null): boolean {
|
|
42
|
+
return value === true || value === 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function findBootstrapUser(email: string): Promise<BootstrapUser | null> {
|
|
46
|
+
let row: BootstrapUserRow | undefined;
|
|
47
|
+
if (postgresPool) {
|
|
48
|
+
row = (await postgresPool.query<BootstrapUserRow>(userQuery("$1"), [email])).rows[0];
|
|
49
|
+
} else if (sqliteDatabase) {
|
|
50
|
+
row = sqliteDatabase.query<BootstrapUserRow, [string]>(userQuery("?1")).get(email) ?? undefined;
|
|
51
|
+
}
|
|
52
|
+
if (!row) return null;
|
|
53
|
+
return {
|
|
54
|
+
...row,
|
|
55
|
+
emailVerified: flag(row.emailVerified),
|
|
56
|
+
signupApproved: flag(row.signupApproved),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { auth, closeAuthDatabase, verifyPassword };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
// The admin session list pages active sessions newest first with a keyset on
|
|
4
|
+
// ("createdAt", "id"). Better Auth's schema indexes only "userId" and "token",
|
|
5
|
+
// so without this index every page sorts the whole session table.
|
|
6
|
+
const INDEX = "IDX_session_createdAt_id";
|
|
7
|
+
|
|
8
|
+
export class IndexSessionCreatedAt1792060000000 implements MigrationInterface {
|
|
9
|
+
name = "IndexSessionCreatedAt1792060000000";
|
|
10
|
+
// CONCURRENTLY cannot run inside a transaction block. The runner therefore
|
|
11
|
+
// uses the "each" transaction mode. SQLite migrations always use one.
|
|
12
|
+
transaction = false;
|
|
13
|
+
|
|
14
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
15
|
+
if (queryRunner.connection.options.type !== "postgres") {
|
|
16
|
+
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "${INDEX}" ON "session" ("createdAt", "id")`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Sign-ins keep writing sessions while the index builds. An interrupted
|
|
20
|
+
// build leaves an invalid index that IF NOT EXISTS would keep.
|
|
21
|
+
const invalid = await queryRunner.query(
|
|
22
|
+
`SELECT 1 AS "invalid" FROM pg_index WHERE indexrelid = to_regclass($1) AND NOT indisvalid`,
|
|
23
|
+
[`"${INDEX}"`],
|
|
24
|
+
) as unknown[];
|
|
25
|
+
if (invalid.length > 0) await queryRunner.query(`DROP INDEX CONCURRENTLY IF EXISTS "${INDEX}"`);
|
|
26
|
+
await queryRunner.query(
|
|
27
|
+
`CREATE INDEX CONCURRENTLY IF NOT EXISTS "${INDEX}" ON "session" ("createdAt", "id")`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A revert runs inside a transaction, so it uses a plain statement.
|
|
32
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
33
|
+
await queryRunner.query(`DROP INDEX IF EXISTS "${INDEX}"`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { error } from "@sveltejs/kit";
|
|
1
|
+
import { error, type RequestEvent } from "@sveltejs/kit";
|
|
2
2
|
import type { Handle } from "@sveltejs/kit/hooks";
|
|
3
|
+
import { building } from "$app/env";
|
|
4
|
+
import { routes } from "$app/manifest";
|
|
3
5
|
import { serverApiClient } from "#lib/server/api.js";
|
|
4
6
|
import { resolveLocale } from "#lib/i18n/messages.js";
|
|
5
7
|
import { applySearchIndexingHeaders } from "#lib/server/search-indexing.js";
|
|
6
8
|
import { isPublicPath } from "#lib/server/guards.js";
|
|
9
|
+
import { needsRuntimeContext } from "#lib/server/runtime-context.js";
|
|
7
10
|
import type { SiteSettings } from "#lib/site.svelte.js";
|
|
8
11
|
import { ApiError } from "@podosoft/podokit-api-client";
|
|
9
12
|
|
|
@@ -12,15 +15,18 @@ function isTemporaryServiceFailure(cause: unknown): boolean {
|
|
|
12
15
|
return cause.statusCode === 408 || cause.statusCode === 429 || cause.statusCode >= 500;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// An anonymous visitor with no site settings, for responses that render no
|
|
19
|
+
// layout. The locale still follows the visitor's cookie.
|
|
20
|
+
function setEmptyRuntimeContext(event: RequestEvent): void {
|
|
21
|
+
event.locals.site = null;
|
|
22
|
+
event.locals.siteUnavailable = false;
|
|
23
|
+
event.locals.user = null;
|
|
24
|
+
event.locals.session = null;
|
|
25
|
+
event.locals.authUnavailable = false;
|
|
26
|
+
event.locals.locale = resolveLocale(event.cookies.get("locale"));
|
|
27
|
+
}
|
|
23
28
|
|
|
29
|
+
async function loadRuntimeContext(event: RequestEvent): Promise<void> {
|
|
24
30
|
// Public site settings (branding, locale/timezone defaults, maintenance, …)
|
|
25
31
|
// and the session are independent, so both requests run at once. The
|
|
26
32
|
// settings are loaded once here and reused by the layout load. The async
|
|
@@ -73,6 +79,18 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
73
79
|
) {
|
|
74
80
|
error(503, "Service temporarily unavailable");
|
|
75
81
|
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
85
|
+
// Listed asset endpoints, missing subresources, the API proxy, and
|
|
86
|
+
// prerendering skip the two API lookups. Pages, their data, form actions,
|
|
87
|
+
// remote functions, and the 404 page get the complete runtime context.
|
|
88
|
+
const route = routes.find(({ id }) => id === event.route.id);
|
|
89
|
+
if (needsRuntimeContext(event, { building, route })) {
|
|
90
|
+
await loadRuntimeContext(event);
|
|
91
|
+
} else {
|
|
92
|
+
setEmptyRuntimeContext(event);
|
|
93
|
+
}
|
|
76
94
|
const response = await resolve(event, {
|
|
77
95
|
transformPageChunk: ({ html }) =>
|
|
78
96
|
html.replace(
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
adminUsersQuery,
|
|
4
|
+
keepLatestFilter,
|
|
5
|
+
listAdminUsers,
|
|
6
|
+
listUsersQuery,
|
|
7
|
+
type UserListState,
|
|
8
|
+
} from "./users";
|
|
3
9
|
|
|
4
10
|
const base: UserListState = {
|
|
5
11
|
page: 1,
|
|
@@ -11,9 +17,9 @@ const base: UserListState = {
|
|
|
11
17
|
status: "",
|
|
12
18
|
};
|
|
13
19
|
|
|
14
|
-
describe("
|
|
20
|
+
describe("adminUsersQuery", () => {
|
|
15
21
|
it("pages newest first by default", () => {
|
|
16
|
-
expect(
|
|
22
|
+
expect(adminUsersQuery({ ...base, page: 3 })).toEqual({
|
|
17
23
|
limit: 15,
|
|
18
24
|
offset: 30,
|
|
19
25
|
sortBy: "createdAt",
|
|
@@ -22,54 +28,75 @@ describe("listUsersQuery", () => {
|
|
|
22
28
|
});
|
|
23
29
|
|
|
24
30
|
it("sorts by a table column on the server", () => {
|
|
25
|
-
expect(
|
|
31
|
+
expect(adminUsersQuery({ ...base, sort: { key: "name", dir: "asc" } })).toMatchObject({
|
|
26
32
|
sortBy: "name",
|
|
27
33
|
sortDirection: "asc",
|
|
28
34
|
});
|
|
29
|
-
expect(
|
|
35
|
+
expect(adminUsersQuery({ ...base, sort: { key: "status", dir: "asc" } })).toMatchObject({
|
|
30
36
|
sortBy: "createdAt",
|
|
31
37
|
sortDirection: "desc",
|
|
32
38
|
});
|
|
33
39
|
});
|
|
34
40
|
|
|
35
|
-
it("searches
|
|
36
|
-
expect(
|
|
37
|
-
|
|
41
|
+
it("searches the chosen field with the text trimmed", () => {
|
|
42
|
+
expect(adminUsersQuery({ ...base, search: " Ada@Example.COM " })).toMatchObject({
|
|
43
|
+
search: "Ada@Example.COM",
|
|
38
44
|
searchField: "email",
|
|
39
|
-
searchOperator: "contains",
|
|
40
45
|
});
|
|
41
|
-
expect(
|
|
42
|
-
|
|
46
|
+
expect(adminUsersQuery({ ...base, search: "Ada", searchField: "name" })).toMatchObject({
|
|
47
|
+
search: "Ada",
|
|
43
48
|
searchField: "name",
|
|
44
49
|
});
|
|
45
|
-
expect(
|
|
50
|
+
expect(adminUsersQuery({ ...base, search: " " })).not.toHaveProperty("search");
|
|
46
51
|
});
|
|
47
52
|
|
|
48
|
-
it("
|
|
49
|
-
expect(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
filterOperator: "eq",
|
|
53
|
+
it("sends the role and status filters together", () => {
|
|
54
|
+
expect(adminUsersQuery({ ...base, role: "admin", status: "active" })).toMatchObject({
|
|
55
|
+
role: "admin",
|
|
56
|
+
status: "active",
|
|
53
57
|
});
|
|
54
|
-
expect(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
expect(adminUsersQuery({ ...base, status: "pending" })).toMatchObject({ status: "pending" });
|
|
59
|
+
expect(adminUsersQuery({ ...base, status: "deleted" })).not.toHaveProperty("status");
|
|
60
|
+
expect(adminUsersQuery(base)).not.toHaveProperty("role");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("listAdminUsers", () => {
|
|
65
|
+
it("asks GET /admin/users for one page", async () => {
|
|
66
|
+
const paths: string[] = [];
|
|
67
|
+
const client = {
|
|
68
|
+
get: <T>(path: string): Promise<T> => {
|
|
69
|
+
paths.push(path);
|
|
70
|
+
return Promise.resolve({ items: [], total: 0 } as unknown as T);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await listAdminUsers(client, adminUsersQuery({ ...base, page: 2, search: "ada", role: "admin", status: "banned" }));
|
|
75
|
+
await listAdminUsers(client, adminUsersQuery(base));
|
|
76
|
+
|
|
77
|
+
expect(paths).toEqual([
|
|
78
|
+
"/admin/users?limit=15&offset=15&sortBy=createdAt&sortDirection=desc&search=ada&searchField=email&role=admin&status=banned",
|
|
79
|
+
"/admin/users?limit=15&offset=0&sortBy=createdAt&sortDirection=desc",
|
|
80
|
+
]);
|
|
63
81
|
});
|
|
64
82
|
});
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
// Application-owned users pages from 1.2 still import these.
|
|
85
|
+
describe("deprecated listUsers helpers", () => {
|
|
86
|
+
it("still builds the single-filter Better Auth query", () => {
|
|
87
|
+
expect(listUsersQuery({ ...base, page: 2, search: " Ada@Example.COM ", status: "banned" })).toEqual({
|
|
88
|
+
limit: 15,
|
|
89
|
+
offset: 15,
|
|
90
|
+
sortBy: "createdAt",
|
|
91
|
+
sortDirection: "desc",
|
|
92
|
+
searchValue: "ada@example.com",
|
|
93
|
+
searchField: "email",
|
|
94
|
+
searchOperator: "contains",
|
|
95
|
+
filterField: "banned",
|
|
96
|
+
filterValue: true,
|
|
97
|
+
filterOperator: "eq",
|
|
98
|
+
});
|
|
68
99
|
expect(keepLatestFilter({ role: "admin", status: "" }, { role: "admin", status: "banned" }))
|
|
69
100
|
.toEqual({ role: "", status: "banned" });
|
|
70
|
-
expect(keepLatestFilter({ role: "", status: "banned" }, { role: "user", status: "banned" }))
|
|
71
|
-
.toEqual({ role: "user", status: "" });
|
|
72
|
-
expect(keepLatestFilter({ role: "", status: "" }, { role: "user", status: "" }))
|
|
73
|
-
.toEqual({ role: "user", status: "" });
|
|
74
101
|
});
|
|
75
102
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ApiClient } from "@podosoft/podokit-api-client";
|
|
2
|
+
|
|
3
|
+
// Builds the `GET /admin/users` query for the server-paged users table.
|
|
2
4
|
|
|
3
5
|
export type UserListSort = { key: string; dir: "asc" | "desc" };
|
|
4
6
|
|
|
@@ -12,6 +14,82 @@ export type UserListState = {
|
|
|
12
14
|
status: string;
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
export type AdminUserStatus = "active" | "pending" | "banned";
|
|
18
|
+
|
|
19
|
+
/** A user from `GET /admin/users`. */
|
|
20
|
+
export type AdminUser = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
email: string;
|
|
24
|
+
role: string | null;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
signupApproved: boolean;
|
|
27
|
+
banned: boolean;
|
|
28
|
+
banReason: string | null;
|
|
29
|
+
banExpires: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type AdminUserPage = { items: AdminUser[]; total: number };
|
|
34
|
+
|
|
35
|
+
export type AdminUserQuery = {
|
|
36
|
+
limit: number;
|
|
37
|
+
offset: number;
|
|
38
|
+
sortBy: string;
|
|
39
|
+
sortDirection: "asc" | "desc";
|
|
40
|
+
search?: string;
|
|
41
|
+
searchField?: "email" | "name";
|
|
42
|
+
role?: string;
|
|
43
|
+
status?: AdminUserStatus;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_USER_SORT: UserListSort = { key: "createdAt", dir: "desc" };
|
|
47
|
+
|
|
48
|
+
const SORTABLE_FIELDS = new Set(["name", "email", "role", "createdAt"]);
|
|
49
|
+
const STATUSES = new Set<string>(["active", "pending", "banned"]);
|
|
50
|
+
|
|
51
|
+
function isStatus(value: string): value is AdminUserStatus {
|
|
52
|
+
return STATUSES.has(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function adminUsersQuery(state: UserListState): AdminUserQuery {
|
|
56
|
+
const sort = state.sort && SORTABLE_FIELDS.has(state.sort.key) ? state.sort : DEFAULT_USER_SORT;
|
|
57
|
+
const search = state.search.trim();
|
|
58
|
+
return {
|
|
59
|
+
limit: state.perPage,
|
|
60
|
+
offset: (Math.max(state.page, 1) - 1) * state.perPage,
|
|
61
|
+
sortBy: sort.key,
|
|
62
|
+
sortDirection: sort.dir,
|
|
63
|
+
// The server matches case-insensitively, so the text is sent as typed.
|
|
64
|
+
...(search ? { search, searchField: state.searchField === "name" ? "name" as const : "email" as const } : {}),
|
|
65
|
+
...(state.role ? { role: state.role } : {}),
|
|
66
|
+
...(isStatus(state.status) ? { status: state.status } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Reads one page; the role and status filters combine on the server. */
|
|
71
|
+
export function listAdminUsers(client: Pick<ApiClient, "get">, query: AdminUserQuery): Promise<AdminUserPage> {
|
|
72
|
+
const params = new URLSearchParams({
|
|
73
|
+
limit: String(query.limit),
|
|
74
|
+
offset: String(query.offset),
|
|
75
|
+
sortBy: query.sortBy,
|
|
76
|
+
sortDirection: query.sortDirection,
|
|
77
|
+
});
|
|
78
|
+
if (query.search) {
|
|
79
|
+
params.set("search", query.search);
|
|
80
|
+
params.set("searchField", query.searchField ?? "email");
|
|
81
|
+
}
|
|
82
|
+
if (query.role) params.set("role", query.role);
|
|
83
|
+
if (query.status) params.set("status", query.status);
|
|
84
|
+
return client.get<AdminUserPage>(`/admin/users?${params.toString()}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The 1.2 users page is application-owned and still imports the helpers below,
|
|
88
|
+
// which build a Better Auth `admin.listUsers` query. Keep them until the next
|
|
89
|
+
// major release so `podo update` does not break that page; new pages use
|
|
90
|
+
// `adminUsersQuery` and `listAdminUsers`.
|
|
91
|
+
|
|
92
|
+
/** @deprecated Use `AdminUserQuery` with `listAdminUsers`. */
|
|
15
93
|
export type ListUsersQuery = {
|
|
16
94
|
limit: number;
|
|
17
95
|
offset: number;
|
|
@@ -25,10 +103,6 @@ export type ListUsersQuery = {
|
|
|
25
103
|
filterOperator?: "eq";
|
|
26
104
|
};
|
|
27
105
|
|
|
28
|
-
export const DEFAULT_USER_SORT: UserListSort = { key: "createdAt", dir: "desc" };
|
|
29
|
-
|
|
30
|
-
const SORTABLE_FIELDS = new Set(["name", "email", "role", "createdAt"]);
|
|
31
|
-
|
|
32
106
|
function filterFor(role: string, status: string): Pick<ListUsersQuery, "filterField" | "filterValue" | "filterOperator"> {
|
|
33
107
|
if (status === "pending") return { filterField: "signupApproved", filterValue: false, filterOperator: "eq" };
|
|
34
108
|
if (status === "banned") return { filterField: "banned", filterValue: true, filterOperator: "eq" };
|
|
@@ -36,6 +110,7 @@ function filterFor(role: string, status: string): Pick<ListUsersQuery, "filterFi
|
|
|
36
110
|
return {};
|
|
37
111
|
}
|
|
38
112
|
|
|
113
|
+
/** @deprecated Use `adminUsersQuery`; `listUsers` accepts only one filter. */
|
|
39
114
|
export function listUsersQuery(state: UserListState): ListUsersQuery {
|
|
40
115
|
const sort = state.sort && SORTABLE_FIELDS.has(state.sort.key) ? state.sort : DEFAULT_USER_SORT;
|
|
41
116
|
const searchField = state.searchField === "name" ? "name" : "email";
|
|
@@ -47,8 +122,6 @@ export function listUsersQuery(state: UserListState): ListUsersQuery {
|
|
|
47
122
|
sortDirection: sort.dir,
|
|
48
123
|
...(search
|
|
49
124
|
? {
|
|
50
|
-
// Better Auth matches with a case-sensitive LIKE on PostgreSQL and stores
|
|
51
|
-
// emails in lower case, so lower-casing makes email search case-insensitive.
|
|
52
125
|
searchValue: searchField === "email" ? search.toLowerCase() : search,
|
|
53
126
|
searchField,
|
|
54
127
|
searchOperator: "contains" as const,
|
|
@@ -58,10 +131,7 @@ export function listUsersQuery(state: UserListState): ListUsersQuery {
|
|
|
58
131
|
};
|
|
59
132
|
}
|
|
60
133
|
|
|
61
|
-
/**
|
|
62
|
-
* `listUsers` accepts one filter, so the role and status selects are mutually
|
|
63
|
-
* exclusive: when both hold a value, the one that just changed wins.
|
|
64
|
-
*/
|
|
134
|
+
/** @deprecated `GET /admin/users` combines the role and status filters. */
|
|
65
135
|
export function keepLatestFilter(
|
|
66
136
|
previous: { role: string; status: string },
|
|
67
137
|
next: { role: string; status: string },
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-owned prefixes for `+server.ts` endpoints that serve files, feeds,
|
|
3
|
+
* or other responses needing neither the session nor the site settings (for
|
|
4
|
+
* example "/docs" when an endpoint serves documentation files). A listed path
|
|
5
|
+
* also covers its subpaths. When an endpoint under a listed prefix answers a
|
|
6
|
+
* request, hooks.server.ts skips its two API lookups and the endpoint sees an
|
|
7
|
+
* anonymous `locals.user`. A `+page` under a listed prefix still gets the
|
|
8
|
+
* session and site settings, so it keeps its sign-in and maintenance checks.
|
|
9
|
+
* PodoKit creates this file once and never overwrites it.
|
|
10
|
+
*/
|
|
11
|
+
export const APP_ASSET_PATHS: readonly string[] = [];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Stands in for the application's own list in the app-owned public-paths.ts.
|
|
4
|
+
vi.mock("./public-paths", () => ({ APP_PUBLIC_PATHS: ["/pricing", "/robots.txt"] }));
|
|
5
|
+
|
|
6
|
+
const { isPublicPath } = await import("./guards");
|
|
7
|
+
|
|
8
|
+
describe("isPublicPath", () => {
|
|
9
|
+
it("keeps the sign-in flow and the landing page public", () => {
|
|
10
|
+
expect(isPublicPath("/")).toBe(true);
|
|
11
|
+
expect(isPublicPath("/login")).toBe(true);
|
|
12
|
+
expect(isPublicPath("/reset-password/token")).toBe(true);
|
|
13
|
+
expect(isPublicPath("/admin")).toBe(false);
|
|
14
|
+
expect(isPublicPath("/account")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("adds the application's public prefixes and their subpaths", () => {
|
|
18
|
+
expect(isPublicPath("/pricing")).toBe(true);
|
|
19
|
+
expect(isPublicPath("/pricing/teams")).toBe(true);
|
|
20
|
+
expect(isPublicPath("/robots.txt")).toBe(true);
|
|
21
|
+
expect(isPublicPath("/pricingx")).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { error } from "@sveltejs/kit";
|
|
2
|
+
import { APP_PUBLIC_PATHS } from "./public-paths";
|
|
2
3
|
|
|
3
4
|
/** Roles that may reach the admin console. Kept as a set so custom roles can be
|
|
4
5
|
* added here (and, later, replaced by a permission check) without touching every
|
|
@@ -13,6 +14,7 @@ const PUBLIC_PATHS = [
|
|
|
13
14
|
"/verify-email",
|
|
14
15
|
"/pending-approval",
|
|
15
16
|
"/maintenance",
|
|
17
|
+
...APP_PUBLIC_PATHS,
|
|
16
18
|
// Modules add their public (no-session) page prefixes here.
|
|
17
19
|
// podokit:begin:public-paths
|
|
18
20
|
// podokit:end:public-paths
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-owned public page prefixes. A listed path also covers its
|
|
3
|
+
* subpaths ("/pricing" covers "/pricing/teams"). These pages render for
|
|
4
|
+
* anonymous visitors and keep rendering when the API or site settings are
|
|
5
|
+
* unavailable; every other page requires a session and fails closed with 503
|
|
6
|
+
* during an outage. Modules add their own prefixes in guards.ts, so do not
|
|
7
|
+
* repeat them here. PodoKit creates this file once and never overwrites it.
|
|
8
|
+
*/
|
|
9
|
+
export const APP_PUBLIC_PATHS: readonly string[] = [];
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/runtime-context.test.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Stands in for the application's own list in the app-owned asset-paths.ts.
|
|
4
|
+
vi.mock("./asset-paths", () => ({ APP_ASSET_PATHS: ["/docs", "/feed.xml"] }));
|
|
5
|
+
|
|
6
|
+
const { needsRuntimeContext, prefersHtml } = await import("./runtime-context");
|
|
7
|
+
|
|
8
|
+
const NAVIGATION = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
|
|
9
|
+
const IMAGE = "image/avif,image/webp,image/apng,*/*;q=0.8";
|
|
10
|
+
|
|
11
|
+
interface RequestOptions {
|
|
12
|
+
routeId?: string | null;
|
|
13
|
+
method?: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
isDataRequest?: boolean;
|
|
16
|
+
isRemoteRequest?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function request(path: string, options: RequestOptions = {}) {
|
|
20
|
+
const url = new URL(path, "http://app.localhost");
|
|
21
|
+
return {
|
|
22
|
+
url,
|
|
23
|
+
route: { id: options.routeId === undefined ? url.pathname : options.routeId },
|
|
24
|
+
request: new Request(url, { method: options.method ?? "GET", headers: options.headers }),
|
|
25
|
+
isDataRequest: options.isDataRequest ?? false,
|
|
26
|
+
isRemoteRequest: options.isRemoteRequest ?? false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("needsRuntimeContext", () => {
|
|
31
|
+
it("loads the session and site settings for pages and their data", () => {
|
|
32
|
+
expect(needsRuntimeContext(request("/account", { headers: { accept: NAVIGATION } }))).toBe(true);
|
|
33
|
+
expect(needsRuntimeContext(request("/account", { headers: { accept: "*/*" } }))).toBe(true);
|
|
34
|
+
expect(needsRuntimeContext(request("/account", { isDataRequest: true }))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("loads them for an unmatched route because the 404 page renders the root layout", () => {
|
|
38
|
+
// Without the session a signed-in visitor was sent to /login, which sent
|
|
39
|
+
// them straight back: a redirect loop on every missing page.
|
|
40
|
+
expect(
|
|
41
|
+
needsRuntimeContext(
|
|
42
|
+
request("/missing", { routeId: null, headers: { accept: NAVIGATION, "sec-fetch-dest": "document" } }),
|
|
43
|
+
),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
expect(needsRuntimeContext(request("/missing", { routeId: null, headers: { accept: "*/*" } }))).toBe(true);
|
|
46
|
+
expect(needsRuntimeContext(request("/missing", { routeId: null, isDataRequest: true }))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("skips them for a missing subresource that SvelteKit answers without a layout", () => {
|
|
50
|
+
for (const destination of ["image", "script", "style", "font"]) {
|
|
51
|
+
expect(
|
|
52
|
+
needsRuntimeContext(request("/missing.png", { routeId: null, headers: { "sec-fetch-dest": destination } })),
|
|
53
|
+
).toBe(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("skips them for the backend proxy", () => {
|
|
58
|
+
expect(needsRuntimeContext(request("/api", { routeId: "/api/[...path]" }))).toBe(false);
|
|
59
|
+
expect(needsRuntimeContext(request("/api/site/settings", { routeId: "/api/[...path]" }))).toBe(false);
|
|
60
|
+
expect(needsRuntimeContext(request("/api/auth/get-session", { routeId: "/api/auth/[...all]" }))).toBe(false);
|
|
61
|
+
expect(needsRuntimeContext(request("/apiary", { headers: { accept: NAVIGATION } }))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("skips them while prerendering", () => {
|
|
65
|
+
expect(needsRuntimeContext(request("/pricing", { headers: { accept: NAVIGATION } }), { building: true })).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("loads them for remote functions, which may read the session without a route", () => {
|
|
69
|
+
expect(needsRuntimeContext(request("/_app/remote/abc/list", { routeId: null, isRemoteRequest: true }))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("skips them when an endpoint under an application asset path answers", () => {
|
|
73
|
+
const files = { page: false, endpoint: true };
|
|
74
|
+
expect(
|
|
75
|
+
needsRuntimeContext(
|
|
76
|
+
request("/docs/guide/diagram.png", { routeId: "/docs/[...path]", headers: { accept: IMAGE, "sec-fetch-dest": "image" } }),
|
|
77
|
+
{ route: files },
|
|
78
|
+
),
|
|
79
|
+
).toBe(false);
|
|
80
|
+
// Opening a file in a tab is a navigation, but no page renders.
|
|
81
|
+
expect(
|
|
82
|
+
needsRuntimeContext(
|
|
83
|
+
request("/docs/manual.pdf", { routeId: "/docs/[...path]", headers: { accept: NAVIGATION, "sec-fetch-dest": "document" } }),
|
|
84
|
+
{ route: files },
|
|
85
|
+
),
|
|
86
|
+
).toBe(false);
|
|
87
|
+
expect(needsRuntimeContext(request("/feed.xml", { headers: { accept: "application/rss+xml" } }), { route: files })).toBe(false);
|
|
88
|
+
expect(needsRuntimeContext(request("/documents", { headers: { accept: "*/*" } }), { route: files })).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("keeps them when a page under an application asset path answers", () => {
|
|
92
|
+
const page = { page: true, endpoint: false };
|
|
93
|
+
expect(needsRuntimeContext(request("/docs", { headers: { accept: "*/*" } }), { route: page })).toBe(true);
|
|
94
|
+
expect(needsRuntimeContext(request("/docs", { headers: { accept: NAVIGATION } }), { route: page })).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("follows SvelteKit's page or endpoint choice when an asset route has both", () => {
|
|
98
|
+
const both = { page: true, endpoint: true };
|
|
99
|
+
const docs = (options: RequestOptions) =>
|
|
100
|
+
needsRuntimeContext(request("/docs/intro", { routeId: "/docs/[slug]", ...options }), { route: both });
|
|
101
|
+
expect(docs({ headers: { accept: NAVIGATION } })).toBe(true);
|
|
102
|
+
expect(docs({ isDataRequest: true })).toBe(true);
|
|
103
|
+
expect(docs({ method: "POST", headers: { accept: "application/json", "x-sveltekit-action": "true" } })).toBe(true);
|
|
104
|
+
expect(docs({ headers: { accept: IMAGE } })).toBe(false);
|
|
105
|
+
expect(docs({ headers: { accept: "*/*" } })).toBe(false);
|
|
106
|
+
expect(docs({ method: "PUT", headers: { accept: NAVIGATION } })).toBe(false);
|
|
107
|
+
// Without route information the same negotiation applies.
|
|
108
|
+
expect(needsRuntimeContext(request("/docs/intro", { headers: { accept: IMAGE } }))).toBe(false);
|
|
109
|
+
expect(needsRuntimeContext(request("/docs/intro", { headers: { accept: NAVIGATION } }))).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("prefersHtml", () => {
|
|
114
|
+
it("follows SvelteKit's page and endpoint negotiation", () => {
|
|
115
|
+
expect(prefersHtml(NAVIGATION)).toBe(true);
|
|
116
|
+
expect(prefersHtml("text/*")).toBe(true);
|
|
117
|
+
expect(prefersHtml("*/*")).toBe(false);
|
|
118
|
+
expect(prefersHtml(null)).toBe(false);
|
|
119
|
+
expect(prefersHtml(IMAGE)).toBe(false);
|
|
120
|
+
expect(prefersHtml("application/json, text/html;q=0.5")).toBe(true);
|
|
121
|
+
expect(prefersHtml("application/json, */*;q=0.9, text/html;q=0.5")).toBe(false);
|
|
122
|
+
expect(prefersHtml("*/*;q=0.9, text/html")).toBe(true);
|
|
123
|
+
expect(prefersHtml("text/html;q=0, */*")).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|