diff options
author | Raphael <mail@raphaelkabo.com> | 2023-10-09 11:10:51 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-09 11:10:51 +0100 |
commit | cc6fcb4c405d8cffacbf9b1082abf61e918482fa (patch) | |
tree | 693f324550dccedd50b6313165b88281a8ebcac8 | |
parent | 25fcdd1023550631f5fec6f750829fe09a311d66 (diff) | |
parent | 31022a7d323a351041b7b8508fb56c14fd699580 (diff) |
Merge pull request #114 from lowercasename/rk/static-pages
Static pages
-rw-r--r-- | config/config.example.toml | 15 | ||||
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | pnpm-lock.yaml | 251 | ||||
-rwxr-xr-x | public/css/style.css | 9 | ||||
-rwxr-xr-x | src/app.ts | 2 | ||||
-rw-r--r-- | src/lib/config.ts | 23 | ||||
-rwxr-xr-x | src/routes.js | 6 | ||||
-rw-r--r-- | src/routes/activitypub.ts | 19 | ||||
-rw-r--r-- | src/routes/event.ts | 4 | ||||
-rw-r--r-- | src/routes/frontend.ts | 27 | ||||
-rw-r--r-- | src/routes/static.ts | 36 | ||||
-rw-r--r-- | src/util/config.ts | 19 | ||||
-rw-r--r-- | src/util/markdown.ts | 16 | ||||
-rw-r--r-- | static/privacy-policy.md | 1 | ||||
-rwxr-xr-x | views/layouts/main.handlebars | 23 | ||||
-rwxr-xr-x | views/newevent.handlebars | 3 | ||||
-rw-r--r-- | views/static.handlebars | 10 |
17 files changed, 401 insertions, 67 deletions
diff --git a/config/config.example.toml b/config/config.example.toml index 1ffbeb7..8f5a09d 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -31,3 +31,18 @@ smtp_password = "" [sendgrid] api_key = "" + +# Links to static pages (for example a privacy policy) or an external community page, +# which will be displayed in the footer. +# If paths begin with a slash, they are treated as internal and will open the specified +# Markdown or text file. If they are absolute (begin with https://), they will simply +# link to the specified URL. + +# [[static_pages]] +# title = "Privacy Policy" +# path = "/privacy" +# filename = "privacy-policy.md" + +# [[static_pages]] +# title = "External Link" +# path = "https://example.com" diff --git a/package.json b/package.json index 0a783ce..d573f7c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@sendgrid/mail": "^6.5.5", "cors": "^2.8.5", + "dompurify": "^3.0.6", "express": "^4.18.2", "express-fileupload": "^1.4.1", "express-handlebars": "^6.0.7", @@ -30,6 +31,7 @@ "ical": "^0.6.0", "ical-generator": "^1.15.4", "jimp": "^0.16.13", + "jsdom": "^22.1.0", "marked": "^9.1.0", "moment-timezone": "^0.5.43", "mongoose": "^5.13.20", @@ -45,8 +47,10 @@ "wait-on": "^7.0.1" }, "devDependencies": { + "@types/dompurify": "^3.0.3", "@types/express": "^4.17.18", "@types/ical": "^0.8.1", + "@types/jsdom": "^21.1.3", "@types/multer": "^1.4.8", "@types/node": "^20.8.2", "@types/nodemailer": "^6.4.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c1c8e7..f303bc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: cors: specifier: ^2.8.5 version: 2.8.5 + dompurify: + specifier: ^3.0.6 + version: 3.0.6 express: specifier: ^4.18.2 version: 4.18.2 @@ -38,6 +41,9 @@ dependencies: jimp: specifier: ^0.16.13 version: 0.16.13 + jsdom: + specifier: ^22.1.0 + version: 22.1.0 marked: specifier: ^9.1.0 version: 9.1.0 @@ -79,12 +85,18 @@ dependencies: version: 7.0.1 devDependencies: + '@types/dompurify': + specifier: ^3.0.3 + version: 3.0.3 '@types/express': specifier: ^4.17.18 version: 4.17.18 '@types/ical': specifier: ^0.8.1 version: 0.8.1 + '@types/jsdom': + specifier: ^21.1.3 + version: 21.1.3 '@types/multer': specifier: ^1.4.8 version: 1.4.8 @@ -670,6 +682,11 @@ packages: resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} dev: false + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: false + /@types/body-parser@1.19.3: resolution: {integrity: sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==} dependencies: @@ -693,6 +710,12 @@ packages: '@types/node': 20.8.2 dev: true + /@types/dompurify@3.0.3: + resolution: {integrity: sha512-odiGr/9/qMqjcBOe5UhcNLOFHSYmKFOyr+bJ/Xu3Qp4k1pNPAlNLUVNNLcLfjQI7+W7ObX58EdD3H+3p3voOvA==} + dependencies: + '@types/trusted-types': 2.0.4 + dev: true + /@types/express-serve-static-core@4.17.37: resolution: {integrity: sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==} dependencies: @@ -721,6 +744,14 @@ packages: rrule: 2.6.4 dev: true + /@types/jsdom@21.1.3: + resolution: {integrity: sha512-1zzqSP+iHJYV4lB3lZhNBa012pubABkj9yG/GuXuf6LZH1cSPIJBqFDrm5JX65HHt6VOnNYdTui/0ySerRbMgA==} + dependencies: + '@types/node': 20.8.2 + '@types/tough-cookie': 4.0.3 + parse5: 7.1.2 + dev: true + /@types/mime@1.3.3: resolution: {integrity: sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==} dev: true @@ -801,7 +832,10 @@ packages: /@types/tough-cookie@4.0.3: resolution: {integrity: sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==} - dev: false + + /@types/trusted-types@2.0.4: + resolution: {integrity: sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==} + dev: true /@types/yauzl@2.10.1: resolution: {integrity: sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==} @@ -811,6 +845,10 @@ packages: dev: true optional: true + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: false + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true @@ -837,6 +875,15 @@ packages: hasBin: true dev: true + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: false + /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -1275,6 +1322,13 @@ packages: which: 2.0.2 dev: true + /cssstyle@3.0.0: + resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} + engines: {node: '>=14'} + dependencies: + rrweb-cssom: 0.6.0 + dev: false + /cypress@13.3.0: resolution: {integrity: sha512-mpI8qcTwLGiA4zEQvTC/U1xGUezVV4V8HQCOYjlEOrVmU1etVvxOjkCXHGwrlYdZU/EPmUiWfsO3yt1o+Q2bgw==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} @@ -1332,6 +1386,15 @@ packages: dependencies: assert-plus: 1.0.0 + /data-urls@4.0.0: + resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} + engines: {node: '>=14'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + dev: false + /dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} dev: true @@ -1393,7 +1456,10 @@ packages: dependencies: ms: 2.1.2 supports-color: 8.1.1 - dev: true + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1464,6 +1530,13 @@ packages: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} dev: false + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: false + /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -1471,6 +1544,10 @@ packages: domelementtype: 2.3.0 dev: false + /dompurify@3.0.6: + resolution: {integrity: sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==} + dev: false + /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dependencies: @@ -1515,7 +1592,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: false /escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -2060,7 +2136,6 @@ packages: /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: true /has-property-descriptors@1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} @@ -2082,6 +2157,13 @@ packages: dependencies: function-bind: 1.1.1 + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: false + /htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} dependencies: @@ -2102,6 +2184,17 @@ packages: toidentifier: 1.0.1 dev: false + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: false + /http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -2120,6 +2213,16 @@ packages: sshpk: 1.17.0 dev: true + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: false + /human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -2149,6 +2252,13 @@ packages: safer-buffer: 2.1.2 dev: false + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2270,6 +2380,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2328,6 +2442,44 @@ packages: /jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + /jsdom@22.1.0: + resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==} + engines: {node: '>=16'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + cssstyle: 3.0.0 + data-urls: 4.0.0 + decimal.js: 10.4.3 + domexception: 4.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + rrweb-cssom: 0.6.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 12.0.1 + ws: 8.14.2 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} dev: true @@ -2755,6 +2907,10 @@ packages: path-key: 3.1.1 dev: true + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: false + /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} dev: false @@ -2883,6 +3039,11 @@ packages: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} dev: false + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3033,7 +3194,6 @@ packages: /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: true /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3163,7 +3323,6 @@ packages: /requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: true /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -3208,6 +3367,10 @@ packages: luxon: 1.28.1 dev: true + /rrweb-cssom@0.6.0: + resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -3253,6 +3416,13 @@ packages: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: false + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -3474,7 +3644,10 @@ packages: engines: {node: '>=10'} dependencies: has-flag: 4.0.0 - dev: true + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -3550,7 +3723,13 @@ packages: punycode: 2.3.0 universalify: 0.2.0 url-parse: 1.5.10 - dev: true + + /tr46@4.1.1: + resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} + engines: {node: '>=14'} + dependencies: + punycode: 2.3.0 + dev: false /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -3624,7 +3803,6 @@ packages: /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - dev: true /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} @@ -3651,7 +3829,6 @@ packages: dependencies: querystringify: 2.2.0 requires-port: 1.0.0 - dev: true /utif@2.0.1: resolution: {integrity: sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==} @@ -3697,6 +3874,13 @@ packages: core-util-is: 1.0.2 extsprintf: 1.3.0 + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: false + /wait-on@7.0.1: resolution: {integrity: sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==} engines: {node: '>=12.0.0'} @@ -3711,6 +3895,31 @@ packages: - debug dev: false + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: false + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: false + + /whatwg-url@12.0.1: + resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} + engines: {node: '>=14'} + dependencies: + tr46: 4.1.1 + webidl-conversions: 7.0.0 + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3744,6 +3953,19 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws@8.14.2: + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xhr@2.6.0: resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} dependencies: @@ -3753,6 +3975,11 @@ packages: xtend: 4.0.2 dev: false + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: false + /xml-parse-from-string@1.0.1: resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} dev: false @@ -3770,6 +3997,10 @@ packages: engines: {node: '>=4.0'} dev: false + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/public/css/style.css b/public/css/style.css index 0f149e8..8e6322e 100755 --- a/public/css/style.css +++ b/public/css/style.css @@ -54,6 +54,10 @@ body, html { padding: 5px 0; } +#footerContainer p { + margin-bottom: 0.25rem; +} + #sidebar { background: #f5f5f5; border-bottom: 2px solid #e0e0e0; @@ -396,3 +400,8 @@ label:not(.form-check-label) { input[type="datetime-local"] { max-width: 20rem; } + +article.static-page header { + margin-bottom: 1rem; + border-bottom: 1px solid #e0e0e0; +} @@ -6,6 +6,7 @@ import frontend from "./routes/frontend.js"; import activitypub from "./routes/activitypub.js"; import event from "./routes/event.js"; import group from "./routes/group.js"; +import staticPages from "./routes/static.js"; import { initEmailService } from "./lib/email.js"; @@ -53,6 +54,7 @@ app.use(express.json({ type: "application/json" })); app.use(express.urlencoded({ extended: true })); // Router // +app.use("/", staticPages); app.use("/", frontend); app.use("/", activitypub); app.use("/", event); diff --git a/src/lib/config.ts b/src/lib/config.ts index 7b35b98..6f142e5 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -2,6 +2,12 @@ import fs from "fs"; import toml from "toml"; import { exitWithError } from "./process.js"; +interface StaticPage { + title: string; + path: string; + filename: string; +} + interface GathioConfig { general: { domain: string; @@ -25,9 +31,21 @@ interface GathioConfig { sendgrid?: { api_key: string; }; + static_pages: StaticPage[]; +} + +interface FrontendConfig { + domain: string; + siteName: string; + isFederated: boolean; + emailLogoUrl: string; + showKofi: boolean; + showInstanceInformation: boolean; + staticPages: StaticPage[]; + version: string; } -export const publicConfig = () => { +export const frontendConfig = (): FrontendConfig => { const config = getConfig(); return { domain: config.general.domain, @@ -35,6 +53,9 @@ export const publicConfig = () => { isFederated: config.general.is_federated, emailLogoUrl: config.general.email_logo_url, showKofi: config.general.show_kofi, + showInstanceInformation: config.static_pages?.length > 0, + staticPages: config.static_pages, + version: process.env.npm_package_version || "unknown", }; }; diff --git a/src/routes.js b/src/routes.js index 5371e0e..d59a738 100755 --- a/src/routes.js +++ b/src/routes.js @@ -2,7 +2,7 @@ import fs from "fs"; import express from "express"; import { customAlphabet } from "nanoid"; import randomstring from "randomstring"; -import { getConfig } from "./lib/config.js"; +import { frontendConfig, getConfig } from "./lib/config.js"; import { addToLog } from "./helpers.js"; import moment from "moment-timezone"; import crypto from "crypto"; @@ -1505,9 +1505,7 @@ router.post("/activitypub/inbox", (req, res) => { }); router.use(function (req, res, next) { - res.status(404); - res.render("404", { url: req.url }); - return; + return res.status(404).render("404", frontendConfig()); }); addToLog("startup", "success", "Started up successfully"); diff --git a/src/routes/activitypub.ts b/src/routes/activitypub.ts index 2c4231a..2b8fb4a 100644 --- a/src/routes/activitypub.ts +++ b/src/routes/activitypub.ts @@ -1,7 +1,7 @@ import { Router, Request, Response, NextFunction } from "express"; import { createFeaturedPost, createWebfinger } from "../activitypub.js"; import { acceptsActivityPub } from "../lib/activitypub.js"; -import getConfig from "../lib/config.js"; +import getConfig, { frontendConfig } from "../lib/config.js"; import Event from "../models/Event.js"; import { addToLog } from "../helpers.js"; @@ -15,8 +15,7 @@ const send404IfNotFederated = ( next: NextFunction, ) => { if (!config.general.is_federated) { - res.status(404).render("404", { url: req.url }); - return; + return res.status(404).render("404", frontendConfig()); } next(); }; @@ -49,10 +48,10 @@ router.get("/:eventID/m/:hash", async (req: Request, res: Response) => { id: eventID, }); if (!event) { - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } else { if (!event.activityPubMessages) { - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } const message = event.activityPubMessages.find( (el) => el.id === id, @@ -69,7 +68,7 @@ router.get("/:eventID/m/:hash", async (req: Request, res: Response) => { ); } } else { - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } } } catch (err) { @@ -81,7 +80,7 @@ router.get("/:eventID/m/:hash", async (req: Request, res: Response) => { " failed with error: " + err, ); - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }); @@ -103,7 +102,7 @@ router.get("/.well-known/webfinger", async (req, res) => { const event = await Event.findOne({ id: eventID }); if (!event) { - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } else { if (acceptsActivityPub(req)) { res.header( @@ -122,7 +121,7 @@ router.get("/.well-known/webfinger", async (req, res) => { "error", `Attempt to render webfinger for ${resource} failed with error: ${err}`, ); - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } } }); @@ -167,7 +166,7 @@ router.get("/:eventID/followers", async (req, res) => { "error", `Attempt to render followers for ${eventID} failed with error: ${err}`, ); - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }); diff --git a/src/routes/event.ts b/src/routes/event.ts index 2245009..cfd877e 100644 --- a/src/routes/event.ts +++ b/src/routes/event.ts @@ -2,7 +2,6 @@ import { Router, Response, Request } from "express"; import multer from "multer"; import Jimp from "jimp"; import moment from "moment-timezone"; -import { marked } from "marked"; import { generateEditToken, generateEventID, @@ -26,6 +25,7 @@ import getConfig from "../lib/config.js"; import { sendEmailFromTemplate } from "../lib/email.js"; import crypto from "crypto"; import ical from "ical"; +import { markdownToSanitizedHTML } from "../util/markdown.js"; const config = getConfig(); @@ -148,7 +148,7 @@ router.post( eventID, config.general.domain, publicKey, - marked.parse(eventData.eventDescription), + markdownToSanitizedHTML(eventData.eventDescription), eventData.eventName, eventData.eventLocation, eventImageFilename, diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts index c9594ef..c405572 100644 --- a/src/routes/frontend.ts +++ b/src/routes/frontend.ts @@ -1,9 +1,8 @@ import { Router, Request, Response } from "express"; import moment from "moment-timezone"; import { marked } from "marked"; -import { frontendConfig } from "../util/config.js"; -import { renderPlain } from "../util/markdown.js"; -import getConfig from "../lib/config.js"; +import { markdownToSanitizedHTML, renderPlain } from "../util/markdown.js"; +import getConfig, { frontendConfig } from "../lib/config.js"; import { addToLog, exportICal } from "../helpers.js"; import Event from "../models/Event.js"; import EventGroup, { IEventGroup } from "../models/EventGroup.js"; @@ -30,9 +29,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is .populate("eventGroup"); if (!event) { - res.status(404); - res.render("404", { url: req.url }); - return; + return res.status(404).render("404", frontendConfig()); } const parsedLocation = event.location.replace(/\s+/g, "+"); let displayDate; @@ -94,7 +91,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { eventHasBegun = true; } let fromNow = moment.tz(event.start, event.timezone).fromNow(); - let parsedDescription = marked.parse(event.description); + let parsedDescription = markdownToSanitizedHTML(event.description); let eventEditToken = event.editToken; let escapedName = event.name.replace(/\s+/g, "+"); @@ -252,7 +249,7 @@ router.get("/:eventID", async (req: Request, res: Response) => { err, ); console.log(err); - res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }); @@ -263,9 +260,11 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { }).lean(); if (!eventGroup) { - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } - const parsedDescription = marked.parse(eventGroup.description); + const parsedDescription = markdownToSanitizedHTML( + eventGroup.description, + ); const eventGroupEditToken = eventGroup.editToken; const escapedName = eventGroup.name.replace(/\s+/g, "+"); const eventGroupHasCoverImage = !!eventGroup.image; @@ -364,7 +363,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => { `Attempt to display event group ${req.params.eventGroupID} failed with error: ${err}`, ); console.log(err); - return res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }); @@ -391,7 +390,7 @@ router.get( `Attempt to display event group feed for ${req.params.eventGroupID} failed with error: ${err}`, ); console.log(err); - res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }, ); @@ -413,7 +412,7 @@ router.get("/export/event/:eventID", async (req: Request, res: Response) => { `Attempt to export event ${req.params.eventID} failed with error: ${err}`, ); console.log(err); - res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }); @@ -439,7 +438,7 @@ router.get( `Attempt to export event group ${req.params.eventGroupID} failed with error: ${err}`, ); console.log(err); - res.status(404).render("404", { url: req.url }); + return res.status(404).render("404", frontendConfig()); } }, ); diff --git a/src/routes/static.ts b/src/routes/static.ts new file mode 100644 index 0000000..33f0225 --- /dev/null +++ b/src/routes/static.ts @@ -0,0 +1,36 @@ +import { Router, Request, Response } from "express"; +import fs from "fs"; +import getConfig, { frontendConfig } from "../lib/config.js"; +import { markdownToSanitizedHTML } from "../util/markdown.js"; + +const config = getConfig(); +const router = Router(); + +if (config.static_pages?.length) { + config.static_pages + .filter((page) => page.path?.startsWith("/") && page.filename) + .forEach((page) => { + router.get(page.path, (_: Request, res: Response) => { + try { + if (fs.existsSync(`./static/${page.filename}`)) { + const fileBody = fs.readFileSync( + `./static/${page.filename}`, + "utf-8", + ); + const parsed = markdownToSanitizedHTML(fileBody); + return res.render("static", { + title: page.title, + content: parsed, + ...frontendConfig(), + }); + } + return res.status(404).render("404", frontendConfig()); + } catch (err) { + console.error(err); + return res.status(404).render("404", frontendConfig()); + } + }); + }); +} + +export default router; diff --git a/src/util/config.ts b/src/util/config.ts deleted file mode 100644 index d1fd05b..0000000 --- a/src/util/config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import getConfig from "../lib/config.js"; - -const config = getConfig(); - -interface FrontendConfig { - domain: string; - email: string; - siteName: string; - showKofi: boolean; - isFederated: boolean; -} - -export const frontendConfig = (): FrontendConfig => ({ - domain: config.general.domain, - email: config.general.email, - siteName: config.general.site_name, - showKofi: config.general.show_kofi, - isFederated: config.general.is_federated, -}); diff --git a/src/util/markdown.ts b/src/util/markdown.ts index 9f5d384..bab50bd 100644 --- a/src/util/markdown.ts +++ b/src/util/markdown.ts @@ -1,7 +1,6 @@ -// Extra marked renderer (used to render plaintext event description for page metadata) -// Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ - import { marked } from "marked"; +import { JSDOM } from "jsdom"; +import DOMPurify from "dompurify"; // ? to ? helper function htmlEscapeToText(text: string) { @@ -14,6 +13,9 @@ function htmlEscapeToText(text: string) { }); } +// Extra marked renderer (used to render plaintext event description for page metadata) +// Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ + export const renderPlain = () => { var render = new marked.Renderer(); // render just the text of a link, strong, em @@ -42,3 +44,11 @@ export const renderPlain = () => { }; return render; }; + +export const markdownToSanitizedHTML = (markdown: string) => { + const html = marked.parse(markdown); + const window = new JSDOM("").window; + const purify = DOMPurify(window); + const clean = purify.sanitize(html); + return clean; +}; diff --git a/static/privacy-policy.md b/static/privacy-policy.md new file mode 100644 index 0000000..507ef47 --- /dev/null +++ b/static/privacy-policy.md @@ -0,0 +1 @@ +This is an example privacy policy. You should edit this file - feel free to take inspiration from the [gath.io instance privacy policy](https://gath.io/privacy). diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index daa5a37..fb493a4 100755 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -68,12 +68,27 @@ {{{body}}} </div> <div id="footerContainer"> - <small class="text-muted"> - Version 1.3.0 · <a href="https://github.com/lowercasename/gathio">GitHub</a> · Made with <i class="far fa-heart"></i> by <a href="https://raphaelkabo.com">Raphael</a><br /> - </small> + {{#if showInstanceInformation}} + <p class="small text-muted"> + <strong>{{domain}}</strong> + {{#each staticPages}} + {{#if @first}} + · + {{/if}} + + <a href="{{this.path}}">{{this.title}}</a> + + {{#unless @last}} + · + {{/unless}} + {{/each}} + </p> + {{/if}} + <p class="small text-muted"> + <strong>Gathio</strong> version {{version}} · <a href="https://github.com/lowercasename/gathio">GitHub</a> · Made with <i class="far fa-heart"></i> by <a href="https://raphaelkabo.com">Raphael</a> and <a href="https://github.com/lowercasename/gathio/graphs/contributors">contributors</a> + </p> </div> </div> </div> </div> - </body> </html> diff --git a/views/newevent.handlebars b/views/newevent.handlebars index 349c355..a3b35b3 100755 --- a/views/newevent.handlebars +++ b/views/newevent.handlebars @@ -1,3 +1,4 @@ +<article> <div class="container mb-4"> <div class="row"> <div class="col-sm-4 p-2"> @@ -51,5 +52,7 @@ </form> </div> +</article> + <script src="/js/generate-timezones.js"></script> <script src="/js/modules/new.js"></script>
\ No newline at end of file diff --git a/views/static.handlebars b/views/static.handlebars new file mode 100644 index 0000000..d28d8f2 --- /dev/null +++ b/views/static.handlebars @@ -0,0 +1,10 @@ + +<article class="static-page"> + <header> + <h1>{{title}}</h1> + </header> + <main> + {{{content}}} + </main> +</article> + |