@stacksjs/defaults 0.72.98 → 0.72.99
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/ai/skills/stacks-auth/SKILL.md +35 -5
- package/ai/skills/stacks-config/SKILL.md +11 -1
- package/ai/skills/stacks-desktop/SKILL.md +46 -0
- package/ai/skills/stacks-env/SKILL.md +26 -0
- package/ai/skills/stacks-events/SKILL.md +76 -60
- package/ai/skills/stacks-jobs/SKILL.md +3 -0
- package/ai/skills/stacks-listeners/SKILL.md +39 -9
- package/ai/skills/stacks-middleware/SKILL.md +50 -12
- package/ai/skills/stacks-queue/SKILL.md +5 -0
- package/ai/skills/stacks-router/SKILL.md +17 -7
- package/ai/skills/stacks-scheduler/SKILL.md +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +1 -1
- package/app/Actions/Dashboard/Content/content-input.ts +2 -2
- package/app/Actions/Dashboard/Content/post-input.ts +4 -4
- package/app/Actions/Dashboard/Email/inbox-request.ts +1 -1
- package/app/Actions/Dashboard/Email/mail-preference.ts +1 -1
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +1 -1
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +1 -1
- package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +2 -2
- package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +2 -2
- package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +5 -5
- package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +2 -2
- package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
- package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +2 -2
- package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +2 -2
- package/app/Events.ts +9 -4
- package/app/Gates.ts +83 -85
- package/app/Listener.ts +34 -81
- package/app/Middleware.ts +22 -7
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/project/storage/framework/server/tsconfig.docker.json +1 -2
- package/resources/components/Buttons/Counter.stx +30 -21
- package/resources/components/Dashboard/Analytics/WebAnalyticsDashboard.stx +1 -3
- package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +1 -1
- package/resources/components/Dashboard/Auth/LoginDashboard.stx +2 -1
- package/resources/components/Dashboard/Auth/RegisterDashboard.stx +2 -1
- package/resources/components/Dashboard/Auth/ResetPasswordDashboard.stx +1 -1
- package/resources/components/Dashboard/Ci/CiDashboard.stx +9 -9
- package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +7 -7
- package/resources/components/Dashboard/Commerce/CommerceOverviewCharts.stx +2 -1
- package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +1 -1
- package/resources/components/Dashboard/Content/BlogDashboard.stx +37 -15
- package/resources/components/Dashboard/Content/ContentDashboard.stx +3 -26
- package/resources/components/Dashboard/Email/EmailActivityDashboard.stx +2 -15
- package/resources/components/Dashboard/Environment/EnvironmentDashboard.stx +9 -1
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +15 -13
- package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +10 -8
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +10 -10
- package/resources/components/Dashboard/Management/PermissionsDashboard.stx +3 -3
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +4 -1
- package/resources/components/Dashboard/Navbar.stx +1 -1
- package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +11 -6
- package/resources/components/Dashboard/Settings/SettingsDashboard.stx +2 -2
- package/resources/components/Dashboard/Transaction/index.stx +6 -6
- package/resources/components/Dashboard/UI/Avatar.stx +2 -2
- package/resources/components/Dashboard/UI/DropdownItem.stx +4 -4
- package/resources/components/Dashboard/UI/WindowControls.stx +17 -2
- package/resources/components/Docs/Demo/ComboboxDemo.stx +8 -3
- package/resources/components/Docs/Demo/DropdownDemo.stx +3 -3
- package/resources/components/Docs/Demo/StepperDemo.stx +1 -1
- package/resources/components/Forum/ForumLayout.stx +1 -1
- package/resources/components/Forum/ForumReplyForm.stx +1 -1
- package/resources/components/Marketing/ComingSoon.stx +5 -5
- package/resources/components/Storefront/CartDrawer.stx +44 -22
- package/resources/components/Storefront/ProductGallery.stx +3 -2
- package/resources/components/Storefront/QuantitySelector.stx +12 -7
- package/resources/functions/storefront/cart-cookie.ts +58 -0
- package/resources/layouts/storefront.stx +3 -2
- package/resources/views/cart.stx +19 -3
- package/resources/views/checkout/contact.stx +14 -3
- package/resources/views/checkout/payment.stx +14 -3
- package/resources/views/checkout/shipping.stx +14 -3
- package/resources/views/cms/blocks/form.stx +16 -11
- package/resources/views/cms/page.stx +28 -0
- package/resources/views/coming-soon.stx +0 -6
- package/resources/views/orders/[id].stx +19 -3
- package/views/dashboard/composables/index.ts +1 -1
- package/views/dashboard/composables/useChart.ts +26 -14
- package/views/dashboard/layouts/default.stx +42 -25
|
@@ -50,7 +50,7 @@ export default new Action({
|
|
|
50
50
|
.sort((left, right) => left.label.localeCompare(right.label)),
|
|
51
51
|
statuses: [...new Set([...defaultStatuses, ...records.map(record => record.status)])],
|
|
52
52
|
orderTypes: [...new Set([...defaultTypes, ...records.map(record => record.orderType)])],
|
|
53
|
-
defaultCurrency: commerceCurrency(
|
|
53
|
+
defaultCurrency: commerceCurrency(config.commerce?.currency, 'Commerce configuration'),
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
catch (error) {
|
|
@@ -53,7 +53,7 @@ export default new Action({
|
|
|
53
53
|
.filter(isCommercePosCustomerActive)
|
|
54
54
|
.map(normalizeCommercePosCustomer),
|
|
55
55
|
taxRate: selectCommercePosTaxRate(taxRates),
|
|
56
|
-
defaultCurrency: normalizeCommerceCurrency(
|
|
56
|
+
defaultCurrency: normalizeCommerceCurrency(config.commerce?.currency),
|
|
57
57
|
paymentMethods: [{ id: 'cash', label: 'Cash' }],
|
|
58
58
|
summary: {
|
|
59
59
|
products: records.length,
|
|
@@ -285,7 +285,7 @@ export default new Action({
|
|
|
285
285
|
return dashboardOperationalError(error, 'Sale totals could not be calculated.', 'CommercePosCheckoutAction')
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
const currency = normalizeCommerceCurrency(
|
|
288
|
+
const currency = normalizeCommerceCurrency(config.commerce?.currency)
|
|
289
289
|
const transactionId = randomUUIDv7()
|
|
290
290
|
const referenceNumber = `POS-${transactionId.slice(0, 12).toUpperCase()}`
|
|
291
291
|
let result
|
|
@@ -58,7 +58,7 @@ export default new Action({
|
|
|
58
58
|
reviews: summarizeProductReviews(reviews),
|
|
59
59
|
categories: categoryOptions,
|
|
60
60
|
manufacturers: manufacturerOptions,
|
|
61
|
-
defaultCurrency: normalizeCommerceCurrency(
|
|
61
|
+
defaultCurrency: normalizeCommerceCurrency(config.commerce?.currency),
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
@@ -52,7 +52,7 @@ export default new Action({
|
|
|
52
52
|
summary: summarizeCommerceProducts(records),
|
|
53
53
|
categories: categoryOptions,
|
|
54
54
|
manufacturers: manufacturerOptions,
|
|
55
|
-
defaultCurrency: normalizeCommerceCurrency(
|
|
55
|
+
defaultCurrency: normalizeCommerceCurrency(config.commerce?.currency),
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
@@ -54,12 +54,12 @@ export function slugify(value: string): string {
|
|
|
54
54
|
* query builder's types want a table union rather than a `string`.
|
|
55
55
|
*/
|
|
56
56
|
export async function findRow(table: string, id: number, database: typeof db = db): Promise<unknown> {
|
|
57
|
-
return await
|
|
57
|
+
return await database.selectFrom(table).selectAll().where('id', '=', id).executeTakeFirst()
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** Whether a row with this id exists — the 404 check every write shares. */
|
|
61
61
|
export async function rowExists(table: string, id: number, database: typeof db = db): Promise<boolean> {
|
|
62
|
-
const row = await
|
|
62
|
+
const row = await database.selectFrom(table).select(['id']).where('id', '=', id).executeTakeFirst()
|
|
63
63
|
|
|
64
64
|
return Boolean(row)
|
|
65
65
|
}
|
|
@@ -116,7 +116,7 @@ async function replacePostPivot(
|
|
|
116
116
|
relatedIds: number[],
|
|
117
117
|
now: string,
|
|
118
118
|
): Promise<void> {
|
|
119
|
-
await
|
|
119
|
+
await database
|
|
120
120
|
.deleteFrom(table)
|
|
121
121
|
.where(ownerKey, '=', postId)
|
|
122
122
|
.where(typeKey, '=', 'posts')
|
|
@@ -125,7 +125,7 @@ async function replacePostPivot(
|
|
|
125
125
|
if (relatedIds.length === 0)
|
|
126
126
|
return
|
|
127
127
|
|
|
128
|
-
await
|
|
128
|
+
await database
|
|
129
129
|
.insertInto(table)
|
|
130
130
|
.values(relatedIds.map(relatedId => ({
|
|
131
131
|
[foreignKey]: relatedId,
|
|
@@ -146,12 +146,12 @@ export async function syncPostRelations(database: typeof db, postId: number, pay
|
|
|
146
146
|
|
|
147
147
|
/** Removes both post pivot sets on the caller's database or transaction. */
|
|
148
148
|
export async function detachPostRelations(database: typeof db, postId: number): Promise<void> {
|
|
149
|
-
await
|
|
149
|
+
await database
|
|
150
150
|
.deleteFrom('categorizable_models')
|
|
151
151
|
.where('categorizable_id', '=', postId)
|
|
152
152
|
.where('categorizable_type', '=', 'posts')
|
|
153
153
|
.execute()
|
|
154
|
-
await
|
|
154
|
+
await database
|
|
155
155
|
.deleteFrom('taggable_models')
|
|
156
156
|
.where('taggable_id', '=', postId)
|
|
157
157
|
.where('taggable_type', '=', 'posts')
|
|
@@ -5,7 +5,7 @@ import { response } from '@stacksjs/router'
|
|
|
5
5
|
import { defaultMailbox } from './mail-preference'
|
|
6
6
|
|
|
7
7
|
function configuredMailDomain(): string {
|
|
8
|
-
const email =
|
|
8
|
+
const email = config?.email || {}
|
|
9
9
|
const fromAddress = String(email.from?.address || '').trim()
|
|
10
10
|
const fromDomain = fromAddress.includes('@') ? fromAddress.split('@').at(-1) : ''
|
|
11
11
|
return String(email.domain || fromDomain || 'stacksjs.com').trim().toLowerCase()
|
|
@@ -26,7 +26,7 @@ export interface MailPreferenceInput {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function defaultMailbox(): string {
|
|
29
|
-
const email =
|
|
29
|
+
const email = config?.email || {}
|
|
30
30
|
const domain = String(email.domain || 'stacksjs.com')
|
|
31
31
|
const configured = Array.isArray(email.mailboxes) ? email.mailboxes[0] : undefined
|
|
32
32
|
const mailbox = typeof configured === 'string' ? configured : configured?.email
|
|
@@ -15,7 +15,7 @@ export default new Action({
|
|
|
15
15
|
async handle(request: RequestInstance) {
|
|
16
16
|
const data = campaignWriteData(
|
|
17
17
|
await request.all(),
|
|
18
|
-
String(
|
|
18
|
+
String(config.commerce?.currency || 'USD').toUpperCase(),
|
|
19
19
|
)
|
|
20
20
|
const validationError = validateCampaignWriteData(data)
|
|
21
21
|
if (validationError)
|
|
@@ -18,7 +18,7 @@ export default new Action({
|
|
|
18
18
|
return response.json({ message: 'A valid campaign id is required.' }, 400)
|
|
19
19
|
const data = campaignWriteData(
|
|
20
20
|
await request.all(),
|
|
21
|
-
String(
|
|
21
|
+
String(config.commerce?.currency || 'USD').toUpperCase(),
|
|
22
22
|
)
|
|
23
23
|
const validationError = validateCampaignWriteData(data)
|
|
24
24
|
if (validationError)
|
|
@@ -25,7 +25,7 @@ export default new Action({
|
|
|
25
25
|
|
|
26
26
|
let invitation
|
|
27
27
|
try {
|
|
28
|
-
invitation = await
|
|
28
|
+
invitation = await db
|
|
29
29
|
.selectFrom('team_invitations')
|
|
30
30
|
.where('id', '=', invitationId)
|
|
31
31
|
.where('team_id', '=', teamId)
|
|
@@ -41,7 +41,7 @@ export default new Action({
|
|
|
41
41
|
return response.json({ message: 'Only pending invitations can be revoked.' }, 409)
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
-
const result = await
|
|
44
|
+
const result = await db
|
|
45
45
|
.updateTable('team_invitations')
|
|
46
46
|
.set({
|
|
47
47
|
status: 'revoked',
|
|
@@ -28,7 +28,7 @@ export default new Action({
|
|
|
28
28
|
|
|
29
29
|
let invitation
|
|
30
30
|
try {
|
|
31
|
-
invitation = await
|
|
31
|
+
invitation = await db
|
|
32
32
|
.selectFrom('team_invitations')
|
|
33
33
|
.innerJoin('teams', 'teams.id', '=', 'team_invitations.team_id')
|
|
34
34
|
.where('team_invitations.id', '=', invitationId)
|
|
@@ -55,7 +55,7 @@ export default new Action({
|
|
|
55
55
|
const tokenHash = hashInvitationToken(token)
|
|
56
56
|
const expiresAt = invitationExpiresAt()
|
|
57
57
|
try {
|
|
58
|
-
const result = await
|
|
58
|
+
const result = await db
|
|
59
59
|
.updateTable('team_invitations')
|
|
60
60
|
.set({
|
|
61
61
|
token_hash: tokenHash,
|
|
@@ -69,7 +69,7 @@ export default new Action({
|
|
|
69
69
|
.where('email', '=', email)
|
|
70
70
|
.select(['id'])
|
|
71
71
|
.executeTakeFirst(),
|
|
72
|
-
|
|
72
|
+
trx
|
|
73
73
|
.selectFrom('team_invitations')
|
|
74
74
|
.where('team_id', '=', teamId)
|
|
75
75
|
.where('email', '=', email)
|
|
@@ -78,7 +78,7 @@ export default new Action({
|
|
|
78
78
|
.executeTakeFirst(),
|
|
79
79
|
])
|
|
80
80
|
if (existingUser) {
|
|
81
|
-
const membership = await
|
|
81
|
+
const membership = await trx
|
|
82
82
|
.selectFrom('team_members')
|
|
83
83
|
.where('team_id', '=', teamId)
|
|
84
84
|
.where('user_id', '=', Number(existingUser.id))
|
|
@@ -92,7 +92,7 @@ export default new Action({
|
|
|
92
92
|
return response.json({ message: 'A pending invitation already exists. Resend or revoke it from the invitation list.' }, 409)
|
|
93
93
|
}
|
|
94
94
|
if (pending) {
|
|
95
|
-
const expired = await
|
|
95
|
+
const expired = await trx
|
|
96
96
|
.updateTable('team_invitations')
|
|
97
97
|
.set({ status: 'expired', pending_key: null, updated_at: sqlTimestamp() })
|
|
98
98
|
.where('id', '=', Number(pending.id))
|
|
@@ -103,7 +103,7 @@ export default new Action({
|
|
|
103
103
|
throw new TeamStateConflictError('The pending invitation changed before it could be replaced.')
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
await
|
|
106
|
+
await trx
|
|
107
107
|
.insertInto('team_invitations')
|
|
108
108
|
.values({
|
|
109
109
|
team_id: teamId,
|
|
@@ -119,7 +119,7 @@ export default new Action({
|
|
|
119
119
|
})
|
|
120
120
|
.execute()
|
|
121
121
|
|
|
122
|
-
const invitation = await
|
|
122
|
+
const invitation = await trx
|
|
123
123
|
.selectFrom('team_invitations')
|
|
124
124
|
.where('token_hash', '=', tokenHash)
|
|
125
125
|
.select(['id'])
|
|
@@ -22,7 +22,7 @@ export default new Action({
|
|
|
22
22
|
try {
|
|
23
23
|
result = await db.transaction(async (rawTrx) => {
|
|
24
24
|
const trx = rawTrx as unknown as typeof db
|
|
25
|
-
const member = await
|
|
25
|
+
const member = await trx
|
|
26
26
|
.selectFrom('team_members')
|
|
27
27
|
.where('id', '=', memberId)
|
|
28
28
|
.where('team_id', '=', teamId)
|
|
@@ -33,7 +33,7 @@ export default new Action({
|
|
|
33
33
|
if (member.role === 'owner')
|
|
34
34
|
return response.json({ message: 'Transfer team ownership before removing the owner.' }, 409)
|
|
35
35
|
|
|
36
|
-
const deleted = await
|
|
36
|
+
const deleted = await trx
|
|
37
37
|
.deleteFrom('team_members')
|
|
38
38
|
.where('id', '=', memberId)
|
|
39
39
|
.where('team_id', '=', teamId)
|
|
@@ -27,7 +27,7 @@ export default new Action({
|
|
|
27
27
|
try {
|
|
28
28
|
return await db.transaction(async (rawTrx) => {
|
|
29
29
|
const trx = rawTrx as unknown as typeof db
|
|
30
|
-
const member = await
|
|
30
|
+
const member = await trx
|
|
31
31
|
.selectFrom('team_members')
|
|
32
32
|
.where('id', '=', memberId)
|
|
33
33
|
.where('team_id', '=', teamId)
|
|
@@ -45,7 +45,7 @@ export default new Action({
|
|
|
45
45
|
if (!['active', 'suspended'].includes(status))
|
|
46
46
|
return response.json({ message: 'Choose a valid member status.' }, 422)
|
|
47
47
|
|
|
48
|
-
const updated = await
|
|
48
|
+
const updated = await trx
|
|
49
49
|
.updateTable('team_members')
|
|
50
50
|
.set({ role, status, updated_at: sqlTimestamp() })
|
|
51
51
|
.where('id', '=', memberId)
|
|
@@ -32,7 +32,7 @@ export default new Action({
|
|
|
32
32
|
|
|
33
33
|
try {
|
|
34
34
|
const [members, invitations] = await Promise.all([
|
|
35
|
-
|
|
35
|
+
db
|
|
36
36
|
.selectFrom('team_members')
|
|
37
37
|
.innerJoin('users', 'users.id', '=', 'team_members.user_id')
|
|
38
38
|
.where('team_members.team_id', '=', teamId)
|
|
@@ -47,7 +47,7 @@ export default new Action({
|
|
|
47
47
|
])
|
|
48
48
|
.orderBy('users.name', 'asc')
|
|
49
49
|
.execute(),
|
|
50
|
-
|
|
50
|
+
db
|
|
51
51
|
.selectFrom('team_invitations')
|
|
52
52
|
.where('team_id', '=', teamId)
|
|
53
53
|
.where('status', '=', 'pending')
|
|
@@ -45,7 +45,7 @@ export async function deliverTeamInvitation(input: DeliveryInput): Promise<void>
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
|
-
const result = await
|
|
48
|
+
const result = await db
|
|
49
49
|
.updateTable('team_invitations')
|
|
50
50
|
.set({
|
|
51
51
|
delivery_status: 'sent',
|
|
@@ -66,7 +66,7 @@ export async function deliverTeamInvitation(input: DeliveryInput): Promise<void>
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export async function markTeamInvitationDeliveryFailed(id: number, tokenHash: string, error: string): Promise<void> {
|
|
69
|
-
await
|
|
69
|
+
await db
|
|
70
70
|
.updateTable('team_invitations')
|
|
71
71
|
.set({
|
|
72
72
|
delivery_status: 'failed',
|
package/app/Events.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { defineEvents } from '@stacksjs/events'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* **Events Configuration**
|
|
@@ -6,9 +6,14 @@ import type { Events } from '@stacksjs/types'
|
|
|
6
6
|
* This configuration defines all of your events. Because Stacks is fully-typed, you may
|
|
7
7
|
* hover any of the options below and the definitions will be provided. In case you
|
|
8
8
|
* have any questions, feel free to reach out via Discord or GitHub Discussions.
|
|
9
|
+
*
|
|
10
|
+
* Both halves of every entry are checked. The key must be an event that exists -
|
|
11
|
+
* declare your own on `AppEvents` in `@stacksjs/events` - and each listener must
|
|
12
|
+
* name a file under `app/Listeners/` or `app/Actions/`, or one of the framework
|
|
13
|
+
* defaults behind them.
|
|
9
14
|
*/
|
|
10
|
-
export default {
|
|
11
|
-
// eventName: ['Listener1', 'Listener2'] -> listeners
|
|
15
|
+
export default defineEvents({
|
|
16
|
+
// eventName: ['Listener1', 'Listener2'] -> listeners resolve from ./app/Listeners/*, then ./app/Actions/*
|
|
12
17
|
'user:registered': ['SendWelcomeEmail'],
|
|
13
18
|
'user:created': ['NotifyUser'],
|
|
14
|
-
}
|
|
19
|
+
})
|
package/app/Gates.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UserModel } from '@stacksjs/orm'
|
|
2
|
+
import { defineGates } from '@stacksjs/auth'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Authorization Gates Configuration
|
|
@@ -7,101 +8,98 @@ import type { UserModel } from '@stacksjs/orm'
|
|
|
7
8
|
* Gates provide a simple way to authorize actions, while policies
|
|
8
9
|
* organize authorization logic around particular models.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Gate definitions
|
|
11
|
+
* Registered at boot by `initializeAuthorization()`, which every entry point
|
|
12
|
+
* comes through - HTTP, `buddy seed`, a scheduled job, a console command.
|
|
15
13
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // In your code:
|
|
20
|
-
* import { Gate } from '@stacksjs/auth'
|
|
21
|
-
*
|
|
22
|
-
* if (await Gate.allows('edit-settings', user)) {
|
|
23
|
-
* // User can edit settings
|
|
24
|
-
* }
|
|
14
|
+
* @see https://stacksjs.org/docs/security/authorization
|
|
25
15
|
*/
|
|
26
|
-
export
|
|
16
|
+
export default defineGates({
|
|
27
17
|
/**
|
|
28
|
-
*
|
|
18
|
+
* Gate definitions
|
|
19
|
+
*
|
|
20
|
+
* Simple ability checks that don't require a model. The names below are what
|
|
21
|
+
* `Gate.allows('...')` offers as completions.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* import { Gate } from '@stacksjs/auth'
|
|
25
|
+
*
|
|
26
|
+
* if (await Gate.allows('edit-settings', user)) {
|
|
27
|
+
* // User can edit settings
|
|
28
|
+
* }
|
|
29
29
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
gates: {
|
|
31
|
+
/** Check if user can access admin area */
|
|
32
|
+
'access-admin': (user: UserModel | null) => {
|
|
33
|
+
return user?.email?.endsWith('@stacksjs.org') ?? false
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/** Check if user can edit application settings */
|
|
37
|
+
'edit-settings': (user: UserModel | null) => {
|
|
38
|
+
// Add your logic here
|
|
39
|
+
return user !== null
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/** Check if user can view dashboard */
|
|
43
|
+
'view-dashboard': (user: UserModel | null) => {
|
|
44
|
+
return user !== null
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Add more gates here...
|
|
48
|
+
// 'ability-name': (user, ...args) => boolean,
|
|
32
49
|
},
|
|
33
50
|
|
|
34
51
|
/**
|
|
35
|
-
*
|
|
52
|
+
* Policy mappings
|
|
53
|
+
*
|
|
54
|
+
* Map model names to their policy classes. Policy files live in
|
|
55
|
+
* `app/Policies/`, or in the framework defaults behind it. Both halves are
|
|
56
|
+
* checked: the key names a model that exists, the value a policy that does.
|
|
57
|
+
*
|
|
58
|
+
* A policy whose file is named `<Model>Policy.ts` is picked up by convention
|
|
59
|
+
* and does not need an entry here. An entry WINS over the convention, which
|
|
60
|
+
* is the point of writing one.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Simple mapping (uses PostPolicy for the Post model)
|
|
64
|
+
* Post: 'PostPolicy',
|
|
65
|
+
*
|
|
66
|
+
* // Or with config:
|
|
67
|
+
* Post: {
|
|
68
|
+
* policy: 'PostPolicy',
|
|
69
|
+
* model: 'Post',
|
|
70
|
+
* },
|
|
36
71
|
*/
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
72
|
+
policies: {
|
|
73
|
+
// Post: 'PostPolicy',
|
|
74
|
+
// User: 'UserPolicy',
|
|
75
|
+
// Comment: 'CommentPolicy',
|
|
40
76
|
},
|
|
41
77
|
|
|
42
78
|
/**
|
|
43
|
-
*
|
|
79
|
+
* Before callbacks
|
|
80
|
+
*
|
|
81
|
+
* Run before any gate/policy check. Return true to allow,
|
|
82
|
+
* false to deny, or null to continue to the actual check.
|
|
44
83
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
84
|
+
before: [
|
|
85
|
+
// Example: Super admin bypass
|
|
86
|
+
// (user) => {
|
|
87
|
+
// if (user?.role === 'super-admin')
|
|
88
|
+
// return true // Allow everything for super admins
|
|
89
|
+
// return null // Continue to normal checks
|
|
90
|
+
// },
|
|
91
|
+
],
|
|
52
92
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* model: Post,
|
|
67
|
-
* },
|
|
68
|
-
*/
|
|
69
|
-
export const policies: Record<string, string | { policy: string, model?: string }> = {
|
|
70
|
-
// 'Post': 'PostPolicy',
|
|
71
|
-
// 'User': 'UserPolicy',
|
|
72
|
-
// 'Comment': 'CommentPolicy',
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Before callbacks
|
|
77
|
-
*
|
|
78
|
-
* Run before any gate/policy check. Return true to allow,
|
|
79
|
-
* false to deny, or null to continue to the actual check.
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* // Super admins bypass all checks
|
|
83
|
-
* (user) => user?.role === 'super-admin' ? true : null
|
|
84
|
-
*/
|
|
85
|
-
export const before: Array<(user: UserModel | null, ability: string, args: any[]) => boolean | null | Promise<boolean | null>> = [
|
|
86
|
-
// Example: Super admin bypass
|
|
87
|
-
// (user, ability) => {
|
|
88
|
-
// if (user?.role === 'super-admin') {
|
|
89
|
-
// return true // Allow everything for super admins
|
|
90
|
-
// }
|
|
91
|
-
// return null // Continue to normal checks
|
|
92
|
-
// },
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* After callbacks
|
|
97
|
-
*
|
|
98
|
-
* Run after gate/policy checks. Can override the result.
|
|
99
|
-
*/
|
|
100
|
-
export const after: Array<(user: UserModel | null, ability: string, result: boolean, args: any[]) => boolean | void | Promise<boolean | void>> = [
|
|
101
|
-
// Example: Log all authorization checks
|
|
102
|
-
// (user, ability, result) => {
|
|
103
|
-
// console.log(`User ${user?.id} ${result ? 'allowed' : 'denied'} for ${ability}`)
|
|
104
|
-
// },
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
export default { gates, policies, before, after }
|
|
93
|
+
/**
|
|
94
|
+
* After callbacks
|
|
95
|
+
*
|
|
96
|
+
* Run after gate/policy checks. Returning a boolean overrides the result;
|
|
97
|
+
* returning nothing keeps it.
|
|
98
|
+
*/
|
|
99
|
+
after: [
|
|
100
|
+
// Example: Log all authorization checks
|
|
101
|
+
// (user, ability, result) => {
|
|
102
|
+
// console.log(`User ${user?.id} ${result ? 'allowed' : 'denied'} for ${ability}`)
|
|
103
|
+
// },
|
|
104
|
+
],
|
|
105
|
+
})
|
package/app/Listener.ts
CHANGED
|
@@ -1,84 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { handleError } from '@stacksjs/error-handling'
|
|
3
|
-
import { emitter } from '@stacksjs/events'
|
|
1
|
+
import { registerAppListeners } from '@stacksjs/events'
|
|
4
2
|
import { path as p } from '@stacksjs/path'
|
|
5
|
-
import events from './Events'
|
|
6
3
|
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
if (pending) return pending
|
|
41
|
-
|
|
42
|
-
// Start the import and cache the promise to deduplicate concurrent calls
|
|
43
|
-
pending = (async () => {
|
|
44
|
-
try {
|
|
45
|
-
const imported = await import(p.appPath(`Actions/${listener}.ts`))
|
|
46
|
-
const actionModule = imported.default
|
|
47
|
-
if (actionModule && typeof actionModule.handle === 'function') {
|
|
48
|
-
actionCache.set(listener, actionModule)
|
|
49
|
-
return actionModule
|
|
50
|
-
}
|
|
51
|
-
handleError(`Action "${listener}" does not export a handle method`)
|
|
52
|
-
return null
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
handleError(`Failed to import action "${listener}"`, error)
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
|
-
finally {
|
|
59
|
-
pendingImports.delete(listener)
|
|
60
|
-
}
|
|
61
|
-
})()
|
|
62
|
-
|
|
63
|
-
pendingImports.set(listener, pending)
|
|
64
|
-
return pending
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function processListeners(type: string, listeners: string[], event: any) {
|
|
68
|
-
for (const listener of listeners) {
|
|
69
|
-
try {
|
|
70
|
-
if (typeof listener === 'function') {
|
|
71
|
-
await (listener as any)(event)
|
|
72
|
-
continue
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const action = await resolveAction(listener)
|
|
76
|
-
if (action) {
|
|
77
|
-
await action.handle(event)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
handleError(`Failed to execute listener "${listener}" for event "${type}"`, error)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
4
|
+
/**
|
|
5
|
+
* **Event Listener Bootstrap**
|
|
6
|
+
*
|
|
7
|
+
* Registers every listener the application declares: the `app/Events.ts` map of
|
|
8
|
+
* event name to listener names, and any module under `app/Listeners/` that
|
|
9
|
+
* declares its own `listensTo`.
|
|
10
|
+
*
|
|
11
|
+
* This file used to carry its own implementation of the first half - a wildcard
|
|
12
|
+
* handler that read the map on every dispatch and imported the named action out
|
|
13
|
+
* of `app/Actions/`. It was a second implementation of what
|
|
14
|
+
* `registerAppListeners` already does, and having two was worse than having the
|
|
15
|
+
* wrong one:
|
|
16
|
+
*
|
|
17
|
+
* - In dev they both ran. `injectGlobalAutoImports()` calls
|
|
18
|
+
* `registerAppListeners()` and the dev API entrypoint then called
|
|
19
|
+
* `handleEvents()`, so every listener named in the map fired TWICE per
|
|
20
|
+
* event - two welcome emails, two rows, and no error anywhere to say so.
|
|
21
|
+
* - The copy resolved names against `app/Actions/` alone, so a listener under
|
|
22
|
+
* `app/Listeners/` and any of the framework's default actions were
|
|
23
|
+
* unreachable from the map.
|
|
24
|
+
* - It matched events by exact name only, dropping the glob subscriptions
|
|
25
|
+
* (`user:*`) the emitter supports.
|
|
26
|
+
*
|
|
27
|
+
* Delegating keeps this file as the app's hook into boot - override it to
|
|
28
|
+
* register listeners of your own - with one implementation behind it, whose
|
|
29
|
+
* registry also makes the call idempotent across a dev-server reload.
|
|
30
|
+
*/
|
|
31
|
+
export async function handleEvents(): Promise<number> {
|
|
32
|
+
// Anchored to the project root rather than left to `process.cwd()`. A buddy
|
|
33
|
+
// command, a test runner or a worker started from a subdirectory would
|
|
34
|
+
// otherwise look for `app/Events.ts` beside wherever it happened to be
|
|
35
|
+
// launched, find nothing, and register no listeners without saying so.
|
|
36
|
+
return registerAppListeners({ base: p.projectPath() })
|
|
84
37
|
}
|