mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.5' into fix/email-example
This commit is contained in:
2
.github/workflows/check-code-style.yml
vendored
2
.github/workflows/check-code-style.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- run: npm ci
|
||||
- name: Run ESLint@8
|
||||
run: npx eslint@8 "./npm-packages/meteor-installer/**/*.js"
|
||||
|
||||
2
.github/workflows/check-syntax.yml
vendored
2
.github/workflows/check-syntax.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- run: cd scripts/admin/check-legacy-syntax && npm ci
|
||||
- name: Check syntax
|
||||
run: cd scripts/admin/check-legacy-syntax && node check-syntax.js
|
||||
|
||||
3
.github/workflows/e2e-tests.yml
vendored
3
.github/workflows/e2e-tests.yml
vendored
@@ -32,7 +32,8 @@ jobs:
|
||||
- Babel
|
||||
- Blaze
|
||||
- Coffeescript
|
||||
- Library
|
||||
- Full Skeleton
|
||||
- Tailwind Skeleton
|
||||
- Monorepo
|
||||
- React
|
||||
- R.Router
|
||||
|
||||
2
.github/workflows/guide.yml
vendored
2
.github/workflows/guide.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
- name: Build the Guide
|
||||
run: npm ci && npm run build
|
||||
- name: Deploy to Netlify for preview
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
2
.github/workflows/test-packages.yml
vendored
2
.github/workflows/test-packages.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
- 'oplog,polling'
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.reactivity_order }}
|
||||
cancel-in-progress: true
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
|
||||
2
.github/workflows/windows-selftest.yml
vendored
2
.github/workflows/windows-selftest.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 24.x
|
||||
|
||||
- name: Cache dependencies
|
||||
id: meteor-cache
|
||||
|
||||
34
.travis.yml
34
.travis.yml
@@ -1,34 +0,0 @@
|
||||
language: node_js
|
||||
os: linux
|
||||
dist: jammy
|
||||
sudo: required
|
||||
services: xvfb
|
||||
node_js:
|
||||
- "22.17.0"
|
||||
cache:
|
||||
directories:
|
||||
- ".meteor"
|
||||
- ".babel-cache"
|
||||
script:
|
||||
- travis_retry ./packages/test-in-console/run.sh
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-12
|
||||
- phantom=false
|
||||
- PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium
|
||||
- TEST_PACKAGES_EXCLUDE=stylus
|
||||
- METEOR_MODERN=true
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-12
|
||||
- libnss3
|
||||
|
||||
before_install:
|
||||
- cat /etc/apt/sources.list
|
||||
- python3 --version
|
||||
- echo "deb http://archive.ubuntu.com/ubuntu jammy main universe" | sudo tee -a /etc/apt/sources.list
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y libnss3
|
||||
2
meteor
2
meteor
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BUNDLE_VERSION=22.22.0.6
|
||||
BUNDLE_VERSION=24.14.0.1
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -5,10 +5,10 @@ set -u
|
||||
|
||||
UNAME=$(uname)
|
||||
ARCH=$(uname -m)
|
||||
NODE_VERSION=14.21.3
|
||||
NODE_VERSION=24.14.0
|
||||
MONGO_VERSION_64BIT=6.0.3
|
||||
MONGO_VERSION_32BIT=3.2.22
|
||||
NPM_VERSION=6.14.18
|
||||
NPM_VERSION=11.10.1
|
||||
|
||||
if [ "$UNAME" == "Linux" ] ; then
|
||||
if [ "$ARCH" != "i686" -a "$ARCH" != "x86_64" ] ; then
|
||||
|
||||
@@ -10,7 +10,7 @@ var packageJson = {
|
||||
dependencies: {
|
||||
// Explicit dependency because we are replacing it with a bundled version
|
||||
// and we want to make sure there are no dependencies on a higher version
|
||||
npm: "10.9.4",
|
||||
npm: "11.10.1",
|
||||
pacote: "https://github.com/meteor/pacote/tarball/a81b0324686e85d22c7688c47629d4009000e8b8",
|
||||
"node-gyp": "9.4.0",
|
||||
"@mapbox/node-pre-gyp": "1.0.11",
|
||||
|
||||
36
npm-packages/meteor-node-stubs/package-lock.json
generated
36
npm-packages/meteor-node-stubs/package-lock.json
generated
@@ -171,9 +171,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@meteorjs/create-ecdh/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -254,9 +254,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -319,9 +319,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
|
||||
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -654,9 +654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1184,9 +1184,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1459,9 +1459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"inBundle": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
2201
package-lock.json
generated
2201
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -12,27 +12,27 @@
|
||||
},
|
||||
"homepage": "https://www.meteor.com/",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/eslint-parser": "^7.21.3",
|
||||
"@babel/eslint-plugin": "^7.19.1",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/eslint-parser": "^7.28.6",
|
||||
"@babel/eslint-plugin": "^7.27.1",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@types/lodash.isempty": "^4.4.9",
|
||||
"@types/node": "^18.16.18",
|
||||
"@types/node": "^24.10.13",
|
||||
"@types/sockjs": "^0.3.36",
|
||||
"@types/sockjs-client": "^1.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-vazco": "^7.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.2",
|
||||
"eslint-config-vazco": "^7.4.0",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^2.8.8",
|
||||
"typescript": "^5.4.5"
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"scripts": {
|
||||
"install:modern": "cd tools/modern-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell",
|
||||
|
||||
17
packages/accounts-base/accounts-base.d.ts
vendored
17
packages/accounts-base/accounts-base.d.ts
vendored
@@ -3,11 +3,20 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { Configuration } from 'meteor/service-configuration';
|
||||
import { DDP } from 'meteor/ddp';
|
||||
|
||||
/**
|
||||
* Object containing functions that generate URLs for account-related emails.
|
||||
* Override these to customize URLs in password reset, enrollment, and verification emails.
|
||||
* URL methods can return either a string or a Promise that resolves to a string.
|
||||
*/
|
||||
export interface URLS {
|
||||
resetPassword: (token: string) => string;
|
||||
verifyEmail: (token: string) => string;
|
||||
loginToken: (token: string) => string;
|
||||
enrollAccount: (token: string) => string;
|
||||
/** Generates the URL for password reset emails. Can return a Promise for async URL generation. */
|
||||
resetPassword: (token: string, extraParams?: Record<string, string>) => string | Promise<string>;
|
||||
/** Generates the URL for email verification emails. Can return a Promise for async URL generation. */
|
||||
verifyEmail: (token: string, extraParams?: Record<string, string>) => string | Promise<string>;
|
||||
/** Generates the URL for login token emails. Can return a Promise for async URL generation. */
|
||||
loginToken: (selector: string, token: string, extraParams?: Record<string, string>) => string | Promise<string>;
|
||||
/** Generates the URL for account enrollment emails. Can return a Promise for async URL generation. */
|
||||
enrollAccount: (token: string, extraParams?: Record<string, string>) => string | Promise<string>;
|
||||
}
|
||||
|
||||
export interface EmailFields {
|
||||
|
||||
@@ -28,14 +28,21 @@ if (Meteor.isClient) {
|
||||
Accounts._isolateLoginTokenForTest();
|
||||
const username = `u3_${Random.id()}`;
|
||||
const password = `p3_${Random.id()}`;
|
||||
await new Promise((resolve, reject) => Accounts.createUser({ username, password }, (e)=> e?reject(e):resolve()));
|
||||
await new Promise((resolve, reject) => Accounts.createUser({ username, password }, (e) => e ? reject(e) : resolve()));
|
||||
test.isTrue(!!Meteor.userId());
|
||||
|
||||
// Perform explicit fetch to refresh endpoint to ensure cookie present
|
||||
let r = await fetch('/_accounts/cookie/refresh', { credentials: 'include' });
|
||||
test.isTrue(r.status === 200 || r.status === 204, 'refresh reachable');
|
||||
// Poll refresh until cookie is set (200), because _setHttpOnlyCookie is async and may not
|
||||
// have completed yet — a stale cookie from a previous test could also cause 401. will be fixed after https://github.com/meteor/meteor/pull/14069
|
||||
let r;
|
||||
const loginStart = Date.now();
|
||||
while (true) {
|
||||
r = await fetch('/_accounts/cookie/refresh', { credentials: 'include' });
|
||||
if (r.status === 200 || Date.now() - loginStart > 4000) break;
|
||||
await new Promise(res => setTimeout(res, 100));
|
||||
}
|
||||
test.equal(r.status, 200, 'cookie set after login');
|
||||
|
||||
await new Promise(res => Meteor.logout(()=>res()));
|
||||
await new Promise(res => Meteor.logout(() => res()));
|
||||
test.isFalse(!!Meteor.userId());
|
||||
|
||||
// Poll refresh until 204 or timeout because _clearHttpOnlyCookie is async
|
||||
|
||||
@@ -83,6 +83,25 @@ export class AccountsServer extends AccountsCommon {
|
||||
return Meteor._isPromise(value) ? await value : value;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Object containing functions that generate URLs for account-related emails.
|
||||
* Override these to customize URLs in emails sent by
|
||||
* [`Accounts.sendResetPasswordEmail`](#Accounts-sendResetPasswordEmail),
|
||||
* [`Accounts.sendEnrollmentEmail`](#Accounts-sendEnrollmentEmail), and
|
||||
* [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail).
|
||||
*
|
||||
* By default, URLs use hash fragments (e.g., `#/reset-password/:token`) for security:
|
||||
* hash fragments are not sent to the server in HTTP requests, preventing tokens from
|
||||
* appearing in server logs or referrer headers.
|
||||
* @locus Server
|
||||
* @memberof Accounts
|
||||
* @name urls
|
||||
* @type {Object}
|
||||
* @property {Function} resetPassword - `(token, extraParams) => string` - Generates password reset URL.
|
||||
* @property {Function} verifyEmail - `(token, extraParams) => string` - Generates email verification URL.
|
||||
* @property {Function} enrollAccount - `(token, extraParams) => string` - Generates account enrollment URL.
|
||||
* @property {Function} loginToken - `(selector, token, extraParams) => string` - Generates login token URL.
|
||||
*/
|
||||
this.urls = {
|
||||
resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams),
|
||||
verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams),
|
||||
@@ -93,6 +112,16 @@ export class AccountsServer extends AccountsCommon {
|
||||
|
||||
this.addDefaultRateLimit();
|
||||
|
||||
/**
|
||||
* @summary Builds a URL for account-related emails by combining the app's
|
||||
* root URL with a path and optional extra parameters.
|
||||
* @locus Server
|
||||
* @memberof Accounts
|
||||
* @name buildEmailUrl
|
||||
* @param {String} path - The path to append to the root URL (e.g., `#/reset-password/TOKEN`).
|
||||
* @param {Object} [extraParams={}] - Additional query parameters to include in the URL.
|
||||
* @returns {String} The complete URL.
|
||||
*/
|
||||
this.buildEmailUrl = (path, extraParams = {}) => {
|
||||
const url = new URL(Meteor.absoluteUrl(path));
|
||||
const params = Object.entries(extraParams);
|
||||
|
||||
@@ -36,7 +36,7 @@ fi
|
||||
|
||||
echo Found build $DIRNAME
|
||||
|
||||
trap "echo Found surprising number of tarballs." EXIT
|
||||
trap "echo 'Found surprising number of tarballs.'; aws s3 ls s3://com.meteor.jenkins/$DIRNAME/" EXIT
|
||||
# Check to make sure the proper number of each kind of file is there.
|
||||
aws s3 ls s3://com.meteor.jenkins/$DIRNAME/ | \
|
||||
perl -nle 'if (/\.tar\.gz/) { ++$TAR } else { die "something weird" } END { exit !($TAR == 4) }'
|
||||
|
||||
@@ -5,10 +5,10 @@ set -u
|
||||
|
||||
UNAME=$(uname)
|
||||
ARCH=$(uname -m)
|
||||
NODE_VERSION=22.22.0
|
||||
NODE_VERSION=24.14.0
|
||||
MONGO_VERSION_64BIT=7.0.16
|
||||
MONGO_VERSION_32BIT=3.2.22
|
||||
NPM_VERSION=10.9.4
|
||||
NPM_VERSION=11.10.1
|
||||
|
||||
|
||||
if [ "$UNAME" == "Linux" ] ; then
|
||||
|
||||
@@ -10,7 +10,7 @@ var packageJson = {
|
||||
dependencies: {
|
||||
// Explicit dependency because we are replacing it with a bundled version
|
||||
// and we want to make sure there are no dependencies on a higher version
|
||||
npm: "10.9.4",
|
||||
npm: "11.10.1",
|
||||
"node-gyp": "10.2.0",
|
||||
"node-gyp-build": "4.8.4",
|
||||
"@mapbox/node-pre-gyp": "1.0.11",
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('Meteor Skeletons /', () => {
|
||||
);
|
||||
|
||||
describe(
|
||||
'Full Library Skeleton /',
|
||||
'Full Skeleton /',
|
||||
testMeteorSkeleton({
|
||||
skeletonName: 'full',
|
||||
port: 3204,
|
||||
@@ -150,7 +150,7 @@ describe('Meteor Skeletons /', () => {
|
||||
);
|
||||
|
||||
describe(
|
||||
'Tailwind Library Skeleton /',
|
||||
'Tailwind Skeleton /',
|
||||
testMeteorSkeleton({
|
||||
skeletonName: 'tailwind',
|
||||
port: 3208,
|
||||
|
||||
@@ -130,7 +130,16 @@ export default class Matcher {
|
||||
}
|
||||
|
||||
matchEmpty() {
|
||||
if (this.buf.length > 0) {
|
||||
if (this.buf.length === 0) return;
|
||||
|
||||
// Strip Node.js runtime warning lines before checking for unexpected output.
|
||||
// These originate from third-party packages (e.g. http-proxy using the
|
||||
// deprecated url.parse() API) and should not cause test failures.
|
||||
// Pattern covers: "(node:NNNN) Warning: ...\n(Use `node --trace-warnings ...`)\n"
|
||||
const nodeWarningRe = /\(node:\d+\) \w+: [^\n]+\n(?:\(Use [^\n]+\)\n)?/g;
|
||||
const stripped = this.buf.replace(nodeWarningRe, '');
|
||||
|
||||
if (stripped.length > 0) {
|
||||
Console.info("Extra junk is :", this.buf);
|
||||
throw new TestFailure('junk-at-end', { run: this.run });
|
||||
}
|
||||
|
||||
@@ -1026,12 +1026,173 @@ be called.
|
||||
To customize the contents of the email, see
|
||||
[`Accounts.emailTemplates`](#Accounts-emailTemplates).
|
||||
|
||||
## Email Link Callbacks and URL Customization
|
||||
|
||||
When Meteor sends account-related emails, those emails contain URLs that users click
|
||||
to complete actions like password reset. This section explains how these URLs work
|
||||
and how to customize them.
|
||||
|
||||
### How Email URLs Work
|
||||
|
||||
By default, Meteor generates URLs using hash fragments:
|
||||
|
||||
- `https://yourapp.com/#/reset-password/TOKEN`
|
||||
- `https://yourapp.com/#/verify-email/TOKEN`
|
||||
- `https://yourapp.com/#/enroll-account/TOKEN`
|
||||
|
||||
**Security Note:** Hash fragments (the part after `#`) are intentionally used because
|
||||
they are never sent to the server in HTTP requests. This prevents sensitive tokens
|
||||
from appearing in server logs, proxy logs, or HTTP referrer headers.
|
||||
|
||||
When a user clicks these links, Meteor's client-side code automatically parses
|
||||
`window.location.hash` and triggers the appropriate callback registered with
|
||||
the functions below.
|
||||
|
||||
<ApiBox name="Accounts.onResetPasswordLink" />
|
||||
|
||||
<ApiBox name="Accounts.onEnrollmentLink" />
|
||||
|
||||
<ApiBox name="Accounts.onEmailVerificationLink" />
|
||||
|
||||
### Complete Example: Custom Password Reset Flow
|
||||
|
||||
Here's how to implement password reset without `accounts-ui`:
|
||||
|
||||
```js
|
||||
// client/accounts-hooks.js
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
// Register at top level, NOT inside Meteor.startup()
|
||||
let doneCallback;
|
||||
|
||||
Accounts.onResetPasswordLink((token, done) => {
|
||||
// Store token and done callback for your UI
|
||||
Session.set('resetPasswordToken', token);
|
||||
doneCallback = done;
|
||||
|
||||
// Show your password reset form
|
||||
// The login process is suspended until done() is called
|
||||
});
|
||||
|
||||
// In your password reset form submit handler:
|
||||
function submitNewPassword(newPassword) {
|
||||
const token = Session.get('resetPasswordToken');
|
||||
|
||||
Accounts.resetPassword(token, newPassword, (error) => {
|
||||
if (error) {
|
||||
alert('Reset failed: ' + error.reason);
|
||||
} else {
|
||||
Session.set('resetPasswordToken', null);
|
||||
doneCallback(); // Re-enables auto-login
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing Email URLs
|
||||
|
||||
<ApiBox name="Accounts.urls" />
|
||||
|
||||
`Accounts.urls` is a server-side object containing functions that generate URLs
|
||||
for account emails. Override these to customize the URL format.
|
||||
|
||||
| Property | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `resetPassword` | `(token, extraParams?) => string` | Password reset URL |
|
||||
| `verifyEmail` | `(token, extraParams?) => string` | Email verification URL |
|
||||
| `enrollAccount` | `(token, extraParams?) => string` | Account enrollment URL |
|
||||
| `loginToken` | `(selector, token, extraParams?) => string` | Login token URL |
|
||||
|
||||
#### Async URL Generation
|
||||
|
||||
The URL methods can also return **Promises** that resolve to strings. This is useful when
|
||||
URL generation requires asynchronous operations, such as:
|
||||
- Looking up user data from the database
|
||||
- Calling external services (e.g., URL shorteners)
|
||||
- Generating signed URLs from cloud providers
|
||||
|
||||
The email-sending functions (`Accounts.sendResetPasswordEmail`, `Accounts.sendEnrollmentEmail`,
|
||||
and `Accounts.sendVerificationEmail`) handle both synchronous and asynchronous URL methods
|
||||
transparently.
|
||||
|
||||
**Example: Async URL with database lookup**
|
||||
|
||||
```js
|
||||
// Server-side
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Accounts.urls.resetPassword = async (token, extraParams) => {
|
||||
// Example: Look up user preference for custom domain
|
||||
const user = await Meteor.users.findOneAsync({ 'services.password.reset.token': token });
|
||||
const domain = user?.profile?.preferredDomain || Meteor.absoluteUrl();
|
||||
|
||||
return `${domain}reset-password/${token}`;
|
||||
};
|
||||
```
|
||||
|
||||
**Example: Using a URL shortener service**
|
||||
|
||||
```js
|
||||
// Server-side
|
||||
Accounts.urls.verifyEmail = async (token) => {
|
||||
const longUrl = Meteor.absoluteUrl(`verify-email/${token}`);
|
||||
|
||||
// Shorten the URL using an external service
|
||||
const shortUrl = await shortenUrl(longUrl);
|
||||
return shortUrl;
|
||||
};
|
||||
```
|
||||
|
||||
**Example: Using Clean URLs Instead of Hash Fragments**
|
||||
|
||||
If your router doesn't handle hash fragments well, you can override `Accounts.urls`
|
||||
to use clean URLs:
|
||||
|
||||
```js
|
||||
// Server-side
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Accounts.urls.resetPassword = (token) => {
|
||||
return Meteor.absoluteUrl(`reset-password/${token}`);
|
||||
};
|
||||
|
||||
Accounts.urls.verifyEmail = (token) => {
|
||||
return Meteor.absoluteUrl(`verify-email/${token}`);
|
||||
};
|
||||
|
||||
Accounts.urls.enrollAccount = (token) => {
|
||||
return Meteor.absoluteUrl(`enroll-account/${token}`);
|
||||
};
|
||||
```
|
||||
|
||||
**Important:** When using clean URLs (without `#/`), the built-in
|
||||
`Accounts.onResetPasswordLink`, `Accounts.onEnrollmentLink`, and
|
||||
`Accounts.onEmailVerificationLink` callbacks won't work automatically.
|
||||
Handle tokens in your router instead:
|
||||
|
||||
```js
|
||||
// Example with a router
|
||||
Router.route('/reset-password/:token', function() {
|
||||
const token = this.params.token;
|
||||
// Show password reset UI, call Accounts.resetPassword(token, newPassword)
|
||||
});
|
||||
```
|
||||
|
||||
### Router Integration
|
||||
|
||||
You have three options when integrating with client-side routers:
|
||||
|
||||
1. **Keep default hash URLs** - Works out of the box
|
||||
with `Accounts.on*Link` callbacks. No router configuration needed.
|
||||
|
||||
2. **Override `Accounts.urls` for clean URLs** - More "modern" looking URLs,
|
||||
but requires handling tokens in your router.
|
||||
|
||||
3. **Use hashbang mode** - Some routers support `#!/` routes. Configure your
|
||||
router accordingly and update `Accounts.urls` to use `#!/` instead of `#/`.
|
||||
|
||||
<ApiBox name="Accounts.emailTemplates" />
|
||||
|
||||
This is an `Object` with several fields that are used to generate text/html
|
||||
|
||||
Reference in New Issue
Block a user