@stacksjs/defaults 0.72.98 → 0.72.100
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 +70 -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
|
@@ -47,7 +47,7 @@ The `Schedule` class is the core scheduling API. The lowercase `schedule` export
|
|
|
47
47
|
import { schedule } from '@stacksjs/scheduler'
|
|
48
48
|
|
|
49
49
|
// Static factory methods — each returns UntimedSchedule
|
|
50
|
-
schedule.job(name:
|
|
50
|
+
schedule.job(name: JobName): UntimedSchedule // Runs a job by name via runJob()
|
|
51
51
|
schedule.action(name: string): UntimedSchedule // Runs an action by name via runAction()
|
|
52
52
|
schedule.command(cmd: string): UntimedSchedule // Runs a shell command via runCommand()
|
|
53
53
|
|
|
@@ -21,7 +21,7 @@ export default new Action({
|
|
|
21
21
|
return {
|
|
22
22
|
records,
|
|
23
23
|
summary: summarizeCommerceCustomers(records),
|
|
24
|
-
currency: normalizeCommerceCustomerCurrency(
|
|
24
|
+
currency: normalizeCommerceCustomerCurrency(config.commerce?.currency),
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
@@ -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
|
+
})
|