@stacksjs/defaults 0.70.260 → 0.70.262
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.
|
@@ -46,7 +46,17 @@ export default new Action({
|
|
|
46
46
|
to: user?.email,
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
+
// Same OAuth2-compatible payload LoginAction returns, so a client can
|
|
50
|
+
// code against one shape. Registering used to hand back only `token`,
|
|
51
|
+
// which left brand-new accounts with no refresh token to exchange — they
|
|
52
|
+
// were signed out an hour into their first session while every other
|
|
53
|
+
// user refreshed normally (stacksjs/stacks#2212). The legacy `token`
|
|
54
|
+
// alias stays for backward compatibility.
|
|
49
55
|
return response.json({
|
|
56
|
+
access_token: result.token,
|
|
57
|
+
refresh_token: result.refreshToken,
|
|
58
|
+
token_type: 'Bearer',
|
|
59
|
+
expires_in: result.expiresIn,
|
|
50
60
|
token: result.token,
|
|
51
61
|
user: {
|
|
52
62
|
id: user?.id,
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { RateLimiter } from '@stacksjs/auth'
|
|
3
|
+
import { log } from '@stacksjs/logging'
|
|
3
4
|
import { User } from '@stacksjs/orm'
|
|
4
5
|
import { job } from '@stacksjs/queue'
|
|
5
6
|
import { response } from '@stacksjs/router'
|
|
6
7
|
import { schema } from '@stacksjs/validation'
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The one thing this endpoint ever says.
|
|
11
|
+
*
|
|
12
|
+
* Held in a constant because the security property is that EVERY path returns
|
|
13
|
+
* it byte for byte — unknown address, known address, and a mail dispatch that
|
|
14
|
+
* blew up. The moment one branch returns something else, the endpoint becomes
|
|
15
|
+
* an account-existence oracle again (stacksjs/stacks#2214).
|
|
16
|
+
*/
|
|
17
|
+
const NEUTRAL_RESPONSE = 'If an account exists for that email address, a password reset link has been sent.'
|
|
18
|
+
|
|
8
19
|
export default new Action({
|
|
9
20
|
name: 'SendPasswordResetEmailAction',
|
|
10
21
|
description: 'Send Password Reset Email',
|
|
@@ -15,52 +26,45 @@ export default new Action({
|
|
|
15
26
|
},
|
|
16
27
|
},
|
|
17
28
|
async handle(request) {
|
|
18
|
-
console.log('[Action] SendPasswordResetEmailAction.handle() called')
|
|
19
|
-
|
|
20
29
|
const email = request.get('email')
|
|
21
|
-
console.log('[Action] Email from request:', email)
|
|
22
30
|
|
|
23
|
-
if (!email)
|
|
24
|
-
console.log('[Action] No email provided, returning 422')
|
|
31
|
+
if (!email)
|
|
25
32
|
return response.error('Email is required', 422)
|
|
26
|
-
}
|
|
27
33
|
|
|
28
|
-
// Rate
|
|
34
|
+
// Rate limiting is per-email, so it does not slow enumeration ACROSS
|
|
35
|
+
// addresses at all — the shape of the attack this endpoint invites. It
|
|
36
|
+
// stays because it still limits hammering one mailbox, but it is not the
|
|
37
|
+
// control that protects existence; the uniform response below is.
|
|
29
38
|
const rateLimitKey = `password_reset:${email.toLowerCase()}`
|
|
30
|
-
if (await RateLimiter.isRateLimited(rateLimitKey))
|
|
31
|
-
console.log('[Action] Rate limited for:', rateLimitKey)
|
|
39
|
+
if (await RateLimiter.isRateLimited(rateLimitKey))
|
|
32
40
|
return response.error('Too many password reset attempts. Please try again later.', 429)
|
|
33
|
-
}
|
|
34
41
|
|
|
35
|
-
// Record the attempt
|
|
36
42
|
await RateLimiter.recordFailedAttempt(rateLimitKey)
|
|
37
|
-
console.log('[Action] Recorded rate limit attempt')
|
|
38
43
|
|
|
39
|
-
// Check if user exists
|
|
40
|
-
console.log('[Action] Looking up user by email...')
|
|
41
44
|
const user = await User.where('email', email).first()
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
// No early return for a missing user. It used to answer
|
|
47
|
+
// `404 "No account found with this email address."`, which let anyone test
|
|
48
|
+
// an arbitrary list of addresses for membership against an unauthenticated
|
|
49
|
+
// endpoint. The sibling `PasswordResetAction` already refuses to leak the
|
|
50
|
+
// same fact, so this was an inconsistency between two files in one
|
|
51
|
+
// directory rather than a disagreement about the threat.
|
|
52
|
+
if (user) {
|
|
53
|
+
try {
|
|
54
|
+
await job('SendPasswordResetEmailJob', { email })
|
|
55
|
+
.onQueue('emails')
|
|
56
|
+
.dispatch()
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Swallowed DELIBERATELY, and this is the subtle half. Returning a 500
|
|
60
|
+
// here would reintroduce the oracle whenever the mailer is degraded:
|
|
61
|
+
// an unknown address never reaches dispatch, so it can never 5xx, and
|
|
62
|
+
// "neutral message vs. send failure" becomes the tell. The operator
|
|
63
|
+
// gets the failure in the logs; the caller gets what everyone gets.
|
|
64
|
+
log.error('[password-reset] failed to dispatch reset email', error)
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
return response.success('Password reset link has been sent to your email.')
|
|
68
|
+
return response.success(NEUTRAL_RESPONSE)
|
|
65
69
|
},
|
|
66
70
|
})
|
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.70.
|
|
5
|
+
"version": "0.70.262",
|
|
6
6
|
"description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|