16 Commits

Author SHA1 Message Date
c11d956ced Merge pull request #66 from lordmathis/fix/head-request
Handle HEAD requests with static router
2025-10-23 20:35:47 +02:00
9a232819a8 Handle HEAD requests with static router 2025-10-23 20:26:34 +02:00
f9ce8b9e9f Merge pull request #65 from lordmathis/fix/code-highlight
Fix code highlight theme change
2025-10-23 19:24:14 +02:00
a6d2663a7d Add type declarations for CSS imports with ?inline modifier 2025-10-23 19:19:27 +02:00
071e99f4da Remove documentation.md 2025-10-23 19:17:51 +02:00
b13ee987c7 Fix code highlight theme change 2025-10-23 19:12:09 +02:00
543dbe6ffe Merge pull request #64 from lordmathis/fix/file-rename
Fix move file on frontend
2025-10-22 21:55:52 +02:00
d0842c515f Change move file endpoint from PUT to POST and add integration tests for file moving and renaming 2025-10-22 21:50:37 +02:00
01d9a984fc Merge pull request #63 from lordmathis/dependabot/npm_and_yarn/app/npm_and_yarn-7372c7abf2
Bump vite from 6.3.6 to 6.4.1 in /app in the npm_and_yarn group across 1 directory
2025-10-21 10:21:46 +02:00
dependabot[bot]
a3975c9acd Bump vite in /app in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the /app directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.6 to 6.4.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 05:07:05 +00:00
e82f25a2ed Merge pull request #62 from lordmathis/fix/image-preview
Fix get image url
2025-10-11 22:56:49 +02:00
ec89f95880 Fix get image url 2025-10-11 22:51:08 +02:00
f101376bef Merge pull request #61 from lordmathis/fix/syntax-highlight
Replace react-syntax-highlighter with rehype-highlight
2025-10-11 22:49:21 +02:00
d582b1a1e9 Add syntax highlighting themes for markdown preview 2025-10-11 22:26:21 +02:00
aca127e52e Move rate limiting for authentication endpoints to the public routes group 2025-10-11 22:16:15 +02:00
9ca8a46093 Replace react-syntax-highlighter with rehype-highlight 2025-10-11 22:02:02 +02:00
13 changed files with 192 additions and 1808 deletions

506
app/package-lock.json generated
View File

@@ -24,9 +24,8 @@
"react": "^18.3.1",
"react-arborist": "^3.4.0",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.6",
"rehype-highlight": "^7.0.2",
"rehype-mathjax": "^6.0.0",
"rehype-prism": "^2.3.3",
"rehype-react": "^8.0.0",
"remark": "^15.0.1",
"remark-math": "^6.0.0",
@@ -43,7 +42,6 @@
"@types/node": "^22.14.0",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.3.4",
@@ -57,7 +55,7 @@
"postcss-simple-vars": "^7.0.1",
"sass": "^1.80.4",
"typescript": "^5.8.2",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vite-plugin-compression2": "^1.3.0",
"vitest": "^3.1.4"
}
@@ -2676,16 +2674,6 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/react-syntax-highlighter": {
"version": "15.5.13",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -3376,12 +3364,6 @@
"require-from-string": "^2.0.2"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -3791,22 +3773,6 @@
"node": ">= 8"
}
},
"node_modules/css-selector-parser": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz",
"integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@@ -4791,19 +4757,6 @@
"reusify": "^1.0.4"
}
},
"node_modules/fault": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
"license": "MIT",
"dependencies": {
"format": "^0.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4917,14 +4870,6 @@
"node": ">= 6"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5270,74 +5215,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-html": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.1.0",
"hast-util-from-parse5": "^8.0.0",
"parse5": "^7.0.0",
"vfile": "^6.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz",
"integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"devlop": "^1.0.0",
"hastscript": "^8.0.0",
"property-information": "^6.0.0",
"vfile": "^6.0.0",
"vfile-location": "^5.0.0",
"web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-parse5/node_modules/hastscript": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
"integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^6.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-is-element": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
@@ -5351,16 +5228,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz",
@@ -5417,86 +5284,15 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^2.0.0",
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.0.0",
"property-information": "^5.0.0",
"space-separated-tokens": "^1.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript/node_modules/@types/hast": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^2"
}
},
"node_modules/hastscript/node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/hastscript/node_modules/comma-separated-tokens": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/hastscript/node_modules/property-information": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/hastscript/node_modules/space-separated-tokens": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
"node": ">=12.0.0"
}
},
"node_modules/highlightjs-vue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
"license": "CC0-1.0"
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -6417,13 +6213,14 @@
"license": "MIT"
},
"node_modules/lowlight": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
"license": "MIT",
"dependencies": {
"fault": "^1.0.0",
"highlight.js": "~10.7.0"
"@types/hast": "^3.0.0",
"devlop": "^1.0.0",
"highlight.js": "~11.11.0"
},
"funding": {
"type": "github",
@@ -7306,18 +7103,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
@@ -7847,15 +7632,6 @@
"license": "MIT",
"peer": true
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -8102,23 +7878,6 @@
}
}
},
"node_modules/react-syntax-highlighter": {
"version": "15.6.6",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz",
"integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0",
"prismjs": "^1.30.0",
"refractor": "^3.6.0"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/react-textarea-autosize": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz",
@@ -8212,122 +7971,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/refractor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
"license": "MIT",
"dependencies": {
"hastscript": "^6.0.0",
"parse-entities": "^2.0.0",
"prismjs": "~1.27.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/character-entities": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/character-entities-legacy": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/character-reference-invalid": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/is-alphanumerical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^1.0.0",
"is-decimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/is-decimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/is-hexadecimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
"license": "MIT",
"dependencies": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/prismjs": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -8355,6 +7998,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rehype-highlight": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
"integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-to-text": "^4.0.0",
"lowlight": "^3.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-mathjax": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-mathjax/-/rehype-mathjax-6.0.0.tgz",
@@ -8436,68 +8096,6 @@
"node": ">=6"
}
},
"node_modules/rehype-parse": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
"integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-from-html": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-prism": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/rehype-prism/-/rehype-prism-2.3.3.tgz",
"integrity": "sha512-J9mhio/CwcJRDyIhsp5hgXmyGeQsFN+/1eNEKnBRxfdJAx2CqH41kV0dqn/k2OgMdjk21IoGFgar0MfVtGYTSg==",
"license": "MIT",
"dependencies": {
"hastscript": "^8.0.0",
"prismjs": "^1.29.0",
"rehype-parse": "^9.0.1",
"unist-util-is": "^6.0.0",
"unist-util-select": "^5.1.0",
"unist-util-visit": "^5.0.0"
},
"peerDependencies": {
"unified": "^10 || ^11"
}
},
"node_modules/rehype-prism/node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-prism/node_modules/hastscript": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
"integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^6.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-react": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz",
@@ -9868,23 +9466,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/unist-util-select/-/unist-util-select-5.1.0.tgz",
"integrity": "sha512-4A5mfokSHG/rNQ4g7gSbdEs+H586xyd24sdJqF1IWamqrLHvYb+DH48fzxowyOhOfK7YSqX+XlCojAyuuyyT2A==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"css-selector-parser": "^3.0.0",
"devlop": "^1.1.0",
"nth-check": "^2.0.0",
"zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@@ -10100,20 +9681,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-location": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
@@ -10129,9 +9696,9 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10679,15 +10246,6 @@
"node": ">=0.1"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -44,9 +44,8 @@
"react": "^18.3.1",
"react-arborist": "^3.4.0",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.6",
"rehype-highlight": "^7.0.2",
"rehype-mathjax": "^6.0.0",
"rehype-prism": "^2.3.3",
"rehype-react": "^8.0.0",
"remark": "^15.0.1",
"remark-math": "^6.0.0",
@@ -63,7 +62,6 @@
"@types/node": "^22.14.0",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.3.4",
@@ -77,7 +75,7 @@
"postcss-simple-vars": "^7.0.1",
"sass": "^1.80.4",
"typescript": "^5.8.2",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vite-plugin-compression2": "^1.3.0",
"vitest": "^3.1.4"
},

View File

@@ -120,6 +120,34 @@ describe('MarkdownPreview', () => {
});
});
it('renders code blocks with correct structure for theme switching', async () => {
const content = '```javascript\nconst hello = "world";\n```';
render(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
await waitFor(() => {
// Check that rehype-highlight generates the correct structure
const preElement = screen
.getByRole('code', { hidden: true })
.closest('pre');
const codeElement = preElement?.querySelector('code');
expect(preElement).toBeInTheDocument();
expect(codeElement).toBeInTheDocument();
// The code element should have hljs class for theme switching to work
expect(codeElement).toHaveClass('hljs');
// Should also have language class
expect(codeElement).toHaveClass('language-javascript');
});
});
it('handles image loading errors gracefully', async () => {
const content = '![Test Image](invalid-image.jpg)';

View File

@@ -1,15 +1,16 @@
import React, { useState, useEffect, useMemo, type ReactNode } from 'react';
import { unified, type Preset } from 'unified';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkMath from 'remark-math';
import remarkRehype from 'remark-rehype';
import rehypeMathjax from 'rehype-mathjax';
import rehypeReact, { type Options } from 'rehype-react';
import rehypePrism from 'rehype-prism';
import rehypeHighlight from 'rehype-highlight';
import * as prod from 'react/jsx-runtime';
import { notifications } from '@mantine/notifications';
import { remarkWikiLinks } from '../../utils/remarkWikiLinks';
import { useWorkspace } from '../../hooks/useWorkspace';
import { useHighlightTheme } from '../../hooks/useHighlightTheme';
interface MarkdownPreviewProps {
content: string;
@@ -28,12 +29,6 @@ interface MarkdownLinkProps {
[key: string]: unknown;
}
interface MarkdownCodeProps {
children: ReactNode;
className?: string;
[key: string]: unknown;
}
const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
content,
handleFileSelect,
@@ -42,7 +37,10 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
null
);
const baseUrl = window.API_BASE_URL;
const { currentWorkspace } = useWorkspace();
const { currentWorkspace, colorScheme } = useWorkspace();
// Use the highlight theme hook
useHighlightTheme(colorScheme === 'auto' ? 'light' : colorScheme);
const processor = useMemo(() => {
const handleLinkClick = (
@@ -82,7 +80,7 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
.use(remarkMath)
.use(remarkRehype)
.use(rehypeMathjax)
.use(rehypePrism as Preset)
.use(rehypeHighlight)
.use(rehypeReact, {
jsx: prod.jsx,
jsxs: prod.jsxs,
@@ -107,13 +105,6 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
{children}
</a>
),
code: ({ children, className, ...props }: MarkdownCodeProps) => {
return (
<pre className={className}>
<code {...props}>{children}</code>
</pre>
);
},
},
} as Options);
}, [currentWorkspace?.name, baseUrl, handleFileSelect]);

View File

@@ -0,0 +1,36 @@
import { useEffect } from 'react';
// Import theme CSS as text that will be bundled
import atomOneLightTheme from 'highlight.js/styles/atom-one-light.css?inline';
import atomOneDarkTheme from 'highlight.js/styles/atom-one-dark.css?inline';
export const useHighlightTheme = (colorScheme: 'light' | 'dark') => {
useEffect(() => {
// Remove existing highlight theme
const existingStylesheet = document.querySelector(
'style[data-highlight-theme]'
);
if (existingStylesheet) {
existingStylesheet.remove();
}
// Add new theme stylesheet using bundled CSS
const style = document.createElement('style');
style.setAttribute('data-highlight-theme', 'true');
if (colorScheme === 'dark') {
style.textContent = atomOneDarkTheme as string;
} else {
style.textContent = atomOneLightTheme as string;
}
document.head.appendChild(style);
return () => {
// Cleanup on unmount
const stylesheet = document.querySelector('style[data-highlight-theme]');
if (stylesheet) {
stylesheet.remove();
}
};
}, [colorScheme]);
};

5
app/src/types/css-inline.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
// Type declarations for CSS imports with ?inline modifier
declare module '*.css?inline' {
const content: string;
export default content;
}

View File

@@ -83,7 +83,7 @@ describe('fileHelpers', () => {
const filePath = 'folder/file.md';
const expectedUrl =
'http://localhost:8080/api/v1/workspaces/my-workspace/files/folder%2Ffile.md';
'http://localhost:8080/api/v1/workspaces/my-workspace/files/content?file_path=folder%2Ffile.md';
const actualUrl = getFileUrl(workspaceName, filePath);
expect(actualUrl).toBe(expectedUrl);
@@ -94,7 +94,7 @@ describe('fileHelpers', () => {
const filePath = 'file.md';
const expectedUrl =
'http://localhost:8080/api/v1/workspaces/my%20workspace%20with%20spaces/files/file.md';
'http://localhost:8080/api/v1/workspaces/my%20workspace%20with%20spaces/files/content?file_path=file.md';
const actualUrl = getFileUrl(workspaceName, filePath);
expect(actualUrl).toBe(expectedUrl);
@@ -105,7 +105,7 @@ describe('fileHelpers', () => {
const filePath = 'folder with spaces/file with spaces.md';
const expectedUrl =
'http://localhost:8080/api/v1/workspaces/workspace/files/folder%20with%20spaces%2Ffile%20with%20spaces.md';
'http://localhost:8080/api/v1/workspaces/workspace/files/content?file_path=folder%20with%20spaces%2Ffile%20with%20spaces.md';
const actualUrl = getFileUrl(workspaceName, filePath);
expect(actualUrl).toBe(expectedUrl);
@@ -116,7 +116,7 @@ describe('fileHelpers', () => {
const filePath = 'file?name=test.md';
const expectedUrl =
'http://localhost:8080/api/v1/workspaces/test%26workspace/files/file%3Fname%3Dtest.md';
'http://localhost:8080/api/v1/workspaces/test%26workspace/files/content?file_path=file%3Fname%3Dtest.md';
const actualUrl = getFileUrl(workspaceName, filePath);
expect(actualUrl).toBe(expectedUrl);
@@ -127,7 +127,7 @@ describe('fileHelpers', () => {
const filePath = 'ファイル.md';
const expectedUrl =
'http://localhost:8080/api/v1/workspaces/%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/files/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md';
'http://localhost:8080/api/v1/workspaces/%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88/files/content?file_path=%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md';
const actualUrl = getFileUrl(workspaceName, filePath);
expect(actualUrl).toBe(expectedUrl);
@@ -138,7 +138,7 @@ describe('fileHelpers', () => {
const filePath = 'projects/2024/q1/report.md';
const expectedUrl =
'http://localhost:8080/api/v1/workspaces/docs/files/projects%2F2024%2Fq1%2Freport.md';
'http://localhost:8080/api/v1/workspaces/docs/files/content?file_path=projects%2F2024%2Fq1%2Freport.md';
const actualUrl = getFileUrl(workspaceName, filePath);
expect(actualUrl).toBe(expectedUrl);
@@ -146,20 +146,20 @@ describe('fileHelpers', () => {
it('handles edge cases with empty strings', () => {
expect(getFileUrl('', '')).toBe(
'http://localhost:8080/api/v1/workspaces//files/'
'http://localhost:8080/api/v1/workspaces//files/content?file_path='
);
expect(getFileUrl('workspace', '')).toBe(
'http://localhost:8080/api/v1/workspaces/workspace/files/'
'http://localhost:8080/api/v1/workspaces/workspace/files/content?file_path='
);
expect(getFileUrl('', 'file.md')).toBe(
'http://localhost:8080/api/v1/workspaces//files/file.md'
'http://localhost:8080/api/v1/workspaces//files/content?file_path=file.md'
);
});
it('uses the API base URL correctly', () => {
const url = getFileUrl('test', 'file.md');
expect(url).toBe(
'http://localhost:8080/api/v1/workspaces/test/files/file.md'
'http://localhost:8080/api/v1/workspaces/test/files/content?file_path=file.md'
);
expect(url).toContain(window.API_BASE_URL);
});

View File

@@ -13,5 +13,5 @@ export const isImageFile = (filePath: string): boolean => {
export const getFileUrl = (workspaceName: string, filePath: string) => {
return `${API_BASE_URL}/workspaces/${encodeURIComponent(
workspaceName
)}/files/${encodeURIComponent(filePath)}`;
)}/files/content?file_path=${encodeURIComponent(filePath)}`;
};

View File

@@ -52,9 +52,8 @@ export default defineConfig(({ mode }) => ({
// Markdown processing
markdown: [
'react-syntax-highlighter',
'rehype-highlight',
'rehype-mathjax',
'rehype-prism',
'rehype-react',
'remark',
'remark-math',

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,9 @@ func setupRouter(o Options) *chi.Mux {
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Rate limiting for API routes
// Public routes (no authentication required)
r.Group(func(r chi.Router) {
// Rate limiting for authentication endpoints to prevent brute force attacks
if o.Config.RateLimitRequests > 0 {
r.Use(httprate.LimitByIP(
o.Config.RateLimitRequests,
@@ -72,8 +74,6 @@ func setupRouter(o Options) *chi.Mux {
))
}
// Public routes (no authentication required)
r.Group(func(r chi.Router) {
r.Post("/auth/login", handler.Login(o.SessionManager, o.CookieService))
r.Post("/auth/refresh", handler.RefreshToken(o.SessionManager, o.CookieService))
})
@@ -134,7 +134,7 @@ func setupRouter(o Options) *chi.Mux {
r.Get("/lookup", handler.LookupFileByName())
r.Post("/upload", handler.UploadFile())
r.Put("/move", handler.MoveFile())
r.Post("/move", handler.MoveFile())
r.Post("/", handler.SaveFile())
r.Get("/content", handler.GetFileContent())
@@ -152,7 +152,9 @@ func setupRouter(o Options) *chi.Mux {
})
// Handle all other routes with static file server
r.Get("/*", handlers.NewStaticHandler(o.Config.StaticPath).ServeHTTP)
staticHandler := handlers.NewStaticHandler(o.Config.StaticPath)
r.Get("/*", staticHandler.ServeHTTP)
r.Head("/*", staticHandler.ServeHTTP)
return r
}

View File

@@ -2,9 +2,11 @@ package handlers
import (
"io"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"lemma/internal/context"
@@ -205,7 +207,13 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
return
}
w.Header().Set("Content-Type", "text/plain")
// Detect MIME type based on file extension
contentType := mime.TypeByExtension(filepath.Ext(decodedPath))
if contentType == "" {
// Fallback to text/plain if MIME type cannot be determined
contentType = "text/plain"
}
w.Header().Set("Content-Type", contentType)
_, err = w.Write(content)
if err != nil {
log.Error("failed to write response",

View File

@@ -156,6 +156,54 @@ func testFileHandlers(t *testing.T, dbConfig DatabaseConfig) {
assert.Equal(t, http.StatusNotFound, rr.Code)
})
t.Run("move file", func(t *testing.T) {
srcPath := "original.md"
destPath := "moved.md"
content := "This file will be moved"
// Create file
rr := h.makeRequestRaw(t, http.MethodPost, baseURL+"?file_path="+url.QueryEscape(srcPath), strings.NewReader(content), h.RegularTestUser)
require.Equal(t, http.StatusOK, rr.Code)
// Move file
moveURL := baseURL + "/move?src_path=" + url.QueryEscape(srcPath) + "&dest_path=" + url.QueryEscape(destPath)
rr = h.makeRequest(t, http.MethodPost, moveURL, nil, h.RegularTestUser)
require.Equal(t, http.StatusOK, rr.Code)
// Verify source is gone
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(srcPath), nil, h.RegularTestUser)
assert.Equal(t, http.StatusNotFound, rr.Code)
// Verify destination exists with correct content
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(destPath), nil, h.RegularTestUser)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, content, rr.Body.String())
})
t.Run("rename file in directory", func(t *testing.T) {
srcPath := "folder/old-name.md"
destPath := "folder/new-name.md"
content := "This file will be renamed"
// Create file
rr := h.makeRequestRaw(t, http.MethodPost, baseURL+"?file_path="+url.QueryEscape(srcPath), strings.NewReader(content), h.RegularTestUser)
require.Equal(t, http.StatusOK, rr.Code)
// Rename file (move within same directory)
moveURL := baseURL + "/move?src_path=" + url.QueryEscape(srcPath) + "&dest_path=" + url.QueryEscape(destPath)
rr = h.makeRequest(t, http.MethodPost, moveURL, nil, h.RegularTestUser)
require.Equal(t, http.StatusOK, rr.Code)
// Verify source is gone
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(srcPath), nil, h.RegularTestUser)
assert.Equal(t, http.StatusNotFound, rr.Code)
// Verify destination exists with correct content
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(destPath), nil, h.RegularTestUser)
require.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, content, rr.Body.String())
})
t.Run("last opened file", func(t *testing.T) {
// Initially should be empty
rr := h.makeRequest(t, http.MethodGet, baseURL+"/last", nil, h.RegularTestUser)