0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 07:43:01 +00:00

Compare commits

...

69 Commits

Author SHA1 Message Date
dependabot[bot]
a0dc23385d Bump codemirror from 5.65.17 to 6.0.2
Bumps [codemirror](https://github.com/codemirror/basic-setup) from 5.65.17 to 6.0.2.
- [Changelog](https://github.com/codemirror/basic-setup/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/basic-setup/commits/6.0.2)

---
updated-dependencies:
- dependency-name: codemirror
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 15:20:33 +00:00
Trevor Buckner
5ae01862e5 Merge pull request #4339 from naturalcrit/dependabot/npm_and_yarn/prod-dependencies-b017ff4ab1
Bump the prod-dependencies group across 1 directory with 2 updates
2025-07-15 11:17:59 -04:00
Trevor Buckner
398df7a061 Merge branch 'master' into dependabot/npm_and_yarn/prod-dependencies-b017ff4ab1 2025-07-15 11:15:54 -04:00
Trevor Buckner
443b0f6a37 Merge pull request #4320 from G-Ambatte/experimentalIDValidations
Brew ID validations
2025-07-15 11:14:45 -04:00
Trevor Buckner
544175b994 Merge branch 'master' into experimentalIDValidations 2025-07-15 11:14:11 -04:00
Trevor Buckner
955602e7ee Merge pull request #4333 from G-Ambatte/addForceSSLTests
Add forceSSL middleware tests
2025-07-15 11:11:33 -04:00
G.Ambatte
90e577dd3f Rework tests 2025-07-15 09:02:57 +12:00
G.Ambatte
828208aadb Add more ID validation test cases 2025-07-15 08:19:05 +12:00
G.Ambatte
973e071e93 Slightly loosen Google ID match criteria, add comments 2025-07-15 08:13:35 +12:00
dependabot[bot]
f9e7aa355d Bump the prod-dependencies group across 1 directory with 2 updates
Bumps the prod-dependencies group with 2 updates in the / directory: [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) and [mongoose](https://github.com/Automattic/mongoose).


Updates `core-js` from 3.43.0 to 3.44.0
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.44.0/packages/core-js)

Updates `mongoose` from 8.16.1 to 8.16.3
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.16.1...8.16.3)

---
updated-dependencies:
- dependency-name: core-js
  dependency-version: 3.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-dependencies
- dependency-name: mongoose
  dependency-version: 8.16.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 20:09:17 +00:00
Trevor Buckner
24dfd41714 Merge branch 'master' into experimentalIDValidations 2025-07-14 13:37:19 -04:00
Trevor Buckner
638e54535d Merge branch 'master' into addForceSSLTests 2025-07-14 13:16:14 -04:00
Trevor Buckner
cbc6956221 Merge pull request #4334 from G-Ambatte/addTokenTests
Add token.js tests
2025-07-14 13:15:43 -04:00
Trevor Buckner
248d2038ec Cleanup token.js 2025-07-14 13:10:19 -04:00
Trevor Buckner
5b66175b8c Merge pull request #4337 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-3db4bbab60
Bump the dev-dependencies group across 1 directory with 2 updates
2025-07-14 12:59:01 -04:00
Trevor Buckner
552aa7d41a Merge branch 'master' into dependabot/npm_and_yarn/dev-dependencies-3db4bbab60 2025-07-14 12:53:33 -04:00
Trevor Buckner
b0a108b543 Merge pull request #4338 from G-Ambatte/addSafeHTMLTest
Increase safeHTML testing to 100% coverage
2025-07-14 12:53:12 -04:00
G.Ambatte
505d2840c0 Merge branch 'master' into addSafeHTMLTest 2025-07-14 21:26:22 +12:00
G.Ambatte
41ff50fefe Add missing test 2025-07-14 21:23:38 +12:00
G.Ambatte
2fbcc84a50 Merge branch 'master' into experimentalIDValidations 2025-07-14 14:50:05 +12:00
G.Ambatte
45e4d27c0a Merge branch 'master' into addForceSSLTests 2025-07-14 14:46:10 +12:00
G.Ambatte
77bf3ffc6f Merge branch 'master' into addTokenTests 2025-07-14 14:46:07 +12:00
dependabot[bot]
bc045ec6c9 Bump the dev-dependencies group across 1 directory with 2 updates
Bumps the dev-dependencies group with 2 updates in the / directory: [eslint](https://github.com/eslint/eslint) and [supertest](https://github.com/ladjs/supertest).


Updates `eslint` from 9.30.1 to 9.31.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.30.1...v9.31.0)

Updates `supertest` from 7.1.1 to 7.1.3
- [Release notes](https://github.com/ladjs/supertest/releases)
- [Commits](https://github.com/ladjs/supertest/compare/v7.1.1...v7.1.3)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.31.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: supertest
  dependency-version: 7.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 00:02:03 +00:00
Trevor Buckner
6390ea076a Merge pull request #4330 from naturalcrit/CompressSaveDataUpload
Gzip brew object when sending for save update
2025-07-13 20:00:21 -04:00
Trevor Buckner
6affcb587d Merge branch 'master' into CompressSaveDataUpload 2025-07-13 19:57:10 -04:00
Trevor Buckner
7787afabff Merge pull request #4336 from naturalcrit/fixVersionTest
Fix test option 2 - Add text property to version check test object
2025-07-13 19:55:16 -04:00
Trevor Buckner
fb4a8e5cf1 Merge branch 'CompressSaveDataUpload' of https://github.com/naturalcrit/homebrewery into CompressSaveDataUpload 2025-07-13 19:38:10 -04:00
Trevor Buckner
8432a6e367 cleanup 2025-07-13 19:38:08 -04:00
G.Ambatte
90ee08de42 Add text property to test object 2025-07-14 11:06:35 +12:00
G.Ambatte
40839b18e4 Add tests for token.js 2025-07-14 00:14:58 +12:00
G.Ambatte
677c02cfa5 Add forceSSL tests 2025-07-13 22:36:53 +12:00
G.Ambatte
a7a8803e9d Merge branch 'master' into experimentalIDValidations 2025-07-13 20:52:52 +12:00
Trevor Buckner
5fbc111db7 Merge branch 'master' into CompressSaveDataUpload 2025-07-13 00:55:59 -04:00
Trevor Buckner
5edea7d0f4 Turns out body-parser automatically inflates gzip. Can remove. 2025-07-13 00:55:16 -04:00
Trevor Buckner
d3a9d813c9 Log brew compression size just for testing purposes 2025-07-13 00:54:51 -04:00
Trevor Buckner
fc475b2a7e Allow babel to transpile fflate 2025-07-13 00:52:06 -04:00
Trevor Buckner
76b76b3bb6 Merge pull request #4286 from dbolack-ab/snippets-save-history-too
Add brew snippets to local save history
2025-07-11 13:32:13 -04:00
Trevor Buckner
22ef3cbebc Gzip brew object when sending for save update 2025-07-11 16:55:30 +00:00
Trevor Buckner
9da8a17053 Remove text mismatch logs 2025-07-10 17:17:25 -04:00
Trevor Buckner
7cadbfbd7b allowExceedingIndices for our patch applier
Test if it allows patches to go through, and log error if it doesn't match the expected output.
2025-07-10 17:11:31 -04:00
Trevor Buckner
98b9e86787 Merge pull request #4329 from naturalcrit/AdditionalPatchLogging
On patch failure, compare client and server text bytewise
2025-07-10 12:05:24 -04:00
Trevor Buckner
489b4b2694 Also log differences on MD5 mismatch 2025-07-10 12:04:09 -04:00
Trevor Buckner
8d279260c2 Merge branch 'master' into AdditionalPatchLogging 2025-07-10 11:19:07 -04:00
Trevor Buckner
7c08c430d0 Merge pull request #4324 from G-Ambatte/experimentalSplitHashAndVersionChecks
Split hash and version checks in updateBrew function
2025-07-10 11:18:53 -04:00
Trevor Buckner
45689d119e Comment out one failing test
Patch failure test is no longer being thrown while we monitor in the background
2025-07-10 11:18:39 -04:00
Trevor Buckner
c5805af935 On patch failure, compare client and server text bytewise 2025-07-10 11:12:42 -04:00
Trevor Buckner
b2c4bb7082 Merge branch 'master' into experimentalSplitHashAndVersionChecks 2025-07-10 11:06:29 -04:00
Trevor Buckner
68460447dc Merge pull request #4327 from dbolack-ab/fallbackTest
Run patch processing in parallel to prior system
2025-07-10 09:50:43 -04:00
Trevor Buckner
440c7beff6 Up to 3.19.3 so users can get the update 2025-07-10 09:47:21 -04:00
David Bolack
c7610cf0f8 Run patch processing in parallel to prior system to attempt to narrow down not-quite-so-edge cases that did not come up prior to user testing. 2025-07-10 07:10:13 -05:00
G.Ambatte
7f3a818558 Add Homebrew API coverage tests 2025-07-10 23:25:57 +12:00
G.Ambatte
bc82afa5b2 Split version from hash checks 2025-07-10 21:42:51 +12:00
G.Ambatte
abef250631 Update ID validation check 2025-07-10 20:58:46 +12:00
G.Ambatte
1794e96d50 Update tests 2025-07-10 20:46:01 +12:00
G.Ambatte
25f25da499 Adjust validation regex for IDs 2025-07-10 20:39:12 +12:00
G.Ambatte
aa15bdaacb Initial pass at ID validations 2025-07-10 19:59:09 +12:00
Trevor Buckner
7ba7991631 Additional diff server error logging 2025-07-10 00:37:03 -04:00
Trevor Buckner
0e1ac26999 v3.19.2 2025-07-09 22:42:21 -04:00
Trevor Buckner
f49fed8c35 Merge pull request #4311 from dbolack-ab/md5Fix
Normalize strings before running MD5s
2025-07-09 22:40:15 -04:00
Trevor Buckner
a8236fbab4 Ok. I'm lowering the coverage threshold 2025-07-09 22:14:24 -04:00
Trevor Buckner
daf4eceedd Small rearrangement 2025-07-09 22:09:16 -04:00
David Bolack
a02361ee65 Move normalization to before diffing 2025-07-09 19:37:57 -05:00
David Bolack
81e20f032e NOrmalize strings before rnuning MD5s 2025-07-09 18:52:45 -05:00
Trevor Buckner
1d92b98568 Merge pull request #4310 from dbolack-ab/2025-06-09_Debug
Add Patch wrapper/unwrapper for saves
2025-07-09 18:42:23 -04:00
David Bolack
0f4157d084 Add Patch wrapper/unwrapper for saves
Object encapsulation for the win?
2025-07-09 17:16:47 -05:00
Trevor Buckner
4dcc3749d8 Merge branch 'master' into snippets-save-history-too 2025-07-09 13:04:08 -04:00
Trevor Buckner
8f058d56f2 Merge pull request #4304 from naturalcrit/3.19.1
up To V3.19.1
2025-07-09 11:11:59 -04:00
David Bolack
99b90e0998 Include snippets in the restoration. 2025-07-07 13:54:29 -05:00
David Bolack
1008321957 Add brew snippets to local save history
solves #3113
2025-06-30 12:17:46 -05:00
14 changed files with 613 additions and 78 deletions

View File

@@ -88,6 +88,21 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Wednesday 7/09/2025 - v3.19.3
{{taskList
##### calculuschild
* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background
}}
### Wednesday 7/09/2025 - v3.19.2
{{taskList
##### calculuschild
* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
}}
### Wednesday 7/09/2025 - v3.19.1
{{taskList

View File

@@ -5,6 +5,7 @@ const _ = require('lodash');
const createClass = require('create-react-class');
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags');
@@ -190,8 +191,9 @@ const EditPage = createClass({
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style,
text : newData.text
style : newData.style,
text : newData.text,
snippets : newData.snippets
}
}));
},
@@ -247,6 +249,9 @@ const EditPage = createClass({
save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
const brewState = this.state.brew; // freeze the current state
const preSaveSnapshot = { ...brewState };
this.setState((prevState)=>({
isSaving : true,
error : null,
@@ -256,21 +261,25 @@ const EditPage = createClass({
await updateHistory(this.state.brew).catch(console.error);
await versionHistoryGarbageCollection().catch(console.error);
const preSaveSnapshot = { ...this.state.brew }
//Prepare content to send to server
const brew = { ...this.state.brew };
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
brew.patches = makePatches(this.savedBrew.text, brew.text);
brew.hash = await md5(this.savedBrew.text);
brew.text = undefined;
brew.textBin = undefined;
const brew = { ...brewState };
brew.text = brew.text.normalize('NFC');
this.savedBrew.text = this.savedBrew.text.normalize('NFC');
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
brew.patches = stringifyPatches(makePatches(this.savedBrew.text, brew.text));
brew.hash = await md5(this.savedBrew.text);
//brew.text = undefined; - Temporary parallel path
brew.textBin = undefined;
const compressedBrew = gzipSync(strToU8(JSON.stringify(brew)));
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
const res = await request
.put(`/api/update/${brew.editId}${params}`)
.send(brew)
.set('Content-Encoding', 'gzip')
.set('Content-Type', 'application/json')
.send(compressedBrew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ error: err });
@@ -293,8 +302,8 @@ const EditPage = createClass({
shareId : res.body.shareId,
version : res.body.version
},
isSaving : false,
unsavedTime : new Date()
isSaving : false,
unsavedTime : new Date()
}), ()=>{
this.setState({ unsavedChanges : this.hasChanges() });
});

View File

@@ -176,6 +176,26 @@ const errorIndex = (props)=>{
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
// ID validation error
'11' : dedent`
## No Homebrewery document could be found.
The server could not locate the Homebrewery document. The Brew ID failed the validation check.
:
**Brew ID:** ${props.brew.brewId}`,
// Google ID validation error
'12' : dedent`
## No Google document could be found.
The server could not locate the Google document. The Google ID failed the validation check.
:
**Brew ID:** ${props.brew.brewId}`,
//account page when account is not defined
'50' : dedent`
## You are not signed in

View File

@@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) {
title : brew.title,
text : brew.text,
style : brew.style,
snippets : brew.snippets,
version : brew.version,
shareId : brew.shareId,
savedAt : brew?.savedAt || new Date(),

232
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "homebrewery",
"version": "3.19.1",
"version": "3.19.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "homebrewery",
"version": "3.19.1",
"version": "3.19.3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -15,13 +15,14 @@
"@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.27.6",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^13.0.1",
"@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"codemirror": "^6.0.2",
"cookie-parser": "^1.4.7",
"core-js": "^3.43.0",
"core-js": "^3.44.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
@@ -29,6 +30,7 @@
"express": "^5.1.0",
"express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0",
"fflate": "^0.8.2",
"fs-extra": "11.3.0",
"hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2",
@@ -47,7 +49,7 @@
"marked-subsuper-text": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.16.1",
"mongoose": "^8.16.3",
"nanoid": "5.1.5",
"nconf": "^0.13.0",
"react": "^18.3.1",
@@ -63,7 +65,7 @@
"devDependencies": {
"@stylistic/stylelint-plugin": "^3.1.3",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.30.1",
"eslint": "^9.31.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
@@ -74,7 +76,7 @@
"stylelint": "^16.21.1",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.1"
"supertest": "^7.1.3"
},
"engines": {
"node": "^20.18.x",
@@ -1824,6 +1826,87 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
@@ -1888,6 +1971,12 @@
"@csstools/css-tokenizer": "^3.0.1"
}
},
"node_modules/@dmsnell/diff-match-patch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@dmsnell/diff-match-patch/-/diff-match-patch-1.1.0.tgz",
"integrity": "sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==",
"license": "Apache-2.0"
},
"node_modules/@dual-bundle/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@@ -2050,10 +2139,11 @@
}
},
"node_modules/@eslint/js": {
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
"integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -2796,6 +2886,36 @@
"ieee754": "^1.2.1"
}
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
@@ -4829,10 +4949,19 @@
}
},
"node_modules/codemirror": {
"version": "5.65.17",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.17.tgz",
"integrity": "sha512-1zOsUx3lzAOu/gnMAZkQ9kpIHcPYOc9y1Fbm2UVk5UBPkdq380nhkelG0qUwm1f7wPvTbndu9ZYlug35EwAZRQ==",
"license": "MIT"
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
@@ -5039,10 +5168,11 @@
}
},
"node_modules/core-js": {
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz",
"integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==",
"version": "3.44.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz",
"integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
@@ -5159,6 +5289,12 @@
"object-assign": "^4.1.1"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -5952,18 +6088,19 @@
}
},
"node_modules/eslint": {
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
"version": "9.31.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
"integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.14.0",
"@eslint/core": "^0.15.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.30.1",
"@eslint/js": "9.31.0",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -6129,6 +6266,19 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@eslint/core": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -6643,6 +6793,11 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -10161,9 +10316,10 @@
}
},
"node_modules/mongoose": {
"version": "8.16.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.1.tgz",
"integrity": "sha512-Q+0TC+KLdY4SYE+u9gk9pdW1tWu/pl0jusyEkMGTgBoAbvwQdfy4f9IM8dmvCwb/blSfp7IfLkob7v76x6ZGpQ==",
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.3.tgz",
"integrity": "sha512-p2JOsRQG7j0vXhLpsWw5Slm2VnDeJK8sRyqSyegk5jQujuP9BTOZ1Di9VX/0lYfBhZ2DpAExi51QTd4pIqSgig==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.4",
"kareem": "2.6.3",
@@ -13054,6 +13210,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/style-search": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz",
@@ -13289,9 +13451,9 @@
}
},
"node_modules/superagent": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz",
"integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==",
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.2.tgz",
"integrity": "sha512-vWMq11OwWCC84pQaFPzF/VO3BrjkCeewuvJgt1jfV0499Z1QSAWN4EqfMM5WlFDDX9/oP8JjlDKpblrmEoyu4Q==",
"license": "MIT",
"dependencies": {
"component-emitter": "^1.3.0",
@@ -13321,14 +13483,14 @@
}
},
"node_modules/supertest": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz",
"integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.3.tgz",
"integrity": "sha512-ORY0gPa6ojmg/C74P/bDoS21WL6FMXq5I8mawkEz30/zkwdu0gOeqstFy316vHG6OKxqQ+IbGneRemHI8WraEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"methods": "^1.1.2",
"superagent": "^10.2.1"
"superagent": "^10.2.2"
},
"engines": {
"node": ">=14.18.0"
@@ -14324,6 +14486,12 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.19.1",
"version": "3.19.3",
"type": "module",
"engines": {
"npm": "^10.8.x",
@@ -72,7 +72,7 @@
"lines": 50
},
"server/homebrew.api.js": {
"statements": 70,
"statements": 60,
"branches": 50,
"functions": 65,
"lines": 70
@@ -88,13 +88,14 @@
"@babel/preset-env": "^7.28.0",
"@babel/preset-react": "^7.27.1",
"@babel/runtime": "^7.27.6",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^13.0.1",
"@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0",
"classnames": "^2.5.1",
"codemirror": "^5.65.6",
"codemirror": "^6.0.2",
"cookie-parser": "^1.4.7",
"core-js": "^3.43.0",
"core-js": "^3.44.0",
"cors": "^2.8.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
@@ -102,6 +103,7 @@
"express": "^5.1.0",
"express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0",
"fflate": "^0.8.2",
"fs-extra": "11.3.0",
"hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2",
@@ -120,7 +122,7 @@
"marked-subsuper-text": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.16.1",
"mongoose": "^8.16.3",
"nanoid": "5.1.5",
"nconf": "^0.13.0",
"react": "^18.3.1",
@@ -136,7 +138,7 @@
"devDependencies": {
"@stylistic/stylelint-plugin": "^3.1.3",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.30.1",
"eslint": "^9.31.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
@@ -147,6 +149,6 @@
"stylelint": "^16.21.1",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.1"
"supertest": "^7.1.3"
}
}

View File

@@ -28,6 +28,7 @@
"codemirror/addon/hint/show-hint.js",
"moment",
"superagent",
"@sanity/diff-match-patch"
"@sanity/diff-match-patch",
"fflate"
]
}

View File

@@ -0,0 +1,66 @@
import forceSSL from './forcessl.mw';
describe('Tests for ForceSSL middleware', ()=>{
let originalEnv;
let nextFn;
let req = {};
let res = {};
beforeEach(()=>{
originalEnv = process.env.NODE_ENV;
nextFn = jest.fn();
req = {
header : ()=>{ return 'http'; },
get : ()=>{ return 'test'; },
url : 'URL'
};
res = {
redirect : jest.fn()
};
});
afterEach(()=>{
process.env.NODE_ENV = originalEnv;
jest.clearAllMocks();
});
it('should not redirect when NODE_ENV is set to local', ()=>{
process.env.NODE_ENV = 'local';
forceSSL(null, null, nextFn);
expect(res.redirect).not.toHaveBeenCalled();
expect(nextFn).toHaveBeenCalled();
});
it('should not redirect when NODE_ENV is set to docker', ()=>{
process.env.NODE_ENV = 'docker';
forceSSL(null, null, nextFn);
expect(res.redirect).not.toHaveBeenCalled();
expect(nextFn).toHaveBeenCalled();
});
it('should redirect with 302 when header is not HTTPS and NODE_ENV is not local or docker', ()=>{
process.env.NODE_ENV = 'test';
forceSSL(req, res, nextFn);
expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL');
expect(nextFn).not.toHaveBeenCalled();
});
it('should not redirect when header is HTTPS and NODE_ENV is not local or docker', ()=>{
process.env.NODE_ENV = 'test';
req.header = ()=>{ return 'https'; };
forceSSL(req, res, nextFn);
expect(res.redirect).not.toHaveBeenCalled();
expect(nextFn).toHaveBeenCalled();
});
});

View File

@@ -11,7 +11,7 @@ import { nanoid } from 'nanoid';
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata,
brewSnippetsToJSON } from '../shared/helpers.js';
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js';
@@ -48,6 +48,20 @@ const api = {
}
id = id.slice(googleId.length);
}
// ID Validation Checks
// Homebrewery ID
// Typically 12 characters, but the DB shows a range of 7 to 14 characters
if(!id.match(/^[A-Za-z0-9_-]{7,14}$/)){
throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id };
}
// Google ID
// Typically 33 characters, old format is 44 - always starts with a 1
// Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable
if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32,43})$/)){
throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id };
}
return { id, googleId };
},
//Get array of any of this user's brews tagged with `meta:theme`
@@ -341,22 +355,44 @@ const api = {
const brewFromServer = req.brew;
splitTextStyleAndMetadata(brewFromServer);
if(brewFromServer?.version !== brewFromClient?.version){
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
res.setHeader('Content-Type', 'application/json');
return res.status(409).send(JSON.stringify({ message: `The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
}
brewFromServer.text = brewFromServer.text.normalize('NFC');
brewFromServer.hash = await md5(brewFromServer.text);
if((brewFromServer?.version !== brewFromClient?.version) || (brewFromServer?.hash !== brewFromClient?.hash)) {
if(brewFromClient?.version !== brewFromClient?.version)
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
if(brewFromServer?.hash !== brewFromClient?.hash) {
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
}
if(brewFromServer?.hash !== brewFromClient?.hash) {
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
res.setHeader('Content-Type', 'application/json');
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
}
try {
const patches = parsePatch(brewFromClient.patches);
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = applyPatches(patches, brewFromServer.text, { allowExceedingIndices: true })[0];
if(patchedResult != brewFromClient.text)
throw("Patches did not apply cleanly, text mismatch detected");
// brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', {
patches : brewFromClient.patches,
brewId : brewFromClient.editId || 'unknown',
error : err
});
// While running in parallel, don't throw the error upstream.
// throw err; // rethrow to preserve the 500 behavior
}
let brew = _.assign(brewFromServer, brewFromClient);
brew.title = brew.title.trim();
brew.description = brew.description.trim() || '';
brew.text = applyPatches(brewFromClient.patches, brewFromServer.text)[0];
brew.text = api.mergeBrewText(brew);
const googleId = brew.googleId;

View File

@@ -99,18 +99,87 @@ describe('Tests for api', ()=>{
expect(googleId).toBeUndefined();
});
it('should throw if id is too short', ()=>{
let err;
try {
api.getId({
params : {
id : 'abcd'
}
});
} catch (e) {
err = e;
};
expect(err).toEqual({ HBErrorCode: '11', brewId: 'abcd', message: 'Invalid ID', name: 'ID Error', status: 404 });
});
it('should return id and google id from request body', ()=>{
const { id, googleId } = api.getId({
params : {
id : 'abcdefgh'
id : 'abcdefghijkl'
},
body : {
googleId : '12345'
googleId : '123456789012345678901234567890123'
}
});
expect(id).toEqual('abcdefgh');
expect(googleId).toEqual('12345');
expect(id).toEqual('abcdefghijkl');
expect(googleId).toEqual('123456789012345678901234567890123');
});
it('should throw invalid - google id right length but does not match pattern', ()=>{
let err;
try {
api.getId({
params : {
id : 'abcdefghijkl'
},
body : {
googleId : '012345678901234567890123456789012'
}
});
} catch (e) {
err = e;
}
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
});
it('should throw invalid - google id too short (32 char)', ()=>{
let err;
try {
api.getId({
params : {
id : 'abcdefghijkl'
},
body : {
googleId : '12345678901234567890123456789012'
}
});
} catch (e) {
err = e;
}
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
});
it('should throw invalid - google id too long (45 char)', ()=>{
let err;
try {
api.getId({
params : {
id : 'abcdefghijkl'
},
body : {
googleId : '123456789012345678901234567890123456789012345'
}
});
} catch (e) {
err = e;
}
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
});
it('should return 12-char id and google id from params', ()=>{
@@ -1052,4 +1121,83 @@ brew`);
expect(testBrew.tags).toEqual(['tag a']);
});
});
describe('updateBrew', ()=>{
it('should return error on version mismatch', async ()=>{
const brewFromClient = { version: 1 };
const brewFromServer = { version: 1000, text: '' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
it('should return error on hash mismatch', async ()=>{
const brewFromClient = { version: 1, hash: '1234' };
const brewFromServer = { version: 1, text: 'test' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
// Commenting this one out for now, since we are no longer throwing this error while we monitor
// it('should return error on applying patches', async ()=>{
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
// const req = {
// brew : brewFromServer,
// body : brewFromClient,
// };
// let err;
// try {
// await api.updateBrew(req, res);
// } catch (e) {
// err = e;
// }
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
// });
it('should save brew, no ID', async ()=>{
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
model.save = jest.fn((brew)=>{return brew;});
const req = {
brew : brewFromServer,
body : brewFromClient,
query : { saveToGoogle: false, removeFromGoogle: false }
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({
_id : '1',
description : 'Test Description',
hash : '098f6bcd4621d373cade4e832627b4f6',
title : 'Test Title',
version : 2
})
);
});
});
});

View File

@@ -5,21 +5,16 @@ import config from './config.js';
const generateAccessToken = (account)=>{
const payload = account;
// When the token was issued
payload.issued = (new Date());
// Which service issued the Token
payload.issuer = config.get('authentication_token_issuer');
// Which service is the token intended for
payload.audience = config.get('authentication_token_audience');
// The signing key for signing the token
payload.issued = (new Date()); // When the token was issued
payload.issuer = config.get('authentication_token_issuer'); // Which service issued the Token
payload.audience = config.get('authentication_token_audience'); // Which service is the token intended for
const secret = config.get('authentication_token_secret'); // The signing key for signing the token
delete payload.password;
delete payload._id;
const secret = config.get('authentication_token_secret');
const token = jwt.encode(payload, secret);
return token;
};
export default generateAccessToken;
export default generateAccessToken;

27
server/token.spec.js Normal file
View File

@@ -0,0 +1,27 @@
import { expect, jest } from '@jest/globals';
import config from './config.js';
import generateAccessToken from './token';
describe('Tests for Token', ()=>{
it('Get token', ()=>{
// Mock the Config module, so we aren't grabbing actual secrets for testing
jest.mock('./config.js');
config.get = jest.fn((param)=>{
// The requested key name will be reflected to the output
return param;
});
const account = {};
const token = generateAccessToken(account);
// If these tests fail, the config mock has failed
expect(account).toHaveProperty('issuer', 'authentication_token_issuer');
expect(account).toHaveProperty('audience', 'authentication_token_audience');
// Because the inputs are fixed, this JWT key should be static
expect(typeof token).toBe('string');
});
});

View File

@@ -139,9 +139,45 @@ const fetchThemeBundle = async (obj, renderer, theme)=>{
}));
};
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
const clientText = clientTextRaw?.normalize('NFC') || '';
const serverText = serverTextRaw?.normalize('NFC') || '';
const clientBuffer = Buffer.from(clientText, 'utf8');
const serverBuffer = Buffer.from(serverText, 'utf8');
if (clientBuffer.equals(serverBuffer)) {
console.log(`${label} text matches byte-for-byte.`);
return;
}
console.warn(`${label} text mismatch detected.`);
console.log(`Client length: ${clientBuffer.length}`);
console.log(`Server length: ${serverBuffer.length}`);
// Byte-level diff
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
if (clientBuffer[i] !== serverBuffer[i]) {
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
break;
}
}
// Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
if (clientText[i] !== serverText[i]) {
console.log(`Char mismatch at index ${i}:`);
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
break;
}
}
}
export {
splitTextStyleAndMetadata,
printCurrentBrew,
fetchThemeBundle,
brewSnippetsToJSON
brewSnippetsToJSON,
debugTextMismatch
};

View File

@@ -4,6 +4,17 @@ require('jsdom-global')();
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
test('Exit if no document', function() {
const doc = document;
document = undefined;
const result = safeHTML('');
document = doc;
expect(result).toBe(null);
});
test('Javascript via href', function() {
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
const rendered = safeHTML(source);