@stacksjs/defaults 0.72.5 → 0.72.7
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/app/Actions/Auth/GenerateAuthenticationAction.ts +6 -6
- package/app/Actions/Auth/GenerateRegistrationAction.ts +10 -5
- package/app/Actions/Auth/RegisterAction.ts +5 -2
- package/app/Actions/Auth/VerifyAuthenticationAction.ts +5 -3
- package/app/Actions/Auth/VerifyRegistrationAction.ts +7 -3
- package/app/Actions/Billable/CheckoutAction.ts +14 -1
- package/app/Actions/Cms/AuthorDestroyAction.ts +1 -1
- package/app/Actions/Cms/AuthorShowAction.ts +1 -1
- package/app/Actions/Cms/AuthorUpdateAction.ts +1 -1
- package/app/Actions/Cms/CategorizableDestroyAction.ts +1 -1
- package/app/Actions/Cms/CategorizableShowAction.ts +1 -1
- package/app/Actions/Cms/CategorizableUpdateAction.ts +2 -3
- package/app/Actions/Cms/CommentDestroyAction.ts +1 -1
- package/app/Actions/Cms/CommentShowAction.ts +1 -1
- package/app/Actions/Cms/CommentUpdateAction.ts +1 -1
- package/app/Actions/Cms/PageDestroyAction.ts +1 -1
- package/app/Actions/Cms/PageStoreAction.ts +13 -13
- package/app/Actions/Cms/PageUpdateAction.ts +11 -8
- package/app/Actions/Cms/PostDestroyAction.ts +1 -1
- package/app/Actions/Cms/PostShowAction.ts +1 -1
- package/app/Actions/Cms/PostUpdateAction.ts +1 -1
- package/app/Actions/Cms/TaggableDestroyAction.ts +1 -1
- package/app/Actions/Cms/TaggableShowAction.ts +1 -1
- package/app/Actions/Cms/TaggableUpdateAction.ts +2 -2
- package/app/Actions/Commerce/GiftCardUpdateBalanceAction.ts +1 -1
- package/app/Actions/Commerce/WaitlistProductAnalyticsAction.ts +6 -2
- package/app/Actions/Commerce/WaitlistProductQuantityDistributionAction.ts +6 -2
- package/app/Actions/Commerce/WaitlistProductStatusAction.ts +8 -2
- package/app/Actions/Commerce/WaitlistProductTimeSeriesAction.ts +6 -2
- package/app/Actions/Dashboard/Analytics/campaign-analytics.ts +6 -1
- package/app/Actions/Dashboard/Analytics/request-analytics.ts +3 -3
- package/app/Actions/Dashboard/Buddy/buddy-chat.ts +8 -0
- package/app/Actions/Dashboard/Ci/runner-pressure-monitor.ts +2 -1
- package/app/Actions/Dashboard/Cloud/cloud-overview.ts +3 -1
- package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +12 -4
- package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +5 -3
- package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +3 -3
- package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +4 -4
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +20 -9
- package/app/Actions/Dashboard/Content/file-manager.ts +3 -2
- package/app/Actions/Dashboard/DashboardHomeAction.ts +1 -1
- package/app/Actions/Dashboard/DashboardStatsAction.ts +7 -4
- package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +2 -2
- package/app/Actions/Dashboard/Deployments/deployment-input.ts +3 -3
- package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +12 -0
- package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +3 -3
- package/app/Actions/Dashboard/Jobs/job-records.ts +3 -3
- package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +5 -3
- package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +5 -3
- package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +1 -1
- package/app/Actions/Dashboard/Kanban/kanban-model.ts +3 -3
- package/app/Actions/Dashboard/Kanban/kanban-response.ts +2 -1
- package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +1 -0
- package/app/Actions/Dashboard/Library/GetReleases.ts +8 -3
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +3 -3
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +7 -0
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +5 -1
- package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +12 -9
- package/app/Actions/Dashboard/Operations/control-plane.ts +16 -4
- package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +1 -1
- package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +23 -14
- package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -1
- package/app/Actions/Dashboard/Settings/mail-settings.ts +12 -4
- package/app/Actions/Dashboard/Source/source-inventory.ts +6 -2
- package/app/Actions/Dashboard/dashboard-response.ts +2 -1
- package/app/Actions/HealthAction.ts +11 -6
- package/app/Actions/Locale/SetLocaleAction.ts +3 -1
- package/app/Actions/Monitoring/ErrorShowAction.ts +5 -0
- package/app/Actions/Payment/CreatePaymentIntentAction.ts +3 -1
- package/app/Actions/SendWelcomeEmail.ts +3 -1
- package/app/Actions/Teams/team-response.ts +2 -1
- package/app/Actions/TestErrorAction.ts +1 -1
- package/app/Jobs/PruneModelsJob.ts +1 -1
- package/app/Jobs/PruneQueryLogsJob.ts +3 -1
- package/app/Mail/OrderConfirmation.ts +18 -2
- package/app/Middleware/Abilities.ts +3 -1
- package/app/Middleware/Can.ts +3 -3
- package/app/Middleware/Compress.ts +3 -1
- package/app/Middleware/Csrf.ts +10 -4
- package/app/Middleware/EnsureEmailIsVerified.ts +2 -1
- package/app/Middleware/Permission.ts +2 -1
- package/app/Middleware/Role.ts +2 -1
- package/app/Middleware/Team.ts +2 -1
- package/app/Middleware/Throttle.ts +1 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/functions/dashboard/data.ts +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { FailedJob, Job } from '@stacksjs/orm'
|
|
3
|
-
import { getGlobalMetrics } from '@stacksjs/queue'
|
|
3
|
+
import { getGlobalMetrics, queuedJobState } from '@stacksjs/queue'
|
|
4
4
|
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
5
|
|
|
6
6
|
interface QueueBucket {
|
|
@@ -11,9 +11,11 @@ interface QueueBucket {
|
|
|
11
11
|
total: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// A queued job has no `status` column - `queuedJobState` in @stacksjs/queue
|
|
15
|
+
// holds the rule (reserved_at means a worker has it; available_at in the
|
|
16
|
+
// future means it is not eligible yet), and the queue's own health check reads
|
|
17
|
+
// the same one. A row that finished is deleted by the worker, so 'completed'
|
|
18
|
+
// is not a state the jobs table can be in; failures live in `failed_jobs`.
|
|
17
19
|
|
|
18
20
|
export default new Action({
|
|
19
21
|
name: 'QueueStatsAction',
|
|
@@ -21,19 +23,29 @@ export default new Action({
|
|
|
21
23
|
method: 'GET',
|
|
22
24
|
async handle() {
|
|
23
25
|
try {
|
|
24
|
-
const [totalJobs, failedJobCount, allJobs] = await Promise.all([
|
|
26
|
+
const [totalJobs, failedJobCount, allJobs, allFailedJobs] = await Promise.all([
|
|
25
27
|
Job.count(),
|
|
26
28
|
FailedJob.count(),
|
|
27
29
|
Job.all(),
|
|
30
|
+
FailedJob.all(),
|
|
28
31
|
])
|
|
29
32
|
|
|
30
33
|
const queueMap: Record<string, QueueBucket> = {}
|
|
34
|
+
|
|
35
|
+
// Per-queue failure counts come from failed_jobs, which is where a
|
|
36
|
+
// failure actually lands. This column read `status === 'failed'` off the
|
|
37
|
+
// jobs table before, and so was always zero.
|
|
38
|
+
const failedByQueue = new Map<string, number>()
|
|
39
|
+
for (const f of allFailedJobs) {
|
|
40
|
+
const queueName = String(f.get('queue') || 'default')
|
|
41
|
+
failedByQueue.set(queueName, (failedByQueue.get(queueName) ?? 0) + 1)
|
|
42
|
+
}
|
|
31
43
|
let active = 0
|
|
32
|
-
|
|
44
|
+
const completed = 0
|
|
33
45
|
|
|
34
46
|
for (const j of allJobs) {
|
|
35
47
|
const queueName = String(j.get('queue') || 'default')
|
|
36
|
-
const status =
|
|
48
|
+
const status = queuedJobState({ reserved_at: j.get('reserved_at'), available_at: j.get('available_at') })
|
|
37
49
|
|
|
38
50
|
if (!queueMap[queueName]) {
|
|
39
51
|
queueMap[queueName] = { pending: 0, active: 0, completed: 0, failed: 0, total: 0 }
|
|
@@ -41,16 +53,13 @@ export default new Action({
|
|
|
41
53
|
const bucket = queueMap[queueName]
|
|
42
54
|
bucket.total++
|
|
43
55
|
|
|
44
|
-
if (
|
|
45
|
-
else if (isActive(status)) {
|
|
56
|
+
if (status === 'processing') {
|
|
46
57
|
bucket.active++
|
|
47
58
|
active++
|
|
48
59
|
}
|
|
49
|
-
else
|
|
50
|
-
bucket.
|
|
51
|
-
completed++
|
|
60
|
+
else {
|
|
61
|
+
bucket.pending++
|
|
52
62
|
}
|
|
53
|
-
else if (status === 'failed') bucket.failed++
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
const queues = Object.entries(queueMap).map(([name, data]) => ({
|
|
@@ -59,7 +68,7 @@ export default new Action({
|
|
|
59
68
|
pending: data.pending,
|
|
60
69
|
active: data.active,
|
|
61
70
|
completed: data.completed,
|
|
62
|
-
failed: data.failed,
|
|
71
|
+
failed: failedByQueue.get(name) ?? data.failed,
|
|
63
72
|
total: data.total,
|
|
64
73
|
}))
|
|
65
74
|
|
|
@@ -131,7 +131,7 @@ async function loadSearchableModels(): Promise<SearchableModel[]> {
|
|
|
131
131
|
const search = definition.traits?.useSearch
|
|
132
132
|
if (!search || typeof search !== 'object' || !Array.isArray(search.searchable)) continue
|
|
133
133
|
|
|
134
|
-
const fields = search.searchable
|
|
134
|
+
const fields: string[] = (search.searchable as unknown[])
|
|
135
135
|
.map((field: unknown) => snakeCase(String(field)))
|
|
136
136
|
.filter((field: string) => isSafeIdentifier(field) && !HIDDEN_FIELDS.has(field))
|
|
137
137
|
const table = String(definition.table || pluralize(snakeCase(name)))
|
|
@@ -51,11 +51,19 @@ export interface MailSettingsState {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Whatever the settings form posted.
|
|
56
|
+
*
|
|
57
|
+
* Every field is optional AND `unknown`: this is the raw request body, and
|
|
58
|
+
* `updateMailSettings` runs each value through `stringValue` and validates it.
|
|
59
|
+
* Declaring the four core fields as required meant `request.all()` - the only
|
|
60
|
+
* thing any caller has - did not satisfy the type it was written for.
|
|
61
|
+
*/
|
|
54
62
|
export interface MailSettingsInput {
|
|
55
|
-
revision
|
|
56
|
-
driver
|
|
57
|
-
fromName
|
|
58
|
-
fromAddress
|
|
63
|
+
revision?: unknown
|
|
64
|
+
driver?: unknown
|
|
65
|
+
fromName?: unknown
|
|
66
|
+
fromAddress?: unknown
|
|
59
67
|
smtp?: Record<string, unknown>
|
|
60
68
|
ses?: Record<string, unknown>
|
|
61
69
|
sendgrid?: Record<string, unknown>
|
|
@@ -50,8 +50,12 @@ export function parseCommandSource(
|
|
|
50
50
|
modifiedAt = '',
|
|
51
51
|
): SourceInventoryItem {
|
|
52
52
|
const command = source.match(/\.command\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]([^'"`]+)['"`])?/)
|
|
53
|
-
const sourceAliases = [...source.matchAll(/\.alias\(\s*['"`]([^'"`]+)['"`]\s*\)/g)]
|
|
54
|
-
|
|
53
|
+
const sourceAliases = [...source.matchAll(/\.alias\(\s*['"`]([^'"`]+)['"`]\s*\)/g)]
|
|
54
|
+
.map(match => match[1])
|
|
55
|
+
.filter((alias): alias is string => alias !== undefined)
|
|
56
|
+
const options = [...source.matchAll(/\.option\(\s*['"`]([^'"`]+)['"`]/g)]
|
|
57
|
+
.map(match => match[1])
|
|
58
|
+
.filter((option): option is string => option !== undefined)
|
|
55
59
|
|
|
56
60
|
return {
|
|
57
61
|
name: command?.[1] || registeredSignature,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import type { ResponseStatus } from '@stacksjs/bun-router'
|
|
1
2
|
import { response } from '@stacksjs/router'
|
|
2
3
|
|
|
3
4
|
export function dashboardOperationalError(
|
|
4
5
|
error: unknown,
|
|
5
6
|
message: string,
|
|
6
7
|
action: string,
|
|
7
|
-
status = 503,
|
|
8
|
+
status: ResponseStatus = 503,
|
|
8
9
|
): Response {
|
|
9
10
|
console.error(`[dashboard/api] ${action} failed:`, error)
|
|
10
11
|
return response.json({ message }, status)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Health check action with detailed service status
|
|
2
2
|
// This overrides the default framework health check
|
|
3
3
|
|
|
4
|
-
import { route } from '@stacksjs/router'
|
|
4
|
+
import { response, route } from '@stacksjs/router'
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
name: 'Health',
|
|
@@ -70,13 +70,17 @@ async function getServiceHealth() {
|
|
|
70
70
|
|
|
71
71
|
// Queue Health
|
|
72
72
|
try {
|
|
73
|
-
|
|
73
|
+
// The queue package owns what "healthy" means for a queue - depth
|
|
74
|
+
// thresholds, stuck workers, jobs whose reserve has gone stale - and its
|
|
75
|
+
// own worker health endpoint reads the same function. This used to count
|
|
76
|
+
// rows where `status = 'pending'`, a column the jobs table does not have.
|
|
77
|
+
const { checkQueueHealth } = await import('@stacksjs/queue')
|
|
74
78
|
const queueStart = Date.now()
|
|
75
|
-
const
|
|
79
|
+
const queueHealth = await checkQueueHealth()
|
|
76
80
|
const queueLatency = Date.now() - queueStart
|
|
77
81
|
services.push({
|
|
78
82
|
name: 'Queue',
|
|
79
|
-
status:
|
|
83
|
+
status: queueHealth.status === 'unhealthy' ? 'critical' : queueHealth.status,
|
|
80
84
|
latency: `${queueLatency}ms`,
|
|
81
85
|
uptime: '99.9%',
|
|
82
86
|
})
|
|
@@ -104,9 +108,10 @@ async function getServiceHealth() {
|
|
|
104
108
|
// Also register as a route for redundancy
|
|
105
109
|
route.get('/health', async () => {
|
|
106
110
|
const services = await getServiceHealth()
|
|
107
|
-
|
|
111
|
+
// A route handler returns a Response; a bare object is not one.
|
|
112
|
+
return response.json({
|
|
108
113
|
status: 'ok',
|
|
109
114
|
timestamp: Date.now(),
|
|
110
115
|
services,
|
|
111
|
-
}
|
|
116
|
+
})
|
|
112
117
|
})
|
|
@@ -45,7 +45,9 @@ export default new Action({
|
|
|
45
45
|
|
|
46
46
|
return createLocaleSwitchResponse(request as unknown as Request, String(request.param('locale') ?? ''), {
|
|
47
47
|
locales,
|
|
48
|
-
|
|
48
|
+
// `locales[0]` is `string | undefined` under noUncheckedIndexedAccess,
|
|
49
|
+
// and this list is never empty by construction - but say so.
|
|
50
|
+
defaultLocale: app?.locale ?? locales[0] ?? 'en',
|
|
49
51
|
})
|
|
50
52
|
},
|
|
51
53
|
})
|
|
@@ -7,7 +7,12 @@ export default new Action({
|
|
|
7
7
|
description: 'Fetch a single error by ID',
|
|
8
8
|
method: 'GET',
|
|
9
9
|
async handle(request: RequestInstance) {
|
|
10
|
+
// `getParamAsInt` returns null for a param that is not an integer, which
|
|
11
|
+
// is a bad request rather than a lookup for id `null`.
|
|
10
12
|
const id = request.getParamAsInt('id')
|
|
13
|
+
if (id === null)
|
|
14
|
+
return response.json({ error: 'A numeric error id is required' }, 422)
|
|
15
|
+
|
|
11
16
|
const result = await errors.fetchById(id)
|
|
12
17
|
|
|
13
18
|
if (!result) {
|
|
@@ -21,7 +21,9 @@ export default new Action({
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const paymentIntent = await user?.paymentIntent({
|
|
24
|
-
|
|
24
|
+
// The Product model's column is `price`; `unit_price` has never been
|
|
25
|
+
// one, so this read was undefined and the intent was created for NaN.
|
|
26
|
+
amount: Number(product.get('price')),
|
|
25
27
|
currency: 'usd',
|
|
26
28
|
payment_method_types: ['card'],
|
|
27
29
|
})
|
|
@@ -7,7 +7,9 @@ export interface WelcomeEmailParams {
|
|
|
7
7
|
name?: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Invoked by the event system with the `user:registered` payload rather than
|
|
11
|
+
// by the router with a request - see app/Events.ts.
|
|
12
|
+
export default new Action<string, undefined, '', WelcomeEmailParams>({
|
|
11
13
|
name: 'SendWelcomeEmail',
|
|
12
14
|
description: 'Sends a welcome email to newly registered users',
|
|
13
15
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResponseStatus } from '@stacksjs/bun-router'
|
|
1
2
|
import { response } from '@stacksjs/router'
|
|
2
3
|
|
|
3
4
|
export class TeamStateConflictError extends Error {
|
|
@@ -11,7 +12,7 @@ export function teamOperationalError(
|
|
|
11
12
|
error: unknown,
|
|
12
13
|
message: string,
|
|
13
14
|
action: string,
|
|
14
|
-
status = 503,
|
|
15
|
+
status: ResponseStatus = 503,
|
|
15
16
|
): Response {
|
|
16
17
|
console.error(`[teams/api] ${action} failed:`, error)
|
|
17
18
|
return response.json({ message }, status)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Append ?type=<scenario> to trigger a specific exception.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { EnhancedRequest } from 'bun-router'
|
|
8
|
+
import type { EnhancedRequest } from '@stacksjs/bun-router'
|
|
9
9
|
import { dirname, join } from 'node:path'
|
|
10
10
|
import process from 'node:process'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
@@ -33,7 +33,7 @@ export default class PruneModelsJob {
|
|
|
33
33
|
try {
|
|
34
34
|
log.info('Running model pruning job')
|
|
35
35
|
|
|
36
|
-
const { prunable } = await import('
|
|
36
|
+
const { prunable } = await import('@stacksjs/orm')
|
|
37
37
|
|
|
38
38
|
const results: Array<{ table: string, pruned: number }> = []
|
|
39
39
|
let total = 0
|
|
@@ -37,7 +37,9 @@ export default class PruneQueryLogsJob {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Prune old query logs
|
|
40
|
-
|
|
40
|
+
// `pruneQueryLogs` is an instance method - the routes reach it through
|
|
41
|
+
// 'Controllers/QueryController@method', which instantiates for them.
|
|
42
|
+
const result = await new QueryController().pruneQueryLogs()
|
|
41
43
|
|
|
42
44
|
log.info(`Successfully pruned ${result.pruned} query logs older than ${result.retentionDays} days`)
|
|
43
45
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { config } from '@stacksjs/config'
|
|
2
|
-
import { mail, template } from '@stacksjs/email'
|
|
2
|
+
import { escapeHtml, mail, safe, template } from '@stacksjs/email'
|
|
3
3
|
|
|
4
4
|
export interface OrderConfirmationItem {
|
|
5
5
|
name: string
|
|
@@ -38,7 +38,10 @@ export async function sendOrderConfirmation(options: OrderConfirmationOptions):
|
|
|
38
38
|
orderId: options.orderId,
|
|
39
39
|
orderUrl: options.orderUrl,
|
|
40
40
|
customerName: options.customerName || 'there',
|
|
41
|
-
|
|
41
|
+
// A template variable is a scalar - an array would be interpolated as
|
|
42
|
+
// "[object Object],[object Object]". The rows are rendered here and
|
|
43
|
+
// marked safe, with every value the customer supplied escaped first.
|
|
44
|
+
itemRows: safe(renderItemRows(options.items)),
|
|
42
45
|
subtotal: options.subtotal,
|
|
43
46
|
shipping: options.shipping,
|
|
44
47
|
total: options.total,
|
|
@@ -57,4 +60,17 @@ export async function sendOrderConfirmation(options: OrderConfirmationOptions):
|
|
|
57
60
|
})
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function formatMoney(amount: number): string {
|
|
64
|
+
return (amount / 100).toFixed(2)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderItemRows(items: OrderConfirmationItem[]): string {
|
|
68
|
+
return items
|
|
69
|
+
.map(item => `<tr>
|
|
70
|
+
<td style="padding: 12px 0; color: #d4d4d4; font-size: 15px;">${escapeHtml(item.name)} × ${Number(item.qty) || 0}</td>
|
|
71
|
+
<td style="padding: 12px 0; color: #ececec; font-size: 15px; text-align: right;">${formatMoney(Number(item.lineTotal) || 0)}</td>
|
|
72
|
+
</tr>`)
|
|
73
|
+
.join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
export default sendOrderConfirmation
|
|
@@ -36,7 +36,9 @@ export default new Middleware({
|
|
|
36
36
|
throw new HttpError(401, 'Unauthenticated.')
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// `_currentAccessToken` is declared `unknown` (the token row is
|
|
40
|
+
// project-defined), so name the one field this middleware reads.
|
|
41
|
+
const tokenAbilities: string[] = (token as { abilities?: string[] }).abilities || []
|
|
40
42
|
|
|
41
43
|
// Wildcard grants all permissions
|
|
42
44
|
if (tokenAbilities.includes('*')) {
|
package/app/Middleware/Can.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthorizationException, authorize } from '@stacksjs/auth'
|
|
1
|
+
import { authenticatedUser, AuthorizationException, authorize } from '@stacksjs/auth'
|
|
2
2
|
import { HttpError } from '@stacksjs/error-handling'
|
|
3
3
|
import { Middleware, resolveRouteModel, setRouteModelFallback } from '@stacksjs/router'
|
|
4
4
|
|
|
@@ -76,7 +76,7 @@ export default new Middleware({
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Get the authenticated user
|
|
79
|
-
const user =
|
|
79
|
+
const user = await authenticatedUser(request)
|
|
80
80
|
|
|
81
81
|
// Prepare arguments for the gate check
|
|
82
82
|
const args: any[] = []
|
|
@@ -109,7 +109,7 @@ export default new Middleware({
|
|
|
109
109
|
|
|
110
110
|
// Perform the authorization check
|
|
111
111
|
try {
|
|
112
|
-
await authorize(ability, user, ...args)
|
|
112
|
+
await authorize(ability, user ?? null, ...args)
|
|
113
113
|
}
|
|
114
114
|
catch (error) {
|
|
115
115
|
if (error instanceof AuthorizationException) {
|
|
@@ -180,7 +180,9 @@ export async function applyCompression(request: Request, response: Response): Pr
|
|
|
180
180
|
newHeaders.set('Vary', 'Accept-Encoding')
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
// Node's Buffer satisfies BodyInit at runtime but not in the DOM lib's
|
|
184
|
+
// types; the underlying bytes are what Response wants either way.
|
|
185
|
+
return new Response(new Uint8Array(compressed), {
|
|
184
186
|
status: response.status,
|
|
185
187
|
statusText: response.statusText,
|
|
186
188
|
headers: newHeaders,
|
package/app/Middleware/Csrf.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
2
2
|
import { Buffer } from 'node:buffer'
|
|
3
3
|
import { HttpError } from '@stacksjs/error-handling'
|
|
4
|
+
import type { EnhancedRequest } from '@stacksjs/bun-router'
|
|
4
5
|
import { Middleware } from '@stacksjs/router'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -218,7 +219,7 @@ const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
|
|
|
218
219
|
* the router, such as the local dashboard config editor, can enforce the same
|
|
219
220
|
* double-submit and bearer-token rules without duplicating security logic.
|
|
220
221
|
*/
|
|
221
|
-
export async function validateCsrfRequest(request: Request): Promise<void> {
|
|
222
|
+
export async function validateCsrfRequest(request: Request | EnhancedRequest): Promise<void> {
|
|
222
223
|
const method = request.method.toUpperCase()
|
|
223
224
|
|
|
224
225
|
// Safe methods don't mutate state — no token check needed.
|
|
@@ -236,15 +237,20 @@ export async function validateCsrfRequest(request: Request): Promise<void> {
|
|
|
236
237
|
// (webhooks, third-party callbacks). The router stamps a hint on
|
|
237
238
|
// the request when it resolves such an action; if that hint is
|
|
238
239
|
// present, skip enforcement.
|
|
239
|
-
|
|
240
|
+
const enhanced = request as EnhancedRequest
|
|
241
|
+
if (enhanced._skipCsrf === true) return
|
|
240
242
|
|
|
241
243
|
// Look up the submitted token. Header is the SPA path; body field
|
|
242
244
|
// is the traditional form-post path. We accept either.
|
|
243
245
|
const headerToken = request.headers.get(CSRF_HEADER_NAME)
|
|
244
246
|
|| request.headers.get('X-CSRF-Token')
|
|
245
247
|
|| request.headers.get('X-Csrf-Token')
|
|
246
|
-
const body =
|
|
247
|
-
|
|
248
|
+
const body = enhanced.jsonBody || enhanced.formBody || {}
|
|
249
|
+
// A parsed body is `unknown`-valued: the token is validated as a string
|
|
250
|
+
// two lines down, so read it as one rather than asserting it is one.
|
|
251
|
+
const rawBodyToken = (body as Record<string, unknown> | undefined)?._token
|
|
252
|
+
?? (body as Record<string, unknown> | undefined)?.csrf_token
|
|
253
|
+
const bodyToken: string | undefined = typeof rawBodyToken === 'string' ? rawBodyToken : undefined
|
|
248
254
|
|
|
249
255
|
const submitted = (typeof headerToken === 'string' && headerToken)
|
|
250
256
|
|| (typeof bodyToken === 'string' && bodyToken)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authenticatedUser } from '@stacksjs/auth'
|
|
1
2
|
import { HttpError } from '@stacksjs/error-handling'
|
|
2
3
|
import { Middleware } from '@stacksjs/router'
|
|
3
4
|
|
|
@@ -15,7 +16,7 @@ export default new Middleware({
|
|
|
15
16
|
priority: 4,
|
|
16
17
|
|
|
17
18
|
async handle(request) {
|
|
18
|
-
const user =
|
|
19
|
+
const user = await authenticatedUser(request)
|
|
19
20
|
|
|
20
21
|
if (!user) {
|
|
21
22
|
throw new HttpError(401, 'Unauthenticated.')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authenticatedUser } from '@stacksjs/auth'
|
|
1
2
|
import { HttpError } from '@stacksjs/error-handling'
|
|
2
3
|
import { Middleware } from '@stacksjs/router'
|
|
3
4
|
|
|
@@ -25,7 +26,7 @@ export default new Middleware({
|
|
|
25
26
|
|
|
26
27
|
if (requiredPermissions.length === 0) return
|
|
27
28
|
|
|
28
|
-
const user =
|
|
29
|
+
const user = await authenticatedUser(request)
|
|
29
30
|
|
|
30
31
|
if (!user) {
|
|
31
32
|
throw new HttpError(401, 'Unauthenticated.')
|
package/app/Middleware/Role.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authenticatedUser } from '@stacksjs/auth'
|
|
1
2
|
import { HttpError } from '@stacksjs/error-handling'
|
|
2
3
|
import { Middleware } from '@stacksjs/router'
|
|
3
4
|
|
|
@@ -24,7 +25,7 @@ export default new Middleware({
|
|
|
24
25
|
|
|
25
26
|
if (requiredRoles.length === 0) return
|
|
26
27
|
|
|
27
|
-
const user =
|
|
28
|
+
const user = await authenticatedUser(request)
|
|
28
29
|
|
|
29
30
|
if (!user) {
|
|
30
31
|
throw new HttpError(401, 'Unauthenticated.')
|
package/app/Middleware/Team.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authenticatedUser } from '@stacksjs/auth'
|
|
1
2
|
import { HttpError } from '@stacksjs/error-handling'
|
|
2
3
|
import { Middleware } from '@stacksjs/router'
|
|
3
4
|
|
|
@@ -16,7 +17,7 @@ export default new Middleware({
|
|
|
16
17
|
priority: 3,
|
|
17
18
|
|
|
18
19
|
async handle(request) {
|
|
19
|
-
const user =
|
|
20
|
+
const user = await authenticatedUser(request)
|
|
20
21
|
|
|
21
22
|
if (!user) {
|
|
22
23
|
throw new HttpError(401, 'Unauthenticated.')
|
|
@@ -47,7 +47,7 @@ export default new Middleware({
|
|
|
47
47
|
let limiter = limiterCache.get(params)
|
|
48
48
|
if (!limiter) {
|
|
49
49
|
try {
|
|
50
|
-
const config = parseThrottleString(params)
|
|
50
|
+
const config = parseThrottleString(params as Parameters<typeof parseThrottleString>[0])
|
|
51
51
|
limiter = createRateLimitMiddleware(config, `throttle:${params}`)
|
|
52
52
|
limiterCache.set(params, limiter)
|
|
53
53
|
}
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.7",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@iconify-json/f7": "^1.2.2",
|
|
48
48
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
49
|
-
"@stacksjs/mobile": "^0.72.
|
|
49
|
+
"@stacksjs/mobile": "^0.72.7",
|
|
50
50
|
"@stacksjs/sanitizer": "^0.2.113"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
@@ -342,7 +342,7 @@ export function groupByDay(
|
|
|
342
342
|
const d = new Date(raw)
|
|
343
343
|
if (Number.isNaN(d.getTime())) continue
|
|
344
344
|
const k = d.toISOString().slice(0, 10)
|
|
345
|
-
if (k in buckets) buckets[k]
|
|
345
|
+
if (k in buckets) buckets[k] = (buckets[k] ?? 0) + 1
|
|
346
346
|
}
|
|
347
347
|
return Object.entries(buckets).map(([date, count]) => ({ date, count }))
|
|
348
348
|
}
|
|
@@ -372,7 +372,7 @@ export function sumByDay(
|
|
|
372
372
|
if (Number.isNaN(d.getTime())) continue
|
|
373
373
|
const k = d.toISOString().slice(0, 10)
|
|
374
374
|
if (!(k in buckets)) continue
|
|
375
|
-
buckets[k]
|
|
375
|
+
buckets[k] = (buckets[k] ?? 0) + (Number(safeGet(r, valueKey, 0)) || 0)
|
|
376
376
|
}
|
|
377
377
|
return Object.entries(buckets).map(([date, total]) => ({ date, total }))
|
|
378
378
|
}
|