@stacksjs/defaults 0.74.56 → 0.74.58
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/LoginAction.ts +11 -3
- package/app/Actions/Auth/MagicLinkConsumeAction.ts +7 -4
- package/app/Actions/Auth/VerifyTwoFactorLoginAction.ts +6 -11
- package/app/Middleware/Csrf.ts +1 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/vcs/github/workflows/deploy.yml +44 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth,
|
|
2
|
+
import { Auth, authCookieForBrowserSession, createTwoFactorChallenge, getTwoFactorState, resolveBrowserSessionPolicy } from '@stacksjs/auth'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { schema } from '@stacksjs/validation'
|
|
5
5
|
import { PASSWORD_MAX_LENGTH, PASSWORD_PRESENCE_MESSAGE } from '../../password-policy'
|
|
@@ -28,6 +28,7 @@ export default new Action({
|
|
|
28
28
|
async handle(request: RequestInstance) {
|
|
29
29
|
const email = request.get('email')
|
|
30
30
|
const password = request.get('password')
|
|
31
|
+
const remember = request.get('remember')
|
|
31
32
|
|
|
32
33
|
// Verify once, then keep the observed password version locked while
|
|
33
34
|
// choosing and issuing the challenge or token. A completed password reset
|
|
@@ -37,7 +38,14 @@ export default new Action({
|
|
|
37
38
|
if (twoFactorEnabled) {
|
|
38
39
|
return { kind: 'challenge' as const, token: await createTwoFactorChallenge(authedUser.id as number) }
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
const policy = resolveBrowserSessionPolicy(remember)
|
|
42
|
+
return {
|
|
43
|
+
kind: 'login' as const,
|
|
44
|
+
result: await Auth.loginUsingId(authedUser.id as number, {
|
|
45
|
+
expiresInMinutes: policy.expiresInMinutes,
|
|
46
|
+
withRefreshToken: policy.withRefreshToken,
|
|
47
|
+
}),
|
|
48
|
+
}
|
|
41
49
|
})
|
|
42
50
|
if (!decision)
|
|
43
51
|
return response.unauthorized('Incorrect email or password')
|
|
@@ -84,6 +92,6 @@ export default new Action({
|
|
|
84
92
|
email: user?.email,
|
|
85
93
|
name: user?.name,
|
|
86
94
|
},
|
|
87
|
-
}, { headers: { 'Set-Cookie':
|
|
95
|
+
}, { headers: { 'Set-Cookie': authCookieForBrowserSession(result.token, result.expiresIn) } })
|
|
88
96
|
},
|
|
89
97
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth, authCookie,
|
|
2
|
+
import { Auth, authCookie, withMagicLink } from '@stacksjs/auth'
|
|
3
3
|
import { config } from '@stacksjs/config'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
5
|
import { schema } from '@stacksjs/validation'
|
|
@@ -20,7 +20,10 @@ export default new Action({
|
|
|
20
20
|
if (!config.auth.magicLink?.enabled)
|
|
21
21
|
return response.notFound('Magic-link sign-in is not enabled')
|
|
22
22
|
|
|
23
|
-
const consumed = await
|
|
23
|
+
const consumed = await withMagicLink(String(request.get('token')), async grant => ({
|
|
24
|
+
grant,
|
|
25
|
+
result: await Auth.loginUsingId(grant.userId),
|
|
26
|
+
}))
|
|
24
27
|
if (!consumed.ok) {
|
|
25
28
|
const messages: Record<string, string> = {
|
|
26
29
|
invalid: 'That sign-in link is not valid.',
|
|
@@ -33,7 +36,7 @@ export default new Action({
|
|
|
33
36
|
|
|
34
37
|
// The same token pack + httpOnly cookie a password login issues, so
|
|
35
38
|
// stxPageAuthMiddleware-gated pages treat passwordless users identically.
|
|
36
|
-
const result =
|
|
39
|
+
const { grant, result } = consumed.value
|
|
37
40
|
if (!result)
|
|
38
41
|
return response.unauthorized('That sign-in link is not valid.')
|
|
39
42
|
|
|
@@ -42,7 +45,7 @@ export default new Action({
|
|
|
42
45
|
refresh_token: result.refreshToken,
|
|
43
46
|
token_type: 'Bearer',
|
|
44
47
|
expires_in: result.expiresIn,
|
|
45
|
-
redirect_to:
|
|
48
|
+
redirect_to: grant.redirectTo,
|
|
46
49
|
user: {
|
|
47
50
|
id: result.user?.id,
|
|
48
51
|
email: result.user?.email,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth, authCookie,
|
|
2
|
+
import { Auth, authCookie, verifyTwoFactorLoginCode, withTwoFactorChallenge } from '@stacksjs/auth'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { schema } from '@stacksjs/validation'
|
|
5
5
|
|
|
@@ -27,17 +27,12 @@ export default new Action({
|
|
|
27
27
|
// (whether the code was right or wrong) must start over from
|
|
28
28
|
// LoginAction, not retry — mirrors the WebAuthn challenge
|
|
29
29
|
// delete-on-read semantics in passkey.ts (stacksjs/stacks#1866).
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
const valid = await verifyTwoFactorLoginCode(userId, code)
|
|
35
|
-
if (!valid)
|
|
36
|
-
return response.unauthorized('Invalid code — please sign in again.')
|
|
37
|
-
|
|
38
|
-
const result = await Auth.loginUsingId(userId)
|
|
30
|
+
const result = await withTwoFactorChallenge(challengeToken, async (userId) => {
|
|
31
|
+
if (!await verifyTwoFactorLoginCode(userId, code)) return null
|
|
32
|
+
return Auth.loginUsingId(userId)
|
|
33
|
+
})
|
|
39
34
|
if (!result)
|
|
40
|
-
return response.unauthorized('Invalid
|
|
35
|
+
return response.unauthorized('Invalid or expired login attempt. Please sign in again.')
|
|
41
36
|
|
|
42
37
|
const user = result.user
|
|
43
38
|
|
package/app/Middleware/Csrf.ts
CHANGED
|
@@ -159,7 +159,7 @@ export function createCsrfCookie(req: Request, minted?: string): string {
|
|
|
159
159
|
* behaviour: it says nothing about what the browser is looking at.
|
|
160
160
|
*/
|
|
161
161
|
export function responseMayUseCsrfToken(response: Response): boolean {
|
|
162
|
-
const type = (response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
|
|
162
|
+
const type = ((response.headers.get('content-type') || '').split(';')[0] ?? '').trim().toLowerCase()
|
|
163
163
|
if (!type)
|
|
164
164
|
return true
|
|
165
165
|
return type === 'text/html'
|
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.58",
|
|
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.58",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
|
@@ -28,10 +28,53 @@ concurrency:
|
|
|
28
28
|
cancel-in-progress: false
|
|
29
29
|
|
|
30
30
|
jobs:
|
|
31
|
+
# Is the commit CI passed still the tip of main?
|
|
32
|
+
#
|
|
33
|
+
# Deploys queue one at a time, and each checks out the commit its CI run
|
|
34
|
+
# was for. CI runs are not ordered: when two pushes land close together the
|
|
35
|
+
# older one can finish CI last, and its deploy then replaces the newer
|
|
36
|
+
# release with both deploys green. Checked here, when the deploy's turn
|
|
37
|
+
# comes: if main has moved on, the newer commit's own CI run deploys it.
|
|
38
|
+
current:
|
|
39
|
+
if: github.event_name == 'workflow_run'
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
permissions:
|
|
42
|
+
contents: read
|
|
43
|
+
outputs:
|
|
44
|
+
latest: ${{ steps.tip.outputs.latest }}
|
|
45
|
+
steps:
|
|
46
|
+
- id: tip
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ github.token }}
|
|
49
|
+
SHA: ${{ github.event.workflow_run.head_sha }}
|
|
50
|
+
run: |
|
|
51
|
+
tip=$(gh api "repos/${{ github.repository }}/commits/main" --jq .sha)
|
|
52
|
+
if [ "$tip" = "$SHA" ]; then
|
|
53
|
+
echo "latest=true" >> "$GITHUB_OUTPUT"
|
|
54
|
+
else
|
|
55
|
+
echo "latest=false" >> "$GITHUB_OUTPUT"
|
|
56
|
+
echo "::notice::Skipping ${SHA:0:8}: main is at ${tip:0:8}, whose CI run deploys it."
|
|
57
|
+
fi
|
|
58
|
+
|
|
31
59
|
deploy:
|
|
60
|
+
needs: current
|
|
32
61
|
# `workflow_run` fires for failed runs too — that is the point of the
|
|
33
62
|
# gate, so check the conclusion.
|
|
34
|
-
|
|
63
|
+
#
|
|
64
|
+
# And check what CI ran on. CI also runs for pull requests, and
|
|
65
|
+
# `branches: [main]` matches the head branch name, so a fork's PR from a
|
|
66
|
+
# branch called `main` that passed CI would deploy the fork's commit with
|
|
67
|
+
# the production secrets. Only a push to this repository ships.
|
|
68
|
+
#
|
|
69
|
+
# `always()` because `current` is skipped for a manual dispatch, which
|
|
70
|
+
# ships whatever it was pointed at.
|
|
71
|
+
if: >-
|
|
72
|
+
always() && (
|
|
73
|
+
github.event_name == 'workflow_dispatch' ||
|
|
74
|
+
(github.event.workflow_run.conclusion == 'success' &&
|
|
75
|
+
github.event.workflow_run.event == 'push' &&
|
|
76
|
+
github.event.workflow_run.head_repository.full_name == github.repository &&
|
|
77
|
+
needs.current.outputs.latest == 'true'))
|
|
35
78
|
runs-on: ubuntu-latest
|
|
36
79
|
# Attach a GitHub Environment to get required reviewers and to scope these
|
|
37
80
|
# secrets to it.
|