@plutocms/supabase 0.8.3 → 0.8.4
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.4](https://github.com/plutocms/supabase/compare/v0.8.3...v0.8.4) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **security:** validate request bodies with zod ([#69](https://github.com/plutocms/supabase/issues/69)) ([94f179b](https://github.com/plutocms/supabase/commit/94f179bbfdd67a55c18764a5bf6fde0cc6635936))
|
|
9
|
+
|
|
3
10
|
## [0.8.3](https://github.com/plutocms/supabase/compare/v0.8.2...v0.8.3) (2026-09-13)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.4",
|
|
5
5
|
"trustedDependencies": [
|
|
6
6
|
"@parcel/watcher",
|
|
7
7
|
"@plutocms/pluto",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"@plutocms/pluto": "^0.10.0",
|
|
43
43
|
"@plutocms/utils": "^0.2.1",
|
|
44
44
|
"postgres": "^3.4.9",
|
|
45
|
-
"supabase": "^2.109.1"
|
|
45
|
+
"supabase": "^2.109.1",
|
|
46
|
+
"zod": "^4.6.4"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@antfu/eslint-config": "^6.2.0",
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { serverSupabaseClient } from '#supabase/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { parseBody } from '../../utils/validate'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// Supabase Auth (GoTrue) rejects a password under 6 characters with its own
|
|
6
|
+
// opaque error. Matching that minimum here turns an empty/short password
|
|
7
|
+
// into the same clean 400 shape as every other validation failure, instead
|
|
8
|
+
// of a raw auth-service error reaching the caller.
|
|
9
|
+
const signupSchema = z.object({
|
|
10
|
+
username: z.string().trim().min(3, 'Username must be at least 3 characters.').max(100),
|
|
11
|
+
display_name: z.string().trim().min(1, 'Display name is required.').max(200),
|
|
12
|
+
email: z.string().trim().email('Email must be a valid email address.'),
|
|
13
|
+
password: z.string().min(6, 'Password must be at least 6 characters.'),
|
|
14
|
+
})
|
|
9
15
|
|
|
10
16
|
export default defineEventHandler(async (event) => {
|
|
11
17
|
const client = await serverSupabaseClient<Database>(event)
|
|
12
18
|
|
|
13
|
-
const body = await
|
|
14
|
-
|
|
15
|
-
if (!body.username || body.username.length < 3) {
|
|
16
|
-
return {
|
|
17
|
-
success: false,
|
|
18
|
-
message: 'Username must be at least 3 characters.',
|
|
19
|
-
}
|
|
20
|
-
}
|
|
19
|
+
const body = await parseBody(event, signupSchema)
|
|
21
20
|
|
|
22
21
|
const { data, error } = await client.auth.signUp({
|
|
23
22
|
email: body.email,
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { PlutoMigrationFile } from '../../../shared/types/migrations'
|
|
2
2
|
import type { MigrationFileResult } from '../../utils/migrations'
|
|
3
|
+
import { z } from 'zod'
|
|
3
4
|
import { requireAdmin } from '../../utils/admin-guard'
|
|
4
5
|
import { persistDatabaseUrl } from '../../utils/env-file'
|
|
5
6
|
import { resolveConnectionString, runPendingMigrations } from '../../utils/migrations'
|
|
6
7
|
import { scrubConnectionString } from '../../utils/scrub-connection-string'
|
|
7
8
|
import { regenerateSupabaseTypes } from '../../utils/typegen'
|
|
9
|
+
import { parseBody } from '../../utils/validate'
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
// The whole body is optional: an already-configured DATABASE_URL means the
|
|
12
|
+
// caller need not (and, per resolveConnectionString, cannot) supply one.
|
|
13
|
+
const runMigrationsSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
connectionString: z.string().trim().min(1, 'Connection string must not be empty.').optional(),
|
|
16
|
+
})
|
|
17
|
+
.optional()
|
|
12
18
|
|
|
13
19
|
export default defineEventHandler(async (event) => {
|
|
14
20
|
// requireAdmin specifically, not a named capability — see admin-guard.ts
|
|
@@ -16,7 +22,7 @@ export default defineEventHandler(async (event) => {
|
|
|
16
22
|
// (which defines the capability system) has been applied.
|
|
17
23
|
await requireAdmin(event)
|
|
18
24
|
|
|
19
|
-
const body = await
|
|
25
|
+
const body = await parseBody(event, runMigrationsSchema)
|
|
20
26
|
|
|
21
27
|
// Connection string precedence: an already-configured DATABASE_URL always
|
|
22
28
|
// wins, and a body-supplied string is ignored completely in that case.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { serverSupabaseClient } from '#supabase/server'
|
|
2
|
+
import { z } from 'zod'
|
|
2
3
|
import { requireCapability } from '../../utils/capability-guard'
|
|
4
|
+
import { parseBody } from '../../utils/validate'
|
|
3
5
|
|
|
4
6
|
// A key is lowercase segments separated by dots, e.g. 'website_title' or
|
|
5
7
|
// 'blog.posts_per_page'. Mirrors the settings_setting_key_format check
|
|
@@ -10,43 +12,26 @@ import { requireCapability } from '../../utils/capability-guard'
|
|
|
10
12
|
const SETTING_KEY_PATTERN = /^[a-z0-9_]+(?:\.[a-z0-9_]+)*$/
|
|
11
13
|
const MAX_SETTING_VALUE_LENGTH = 10_000
|
|
12
14
|
|
|
15
|
+
const settingsUpdateSchema = z.record(
|
|
16
|
+
z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(
|
|
19
|
+
SETTING_KEY_PATTERN,
|
|
20
|
+
'Setting key must use lowercase letters, digits, underscores, and dot-separated segments.'
|
|
21
|
+
),
|
|
22
|
+
z.string().max(MAX_SETTING_VALUE_LENGTH, 'Setting value is too long.')
|
|
23
|
+
)
|
|
24
|
+
|
|
13
25
|
export default defineEventHandler(async (event) => {
|
|
14
26
|
await requireCapability(event, 'settings:manage')
|
|
15
27
|
|
|
16
28
|
const client = await serverSupabaseClient<Database>(event)
|
|
17
|
-
const body = await
|
|
18
|
-
|
|
19
|
-
if (!body || typeof body !== 'object') {
|
|
20
|
-
throw createError({ statusCode: 400, statusMessage: 'No payload sent.' })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const transformed = Object.entries(body).map(([key, value]) => {
|
|
24
|
-
if (!SETTING_KEY_PATTERN.test(key)) {
|
|
25
|
-
throw createError({
|
|
26
|
-
statusCode: 400,
|
|
27
|
-
statusMessage: `Invalid setting key: ${key}`,
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (typeof value !== 'string') {
|
|
32
|
-
throw createError({
|
|
33
|
-
statusCode: 400,
|
|
34
|
-
statusMessage: `Setting "${key}" must be a string.`,
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (value.length > MAX_SETTING_VALUE_LENGTH) {
|
|
39
|
-
throw createError({
|
|
40
|
-
statusCode: 400,
|
|
41
|
-
statusMessage: `Setting "${key}" is too long.`,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
29
|
+
const body = await parseBody(event, settingsUpdateSchema)
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
31
|
+
const transformed = Object.entries(body).map(([key, value]) => ({
|
|
32
|
+
setting_key: key,
|
|
33
|
+
setting_value: value,
|
|
34
|
+
}))
|
|
50
35
|
|
|
51
36
|
const { data, error } = await client
|
|
52
37
|
.from('settings')
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { PlutoMigrationFile } from '../../../shared/types/migrations'
|
|
2
2
|
import postgres from 'postgres'
|
|
3
|
+
import { z } from 'zod'
|
|
3
4
|
import { persistDatabaseUrl } from '../../utils/env-file'
|
|
4
5
|
import { runPendingMigrations } from '../../utils/migrations'
|
|
5
6
|
import { scrubConnectionString } from '../../utils/scrub-connection-string'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { parseBody } from '../../utils/validate'
|
|
8
|
+
|
|
9
|
+
// Only checks that a connection string was actually sent. A Postgres
|
|
10
|
+
// connection string can take many valid forms (postgres:// URL, key=value
|
|
11
|
+
// DSN, and so on), so this route's real security model is the
|
|
12
|
+
// DATABASE_URL precedence check above, not a stricter format check here.
|
|
13
|
+
const setupSchema = z.object({
|
|
14
|
+
connectionString: z.string().trim().min(1, 'Connection string must not be empty.'),
|
|
15
|
+
})
|
|
10
16
|
|
|
11
17
|
export default defineEventHandler(async (event) => {
|
|
12
18
|
// An already-configured DATABASE_URL means setup has already completed.
|
|
@@ -23,7 +29,7 @@ export default defineEventHandler(async (event) => {
|
|
|
23
29
|
})
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
const body = await
|
|
32
|
+
const body = await parseBody(event, setupSchema)
|
|
27
33
|
|
|
28
34
|
const config = useRuntimeConfig()
|
|
29
35
|
// Nuxt's schema inference narrows `plutoLayerMigrations` to whatever
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { H3Event } from 'h3'
|
|
2
|
+
import type { ZodSchema } from 'zod'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One field-level validation error.
|
|
6
|
+
*
|
|
7
|
+
* Matches `ContentValidationError` from `@plutocms/pluto`'s
|
|
8
|
+
* `shared/utils/content-validate.ts`, so a zod failure and a content-model
|
|
9
|
+
* failure look the same to a caller.
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationError {
|
|
12
|
+
field: string
|
|
13
|
+
message: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads a request body and checks it against a zod schema.
|
|
18
|
+
*
|
|
19
|
+
* On success, returns the parsed, typed body. On failure, throws the same
|
|
20
|
+
* 400 shape `@plutocms/pluto`'s generic content routes throw for a content
|
|
21
|
+
* validation failure: `{ statusCode: 400, statusMessage: 'Validation
|
|
22
|
+
* failed.', data: { errors } }`, with `errors` as `{ field, message }[]`.
|
|
23
|
+
* Every route in this workspace should call this helper instead of
|
|
24
|
+
* hand-rolling its own zod error handling, so a malformed request fails
|
|
25
|
+
* the same way everywhere.
|
|
26
|
+
*
|
|
27
|
+
* A nested field path (for example `address.zip`) is joined with `.` to
|
|
28
|
+
* build `field`.
|
|
29
|
+
*/
|
|
30
|
+
export async function parseBody<T>(event: H3Event, schema: ZodSchema<T>): Promise<T> {
|
|
31
|
+
const body = await readBody(event)
|
|
32
|
+
const result = schema.safeParse(body)
|
|
33
|
+
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
const errors: ValidationError[] = result.error.issues.map((issue) => ({
|
|
36
|
+
field: issue.path.join('.'),
|
|
37
|
+
message: issue.message,
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
throw createError({ statusCode: 400, statusMessage: 'Validation failed.', data: { errors } })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result.data
|
|
44
|
+
}
|