@stacksjs/defaults 0.74.57 → 0.74.59
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 +15 -4
- package/app/Actions/Auth/MagicLinkConsumeAction.ts +7 -4
- package/app/Actions/Auth/RegisterAction.ts +7 -3
- package/app/Actions/Auth/VerifyTwoFactorLoginAction.ts +11 -12
- 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,16 +28,27 @@ 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
|
|
34
35
|
// cannot be followed by this old in-flight login minting fresh access.
|
|
35
36
|
const decision = await Auth.withVerifiedCredentials({ email, password }, async (authedUser) => {
|
|
36
37
|
const { enabled: twoFactorEnabled } = await getTwoFactorState(authedUser.id as number)
|
|
38
|
+
const policy = resolveBrowserSessionPolicy(remember)
|
|
37
39
|
if (twoFactorEnabled) {
|
|
38
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'challenge' as const,
|
|
42
|
+
token: await createTwoFactorChallenge(authedUser.id as number, { remembered: policy.remembered }),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
kind: 'login' as const,
|
|
47
|
+
result: await Auth.loginUsingId(authedUser.id as number, {
|
|
48
|
+
expiresInMinutes: policy.expiresInMinutes,
|
|
49
|
+
withRefreshToken: policy.withRefreshToken,
|
|
50
|
+
}),
|
|
39
51
|
}
|
|
40
|
-
return { kind: 'login' as const, result: await Auth.loginUsingId(authedUser.id as number) }
|
|
41
52
|
})
|
|
42
53
|
if (!decision)
|
|
43
54
|
return response.unauthorized('Incorrect email or password')
|
|
@@ -84,6 +95,6 @@ export default new Action({
|
|
|
84
95
|
email: user?.email,
|
|
85
96
|
name: user?.name,
|
|
86
97
|
},
|
|
87
|
-
}, { headers: { 'Set-Cookie':
|
|
98
|
+
}, { headers: { 'Set-Cookie': authCookieForBrowserSession(result.token, result.expiresIn) } })
|
|
88
99
|
},
|
|
89
100
|
})
|
|
@@ -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,
|
|
2
|
+
import { Auth, authCookieForBrowserSession, register, resolveBrowserSessionPolicy } from '@stacksjs/auth'
|
|
3
3
|
import { dispatch } from '@stacksjs/events'
|
|
4
4
|
import { response } from '@stacksjs/router'
|
|
5
5
|
import { schema } from '@stacksjs/validation'
|
|
@@ -29,9 +29,13 @@ export default new Action({
|
|
|
29
29
|
const email = request.get('email')
|
|
30
30
|
const password = request.get('password')
|
|
31
31
|
const name = request.get('name')
|
|
32
|
+
const policy = resolveBrowserSessionPolicy(request.get('remember'))
|
|
32
33
|
|
|
33
34
|
const referralCode = request.get('referralCode')
|
|
34
|
-
const result = await register(
|
|
35
|
+
const result = await register(
|
|
36
|
+
{ email, password, name, referralCode: typeof referralCode === 'string' ? referralCode : undefined },
|
|
37
|
+
{ expiresInMinutes: policy.expiresInMinutes, withRefreshToken: policy.withRefreshToken },
|
|
38
|
+
)
|
|
35
39
|
|
|
36
40
|
if (result) {
|
|
37
41
|
const user = await Auth.getUserFromToken(result.token)
|
|
@@ -72,7 +76,7 @@ export default new Action({
|
|
|
72
76
|
email: user?.email,
|
|
73
77
|
name: user?.name,
|
|
74
78
|
},
|
|
75
|
-
}, { headers: { 'Set-Cookie':
|
|
79
|
+
}, { headers: { 'Set-Cookie': authCookieForBrowserSession(result.token, result.expiresIn) } })
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
return response.error('Registration failed')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import { Auth,
|
|
2
|
+
import { Auth, authCookieForBrowserSession, resolveBrowserSessionPolicy, verifyTwoFactorLoginCode, withTwoFactorChallenge } from '@stacksjs/auth'
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { schema } from '@stacksjs/validation'
|
|
5
5
|
|
|
@@ -27,17 +27,16 @@ 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const result = await Auth.loginUsingId(userId)
|
|
30
|
+
const result = await withTwoFactorChallenge(challengeToken, async (userId, challenge) => {
|
|
31
|
+
if (!await verifyTwoFactorLoginCode(userId, code)) return null
|
|
32
|
+
const policy = resolveBrowserSessionPolicy(challenge.remembered)
|
|
33
|
+
return Auth.loginUsingId(userId, {
|
|
34
|
+
expiresInMinutes: policy.expiresInMinutes,
|
|
35
|
+
withRefreshToken: policy.withRefreshToken,
|
|
36
|
+
})
|
|
37
|
+
})
|
|
39
38
|
if (!result)
|
|
40
|
-
return response.unauthorized('Invalid
|
|
39
|
+
return response.unauthorized('Invalid or expired login attempt. Please sign in again.')
|
|
41
40
|
|
|
42
41
|
const user = result.user
|
|
43
42
|
|
|
@@ -56,6 +55,6 @@ export default new Action({
|
|
|
56
55
|
email: user?.email,
|
|
57
56
|
name: user?.name,
|
|
58
57
|
},
|
|
59
|
-
}, { headers: { 'Set-Cookie':
|
|
58
|
+
}, { headers: { 'Set-Cookie': authCookieForBrowserSession(result.token, result.expiresIn) } })
|
|
60
59
|
},
|
|
61
60
|
})
|
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.59",
|
|
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.59",
|
|
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.
|