@stacksjs/defaults 0.74.62 → 0.74.64
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { authCookieForBrowserSession, refreshToken } from '@stacksjs/auth'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { schema } from '@stacksjs/validation'
|
|
5
5
|
|
|
@@ -34,7 +34,7 @@ export default new Action({
|
|
|
34
34
|
refresh_token: result.refreshToken,
|
|
35
35
|
token_type: 'Bearer',
|
|
36
36
|
expires_in: result.expiresIn,
|
|
37
|
-
}, { headers: { 'Set-Cookie':
|
|
37
|
+
}, { headers: { 'Set-Cookie': authCookieForBrowserSession(result.plainTextToken, result.expiresIn) } })
|
|
38
38
|
}
|
|
39
39
|
catch (error: any) {
|
|
40
40
|
return response.unauthorized(error.message || 'Invalid or expired refresh token')
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { AuthenticationCredential } from '@stacksjs/auth'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import {
|
|
4
|
+
Auth,
|
|
5
|
+
authCookieForBrowserSession,
|
|
4
6
|
consumeWebAuthnChallenge,
|
|
5
7
|
getUserPasskey,
|
|
8
|
+
resolveBrowserSessionPolicy,
|
|
6
9
|
updatePasskeyCounter,
|
|
7
10
|
verifyAuthenticationResponse,
|
|
8
11
|
} from '@stacksjs/auth'
|
|
@@ -86,7 +89,27 @@ export default new Action({
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
const policy = resolveBrowserSessionPolicy(false)
|
|
93
|
+
const session = await Auth.loginUsingId(user.id as number, {
|
|
94
|
+
expiresInMinutes: policy.expiresInMinutes,
|
|
95
|
+
withRefreshToken: policy.withRefreshToken,
|
|
96
|
+
})
|
|
97
|
+
if (!session)
|
|
98
|
+
return response.serverError('Authentication session could not be created')
|
|
99
|
+
|
|
100
|
+
return response.json({
|
|
101
|
+
...verification,
|
|
102
|
+
access_token: session.token,
|
|
103
|
+
refresh_token: session.refreshToken,
|
|
104
|
+
token_type: 'Bearer',
|
|
105
|
+
expires_in: session.expiresIn,
|
|
106
|
+
token: session.token,
|
|
107
|
+
user: {
|
|
108
|
+
id: session.user?.id,
|
|
109
|
+
email: session.user?.email,
|
|
110
|
+
name: session.user?.name,
|
|
111
|
+
},
|
|
112
|
+
}, { headers: { 'Set-Cookie': authCookieForBrowserSession(session.token, session.expiresIn) } })
|
|
90
113
|
}
|
|
91
114
|
catch (error) {
|
|
92
115
|
console.error('Authentication verification failed:', error)
|
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.74.
|
|
5
|
+
"version": "0.74.64",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
58
|
-
"@stacksjs/mobile": "^0.74.
|
|
58
|
+
"@stacksjs/mobile": "^0.74.64",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Form block: renders a @stacksjs/forms form inline on a CMS page.
|
|
3
3
|
`props.formUuid` names the form; the client script fetches the public
|
|
4
4
|
definition (same-origin), renders the fields, evaluates conditions live,
|
|
5
|
-
and submits
|
|
5
|
+
and submits through useMutation, which repeats the page's CSRF cookie in
|
|
6
|
+
the header the submit route checks.
|
|
6
7
|
--}}
|
|
7
8
|
<div class="cms-form mx-auto max-w-xl px-6 py-8" data-form-uuid="{{ props.formUuid }}">
|
|
8
9
|
<script client>
|
|
@@ -84,24 +85,24 @@
|
|
|
84
85
|
payload.append('field', field.name)
|
|
85
86
|
payload.append('file', file)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// Through the data layer rather than a bare fetch(): it is what echoes
|
|
89
|
+
// the CSRF cookie the submit routes require, and it passes FormData
|
|
90
|
+
// through untouched.
|
|
91
|
+
let body: { path?: string }
|
|
92
|
+
try {
|
|
93
|
+
body = await useMutation(`/api/forms/${uuid}/uploads`).mutate(payload)
|
|
94
|
+
}
|
|
95
|
+
catch (failure) {
|
|
95
96
|
// Clear the input as well as the value: leaving a rejected file
|
|
96
97
|
// showing next to its own error message reads as though it was taken.
|
|
97
98
|
input.value = ''
|
|
98
99
|
delete values[field.name]
|
|
99
100
|
delete attached[field.name]
|
|
100
|
-
errors.set({ ...errors(), [field.name]:
|
|
101
|
+
errors.set({ ...errors(), [field.name]: (failure as { data?: { message?: string } }).data?.message ?? 'That file could not be uploaded.' })
|
|
101
102
|
return
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
values[field.name] = body.path
|
|
105
|
+
values[field.name] = body.path ?? ''
|
|
105
106
|
attached[field.name] = file.name
|
|
106
107
|
}
|
|
107
108
|
catch {
|
|
@@ -144,20 +145,19 @@
|
|
|
144
145
|
|
|
145
146
|
try {
|
|
146
147
|
const uuid = host()?.dataset.formUuid
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const body = await reply.json().catch(() => ({}))
|
|
154
|
-
|
|
155
|
-
if (reply.status === 422) {
|
|
156
|
-
errors.set(body.errors ?? {})
|
|
157
|
-
return
|
|
148
|
+
// useMutation, not fetch(): the submit route is CSRF-protected and the
|
|
149
|
+
// data layer is what repeats the cookie in the header. A bare fetch got
|
|
150
|
+
// 403 "CSRF token mismatch" on every submission.
|
|
151
|
+
let body: { message?: string, redirect?: string }
|
|
152
|
+
try {
|
|
153
|
+
body = await useMutation(`/api/forms/${uuid}/submissions`).mutate({ ...values, _renderedMs: Date.now() - openedAt })
|
|
158
154
|
}
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
catch (failure) {
|
|
156
|
+
const { status, data } = failure as { status?: number, data?: { errors?: FormErrors, message?: string } }
|
|
157
|
+
if (status === 422)
|
|
158
|
+
errors.set(data?.errors ?? {})
|
|
159
|
+
else
|
|
160
|
+
errors.set({ _form: data?.message ?? 'Something went wrong. Please try again.' })
|
|
161
161
|
return
|
|
162
162
|
}
|
|
163
163
|
if (body.redirect) {
|
package/routes/auth.ts
CHANGED
|
@@ -53,7 +53,7 @@ route.post('/verify-registration', 'Actions/Auth/VerifyRegistrationAction').midd
|
|
|
53
53
|
// Passkey AUTHENTICATION (logging in) is correctly unauthenticated —
|
|
54
54
|
// the caller doesn't have a session yet, that's the point.
|
|
55
55
|
route.get('/generate-authentication-options', 'Actions/Auth/GenerateAuthenticationAction').rateLimit(10, 'minute')
|
|
56
|
-
route.
|
|
56
|
+
route.post('/verify-authentication', 'Actions/Auth/VerifyAuthenticationAction').rateLimit(10, 'minute')
|
|
57
57
|
|
|
58
58
|
// TOTP 2FA. Setup/enable/disable act on the caller's own authenticated
|
|
59
59
|
// account (auth-gated, same identity rule as passkey enrollment above).
|
package/routes/forms.ts
CHANGED
|
@@ -82,7 +82,7 @@ route.post('/api/forms/{uuid}/uploads', async (request: any) => {
|
|
|
82
82
|
}).rateLimit(20, 'minute')
|
|
83
83
|
|
|
84
84
|
route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
85
|
-
const { dispatchSubmissionNotifications, loadFormByUuid, submitForm } = await import('@stacksjs/forms')
|
|
85
|
+
const { dispatchSubmissionNotifications, loadFormByUuid, submissionIdentity, submitForm } = await import('@stacksjs/forms')
|
|
86
86
|
|
|
87
87
|
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
|
|
88
88
|
if (!form)
|
|
@@ -105,10 +105,14 @@ route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
|
105
105
|
|
|
106
106
|
// Fire-and-forget AFTER the write: a slow mail transport must not hold a
|
|
107
107
|
// parent's phone on a spinner, and a failed one must not undo the answers.
|
|
108
|
+
// The identity lookup belongs inside that chain too. It used to be awaited
|
|
109
|
+
// in the handler, so when it threw - and it always did, selecting a
|
|
110
|
+
// `values` column that has never existed - the visitor got a 500 for an
|
|
111
|
+
// answer that was already saved, and every retry saved another copy.
|
|
108
112
|
if (result.submissionId > 0) {
|
|
109
|
-
void
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
void submissionIdentity(result.submissionId)
|
|
114
|
+
.then(identity => dispatchSubmissionNotifications(form, result, identity))
|
|
115
|
+
.catch(() => {})
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
return response.json({
|
|
@@ -119,38 +123,3 @@ route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
|
119
123
|
redirect: result.redirect,
|
|
120
124
|
}, { status: 201 })
|
|
121
125
|
}).rateLimit(10, 'minute')
|
|
122
|
-
|
|
123
|
-
async function submissionIdentity(submissionId: number): Promise<{ email: string | null, name: string | null, values: Record<string, unknown> }> {
|
|
124
|
-
const { db } = await import('@stacksjs/database')
|
|
125
|
-
const row = await db
|
|
126
|
-
.selectFrom('form_submissions')
|
|
127
|
-
.where('id', '=', submissionId)
|
|
128
|
-
.select(['email', 'name', 'values'])
|
|
129
|
-
.executeTakeFirst() as { email: string | null, name: string | null, values: string | null } | undefined
|
|
130
|
-
|
|
131
|
-
let values: Record<string, unknown> = {}
|
|
132
|
-
try {
|
|
133
|
-
values = row?.values ? JSON.parse(row.values) as Record<string, unknown> : {}
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// unreadable values only degrade the notification summary
|
|
137
|
-
}
|
|
138
|
-
return { email: row?.email ?? null, name: row?.name ?? null, values }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Admin CSV export. Auth'd; site scoping rides the form lookup. */
|
|
142
|
-
route.get('/api/admin/forms/{uuid}/submissions.csv', async (request: any) => {
|
|
143
|
-
const { exportSubmissionsCsv, loadFormByUuid } = await import('@stacksjs/forms')
|
|
144
|
-
|
|
145
|
-
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
|
|
146
|
-
if (!form)
|
|
147
|
-
return response.notFound('Form not found')
|
|
148
|
-
|
|
149
|
-
const csv = await exportSubmissionsCsv(form)
|
|
150
|
-
return new Response(csv, {
|
|
151
|
-
headers: {
|
|
152
|
-
'Content-Type': 'text/csv; charset=utf-8',
|
|
153
|
-
'Content-Disposition': `attachment; filename="${form.handle}-submissions.csv"`,
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
}).middleware('auth')
|