@beechcms/api 0.4.0-preview.9 → 0.4.1
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/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/index-CH13idU1.js +554 -0
- package/assets/dashboard/assets/index-CewtCjom.css +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +213 -213
- package/package.json +2 -2
- package/src/auth/bcrypt-hash-provider.ts +20 -0
- package/src/auth/constants.ts +10 -10
- package/src/auth/generate-refresh-token.test.ts +19 -0
- package/src/auth/hash-provider.test.ts +46 -0
- package/src/auth/in-memory-hash-provider.ts +13 -0
- package/src/auth/jose-token-service.ts +55 -0
- package/src/auth/login.test.ts +92 -0
- package/src/auth/login.ts +74 -91
- package/src/auth/refresh.ts +5 -127
- package/src/auth/static-token-service.ts +18 -0
- package/src/auth/token-service.test.ts +82 -0
- package/src/factory.ts +339 -303
- package/src/features/content/constants.ts +10 -0
- package/src/features/content/handlers/create.ts +157 -0
- package/src/features/content/handlers/delete.ts +80 -0
- package/src/features/content/handlers/facets.ts +45 -0
- package/src/features/content/handlers/get.ts +116 -0
- package/src/features/content/handlers/list.ts +88 -0
- package/src/features/content/handlers/update.ts +211 -0
- package/src/features/content/index.ts +20 -0
- package/src/features/draft/draft.handler.ts +283 -198
- package/src/features/draft/index.ts +1 -1
- package/src/features/email/email.provider.ts +38 -38
- package/src/features/email/email.service.ts +80 -80
- package/src/features/email/email.types.ts +98 -98
- package/src/features/email/index.ts +28 -28
- package/src/features/email/providers/resend.ts +63 -63
- package/src/features/email/templates/password-changed.ts +59 -59
- package/src/features/email/templates/password-reset.ts +64 -64
- package/src/features/email/templates/shell.ts +92 -93
- package/src/features/notifications/index.ts +1 -1
- package/src/features/notifications/notifications.handler.ts +101 -88
- package/src/features/password-reset/index.ts +15 -15
- package/src/features/password-reset/request.ts +82 -88
- package/src/features/password-reset/reset.ts +92 -110
- package/src/features/rotate-field/index.ts +1 -1
- package/src/features/rotate-field/rotate-field.handler.ts +128 -82
- package/src/features/rotate-field/rotate-field.schema.ts +13 -9
- package/src/features/schema/schema.handler.ts +16 -16
- package/src/features/settings/settings.handler.ts +301 -249
- package/src/features/setup/index.ts +91 -59
- package/src/features/stats/index.ts +1 -1
- package/src/features/stats/stats.handler.ts +430 -395
- package/src/index.ts +24 -11
- package/src/media-utils.ts +78 -78
- package/src/middleware/auth-providers.middleware.ts +32 -0
- package/src/middleware/observability.middleware.ts +52 -0
- package/src/middleware/rate-limit.middleware.ts +41 -0
- package/src/middleware/repository.middleware.ts +60 -0
- package/src/middleware/storage.middleware.ts +21 -0
- package/src/middleware.ts +47 -67
- package/src/public/access-policy.ts +23 -23
- package/src/public/api-key-middleware.ts +53 -53
- package/src/public/index.ts +12 -12
- package/src/public/problem-details.ts +48 -42
- package/src/public/public-add.ts +156 -183
- package/src/public/public-edit.ts +159 -183
- package/src/public/public-errors.ts +15 -15
- package/src/public/public-read.ts +216 -217
- package/src/public/public-routes.ts +84 -31
- package/src/public/query-builder.test.ts +220 -0
- package/src/public/query-builder.ts +152 -241
- package/src/public/rate-limit-middleware.ts +30 -42
- package/src/public/response-builder.ts +26 -26
- package/src/public/sanitize.ts +65 -65
- package/src/public/slug-utils.ts +14 -14
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +26 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +11 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +33 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +13 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +18 -0
- package/src/rate-limit/no-op-rate-limiter.ts +7 -0
- package/src/search-utils.test.ts +207 -0
- package/src/search-utils.ts +209 -192
- package/src/search.ts +61 -72
- package/src/shared/apply-policies.test.ts +77 -0
- package/src/shared/apply-policies.ts +63 -63
- package/src/shared/background-notification-service.test.ts +58 -0
- package/src/shared/background-notification-service.ts +48 -0
- package/src/shared/base.repository.d1.ts +28 -0
- package/src/shared/content-utils.test.ts +161 -0
- package/src/shared/content-utils.ts +82 -108
- package/src/shared/content.repository.d1.test.ts +312 -0
- package/src/shared/content.repository.d1.ts +382 -0
- package/src/shared/d1-activity-log.repository.test.ts +136 -0
- package/src/shared/d1-activity-log.repository.ts +101 -0
- package/src/shared/d1-activity-logger.test.ts +82 -0
- package/src/shared/d1-activity-logger.ts +63 -0
- package/src/shared/d1-analytics.repository.test.ts +74 -0
- package/src/shared/d1-analytics.repository.ts +81 -0
- package/src/shared/d1-content-scan.repository.ts +29 -0
- package/src/shared/d1-notification.repository.test.ts +124 -0
- package/src/shared/d1-notification.repository.ts +114 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +77 -0
- package/src/shared/d1-password-reset-token.repository.ts +52 -0
- package/src/shared/d1-search.repository.test.ts +83 -0
- package/src/shared/d1-search.repository.ts +84 -0
- package/src/shared/d1-session.repository.test.ts +121 -0
- package/src/shared/d1-session.repository.ts +98 -0
- package/src/shared/d1-user.repository.test.ts +147 -0
- package/src/shared/d1-user.repository.ts +109 -0
- package/src/shared/d1-widget.repository.test.ts +217 -0
- package/src/shared/d1-widget.repository.ts +337 -0
- package/src/shared/fixed-clock.ts +21 -0
- package/src/shared/fts-sync.ts +4 -4
- package/src/shared/idempotency.repository.d1.test.ts +79 -0
- package/src/shared/idempotency.repository.d1.ts +56 -0
- package/src/shared/in-memory-activity-logger.ts +15 -0
- package/src/shared/in-memory-notification-service.ts +15 -0
- package/src/shared/media.repository.d1.test.ts +103 -0
- package/src/shared/media.repository.d1.ts +64 -0
- package/src/shared/query-utils.ts +137 -137
- package/src/shared/request-utils.ts +22 -0
- package/src/shared/sequential-id-generator.ts +22 -0
- package/src/shared/storage/factory.ts +40 -0
- package/src/shared/storage/r2-binding-bucket.ts +81 -0
- package/src/shared/storage/s3-bucket.ts +163 -0
- package/src/shared/storage-utils.ts +36 -36
- package/src/shared/system-stats.repository.d1.test.ts +54 -0
- package/src/shared/system-stats.repository.d1.ts +44 -0
- package/src/types.ts +63 -36
- package/src/upload.ts +186 -335
- package/src/widget.ts +208 -349
- package/assets/dashboard/assets/index-CC-jbp6g.js +0 -554
- package/assets/dashboard/assets/index-CQODXprH.css +0 -1
- package/src/content.ts +0 -502
- package/src/features/draft/draft.test.ts +0 -315
- package/src/features/rotate-field/rotate-field.test.ts +0 -297
- package/src/shared/activity-logger.ts +0 -79
- package/src/shared/notification-service.ts +0 -56
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { S3Client, ListObjectsV2Command, ListObjectsV2CommandOutput } from '@aws-sdk/client-s3'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Calcola la dimensione totale occupata in un bucket R2 (in byte).
|
|
5
|
-
* Nota: esegue una scansione completa del bucket. Per bucket molto grandi
|
|
6
|
-
* andrebbe implementata una cache o usato Cloudflare Logpush/Analytics.
|
|
7
|
-
*/
|
|
8
|
-
export async function getBucketSize(client: S3Client, bucketName: string): Promise<number> {
|
|
9
|
-
let totalSize = 0
|
|
10
|
-
let isTruncatedFlag = true
|
|
11
|
-
let continuationToken: string | undefined = undefined
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
while (isTruncatedFlag) {
|
|
15
|
-
const command = new ListObjectsV2Command({
|
|
16
|
-
Bucket: bucketName,
|
|
17
|
-
ContinuationToken: continuationToken,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const response = (await client.send(command)) as ListObjectsV2CommandOutput
|
|
21
|
-
|
|
22
|
-
if (response.Contents) {
|
|
23
|
-
for (const obj of response.Contents) {
|
|
24
|
-
totalSize += obj.Size ?? 0
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
isTruncatedFlag = response.IsTruncated ?? false
|
|
29
|
-
continuationToken = response.NextContinuationToken
|
|
30
|
-
}
|
|
31
|
-
return totalSize
|
|
32
|
-
} catch (err) {
|
|
33
|
-
console.error('Error calculating bucket size:', err)
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
import { S3Client, ListObjectsV2Command, ListObjectsV2CommandOutput } from '@aws-sdk/client-s3'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calcola la dimensione totale occupata in un bucket R2 (in byte).
|
|
5
|
+
* Nota: esegue una scansione completa del bucket. Per bucket molto grandi
|
|
6
|
+
* andrebbe implementata una cache o usato Cloudflare Logpush/Analytics.
|
|
7
|
+
*/
|
|
8
|
+
export async function getBucketSize(client: S3Client, bucketName: string): Promise<number> {
|
|
9
|
+
let totalSize = 0
|
|
10
|
+
let isTruncatedFlag = true
|
|
11
|
+
let continuationToken: string | undefined = undefined
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
while (isTruncatedFlag) {
|
|
15
|
+
const command = new ListObjectsV2Command({
|
|
16
|
+
Bucket: bucketName,
|
|
17
|
+
ContinuationToken: continuationToken,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const response = (await client.send(command)) as ListObjectsV2CommandOutput
|
|
21
|
+
|
|
22
|
+
if (response.Contents) {
|
|
23
|
+
for (const obj of response.Contents) {
|
|
24
|
+
totalSize += obj.Size ?? 0
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
isTruncatedFlag = response.IsTruncated ?? false
|
|
29
|
+
continuationToken = response.NextContinuationToken
|
|
30
|
+
}
|
|
31
|
+
return totalSize
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error('Error calculating bucket size:', err)
|
|
34
|
+
throw new Error('Failed to calculate bucket size. Check your R2/S3 credentials and endpoint.')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { D1SystemStatsRepository } from './system-stats.repository.d1'
|
|
3
|
+
|
|
4
|
+
function makeMockDb(firstResult: unknown = null) {
|
|
5
|
+
const runMock = vi.fn().mockResolvedValue({ success: true })
|
|
6
|
+
const firstMock = vi.fn().mockResolvedValue(firstResult)
|
|
7
|
+
// bind() returns the same statement interface for chained calls.
|
|
8
|
+
// The statement also exposes first() directly for queries without bind (e.g. getStorageUsage).
|
|
9
|
+
const bindMock = vi.fn(() => ({ run: runMock, first: firstMock }))
|
|
10
|
+
const stmt = { bind: bindMock, run: runMock, first: firstMock }
|
|
11
|
+
const prepareMock = vi.fn(() => stmt)
|
|
12
|
+
return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock, firstMock }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('D1SystemStatsRepository', () => {
|
|
16
|
+
describe('incrementStorage', () => {
|
|
17
|
+
it('calls UPDATE system_stats with the byte count', async () => {
|
|
18
|
+
const { db, prepareMock, bindMock } = makeMockDb()
|
|
19
|
+
await new D1SystemStatsRepository(db).incrementStorage(512)
|
|
20
|
+
expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('UPDATE system_stats'))
|
|
21
|
+
expect(bindMock).toHaveBeenCalledWith(512)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('decrementStorage', () => {
|
|
26
|
+
it('calls UPDATE system_stats with MAX(0, ...) and the byte count', async () => {
|
|
27
|
+
const { db, prepareMock, bindMock } = makeMockDb()
|
|
28
|
+
await new D1SystemStatsRepository(db).decrementStorage(256)
|
|
29
|
+
expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('MAX(0'))
|
|
30
|
+
expect(bindMock).toHaveBeenCalledWith(256)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('setStorage', () => {
|
|
35
|
+
it('calls UPDATE system_stats with the stringified byte count', async () => {
|
|
36
|
+
const { db, prepareMock, bindMock } = makeMockDb()
|
|
37
|
+
await new D1SystemStatsRepository(db).setStorage(1024)
|
|
38
|
+
expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('UPDATE system_stats'))
|
|
39
|
+
expect(bindMock).toHaveBeenCalledWith('1024')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('getStorageUsage', () => {
|
|
44
|
+
it('parses and returns the integer value from D1', async () => {
|
|
45
|
+
const { db } = makeMockDb({ value: '2048' })
|
|
46
|
+
expect(await new D1SystemStatsRepository(db).getStorageUsage()).toBe(2048)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns 0 when D1 returns null (stat not initialised)', async () => {
|
|
50
|
+
const { db } = makeMockDb(null)
|
|
51
|
+
expect(await new D1SystemStatsRepository(db).getStorageUsage()).toBe(0)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SystemStatsRepository } from '@beechcms/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implementation of SystemStatsRepository using Cloudflare D1.
|
|
5
|
+
* Manages global system statistics such as total storage usage.
|
|
6
|
+
*/
|
|
7
|
+
export class D1SystemStatsRepository implements SystemStatsRepository {
|
|
8
|
+
constructor(private database: D1Database) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Increments the total storage usage count.
|
|
12
|
+
*/
|
|
13
|
+
async incrementStorage(byteCount: number): Promise<void> {
|
|
14
|
+
await this.database.prepare(
|
|
15
|
+
"UPDATE system_stats SET value = CAST(value AS INTEGER) + ? WHERE id = 'total_storage_bytes'"
|
|
16
|
+
).bind(byteCount).run()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decrements the total storage usage count, ensuring it doesn't fall below zero.
|
|
21
|
+
*/
|
|
22
|
+
async decrementStorage(byteCount: number): Promise<void> {
|
|
23
|
+
await this.database.prepare(
|
|
24
|
+
"UPDATE system_stats SET value = MAX(0, CAST(value AS INTEGER) - ?) WHERE id = 'total_storage_bytes'"
|
|
25
|
+
).bind(byteCount).run()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sets the total storage usage count to a specific value.
|
|
30
|
+
*/
|
|
31
|
+
async setStorage(byteCount: number): Promise<void> {
|
|
32
|
+
await this.database.prepare(
|
|
33
|
+
"UPDATE system_stats SET value = ? WHERE id = 'total_storage_bytes'"
|
|
34
|
+
).bind(String(byteCount)).run()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Retrieves the current total storage usage in bytes.
|
|
39
|
+
*/
|
|
40
|
+
async getStorageUsage(): Promise<number> {
|
|
41
|
+
const statsRecordResult = await this.database.prepare("SELECT value FROM system_stats WHERE id = 'total_storage_bytes'").first<{ value: string }>()
|
|
42
|
+
return statsRecordResult ? parseInt(statsRecordResult.value, 10) : 0
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,36 +1,63 @@
|
|
|
1
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import type { Seed } from '@beechcms/core'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import type { Seed, ContentRepository, IdempotencyRepository, BeechBucket, MediaRepository, SystemStatsRepository, IHashProvider, ITokenService, IUserRepository, ISessionRepository, IPasswordResetTokenRepository, IActivityLogger, IActivityLogRepository, INotificationRepository, INotificationService, IWidgetRepository, ISearchRepository, IAnalyticsRepository, IContentScanRepository, ISeedRegistry, IClock, IIdGenerator } from '@beechcms/core'
|
|
3
|
+
import type { IRateLimiterRegistry } from './middleware/rate-limit.middleware'
|
|
4
|
+
|
|
5
|
+
export interface Env {
|
|
6
|
+
DB: D1Database
|
|
7
|
+
JWT_SECRET: string
|
|
8
|
+
JWT_ISSUER?: string
|
|
9
|
+
JWT_AUDIENCE?: string
|
|
10
|
+
R2_ACCESS_KEY_ID?: string
|
|
11
|
+
R2_SECRET_ACCESS_KEY?: string
|
|
12
|
+
R2_ENDPOINT?: string
|
|
13
|
+
R2_BUCKET_NAME?: string
|
|
14
|
+
LOGIN_RATE_LIMITER?: RateLimit
|
|
15
|
+
REFRESH_RATE_LIMITER?: RateLimit
|
|
16
|
+
PUBLIC_READ_RATE_LIMITER?: RateLimit
|
|
17
|
+
PUBLIC_WRITE_RATE_LIMITER?: RateLimit
|
|
18
|
+
CORS_ORIGINS?: string
|
|
19
|
+
PUBLIC_READ_API_KEY?: string
|
|
20
|
+
PUBLIC_WRITE_API_KEY?: string
|
|
21
|
+
PUBLIC_PUBLISHED_ONLY?: string
|
|
22
|
+
PUBLIC_IDEMPOTENCY_TTL_SECONDS?: string
|
|
23
|
+
MEDIA_BASE_URL?: string
|
|
24
|
+
MEDIA_CDN_URL?: string
|
|
25
|
+
RESEND_API_KEY?: string
|
|
26
|
+
APP_URL?: string
|
|
27
|
+
EMAIL_FROM?: string
|
|
28
|
+
FORGOT_PASSWORD_RATE_LIMITER?: RateLimit
|
|
29
|
+
RESET_PASSWORD_RATE_LIMITER?: RateLimit
|
|
30
|
+
ENV?: string
|
|
31
|
+
DATE_FORMAT?: string
|
|
32
|
+
ASSETS?: Fetcher
|
|
33
|
+
MEDIA_BUCKET?: R2Bucket
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Variables {
|
|
37
|
+
jwtPayload: { sub: string; email?: string; name?: string | null }
|
|
38
|
+
getSeed: (slug: string) => Seed | null
|
|
39
|
+
seedRegistry: ISeedRegistry
|
|
40
|
+
repository: ContentRepository
|
|
41
|
+
idempotencyRepository: IdempotencyRepository
|
|
42
|
+
bucket: BeechBucket
|
|
43
|
+
mediaRepository: MediaRepository
|
|
44
|
+
systemStatsRepository: SystemStatsRepository
|
|
45
|
+
hashProvider: IHashProvider
|
|
46
|
+
tokenService: ITokenService
|
|
47
|
+
userRepository: IUserRepository
|
|
48
|
+
sessionRepository: ISessionRepository
|
|
49
|
+
passwordResetTokenRepository: IPasswordResetTokenRepository
|
|
50
|
+
rateLimiters: IRateLimiterRegistry
|
|
51
|
+
activityLogger: IActivityLogger
|
|
52
|
+
activityLogRepository: IActivityLogRepository
|
|
53
|
+
notificationRepository: INotificationRepository
|
|
54
|
+
notificationService: INotificationService
|
|
55
|
+
widgetRepository: IWidgetRepository
|
|
56
|
+
searchRepository: ISearchRepository
|
|
57
|
+
analyticsRepository: IAnalyticsRepository
|
|
58
|
+
contentScanRepository: IContentScanRepository
|
|
59
|
+
clock: IClock
|
|
60
|
+
idGenerator: IIdGenerator
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type AppEnv = { Bindings: Env; Variables: Variables }
|