Merge branch 'master' into Allow_embedded_code_in_posts_permanent_memory

This commit is contained in:
Alexander Friedland 2019-10-07 10:35:04 +02:00 committed by GitHub
commit 39b3d71be4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 4232 additions and 3028 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -7,6 +7,6 @@
"autoFix": true
}
],
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"eslint.autoFixOnSave": true
}

166
CHANGELOG.md Normal file
View File

@ -0,0 +1,166 @@
# Change Log
## [Unreleased](https://github.com/Human-Connection/Human-Connection/tree/HEAD)
[Full Changelog](https://github.com/Human-Connection/Human-Connection/compare/0.1.0...HEAD)
**Fixed bugs:**
- 🐛 \[Bug\] Update maintenance page email [\#1731](https://github.com/Human-Connection/Human-Connection/issues/1731)
- 🐛 \[Bug\] Editing comments is not reactive again [\#1718](https://github.com/Human-Connection/Human-Connection/issues/1718)
- 🐛 \[Bug\] Comments with mentions in the end not displayed [\#1665](https://github.com/Human-Connection/Human-Connection/issues/1665)
- 🐛 \[Bug\] Moderators and Admins can be blocked [\#1663](https://github.com/Human-Connection/Human-Connection/issues/1663)
- 🐛 \[Bug\] Links in Comments don't show up [\#1661](https://github.com/Human-Connection/Human-Connection/issues/1661)
- 🐛 \[Bug\] Delete the Sleep Icon [\#1659](https://github.com/Human-Connection/Human-Connection/issues/1659)
- 🐛 \[Bug\] Mail Layout German/English not working in Webmail [\#1656](https://github.com/Human-Connection/Human-Connection/issues/1656)
- 🐛 \[Bug\] Far to less Characters per Contribution \(2000\) [\#1639](https://github.com/Human-Connection/Human-Connection/issues/1639)
- 🐛 \[Bug\] Notifications do just update with page reload [\#1637](https://github.com/Human-Connection/Human-Connection/issues/1637)
- 🐛 \[Bug\] Admin statistics entries are missing [\#1633](https://github.com/Human-Connection/Human-Connection/issues/1633)
- 🐛 \[Bug\] Can no users used all hashtags be correct? [\#1632](https://github.com/Human-Connection/Human-Connection/issues/1632)
- 🐛 \[Bug\] Create account has no info about email, no localisation, no HC logo [\#1631](https://github.com/Human-Connection/Human-Connection/issues/1631)
- 🐛 \[Bug\] Embeds are displayed when creating comments but get removed [\#1547](https://github.com/Human-Connection/Human-Connection/issues/1547)
- 🐛 \[Bug\] One cypress test fails but it does not fail the build [\#1312](https://github.com/Human-Connection/Human-Connection/issues/1312)
- 🐛 \[Bug\] TypeError: Cannot read property 'offsetTop' of null [\#1273](https://github.com/Human-Connection/Human-Connection/issues/1273)
**Closed issues:**
- 🚀 \[Feature\] Extend Emoticons [\#1745](https://github.com/Human-Connection/Human-Connection/issues/1745)
- 🚀 \[Feature\] Show "Edited" on comments and Posts [\#1669](https://github.com/Human-Connection/Human-Connection/issues/1669)
- 🚀 \[Feature\] Change slug [\#1650](https://github.com/Human-Connection/Human-Connection/issues/1650)
- 🚀 \[Feature\] Make the slug more visible and usable [\#1486](https://github.com/Human-Connection/Human-Connection/issues/1486)
- 🚀 \[Feature\] Report with reason [\#1469](https://github.com/Human-Connection/Human-Connection/issues/1469)
- 🚀 \[Feature\] Update `lastActiveAt` on every JWT token check [\#1305](https://github.com/Human-Connection/Human-Connection/issues/1305)
- 🚀 \[Feature\] Make Invite an Registration E-Mails translatable and pretty [\#1186](https://github.com/Human-Connection/Human-Connection/issues/1186)
- 🚀 \[Feature\] @Username: Unique user identification if identical profile names exist [\#1069](https://github.com/Human-Connection/Human-Connection/issues/1069)
- 🚀 \[Feature\] UserSettings - YourData - Email [\#407](https://github.com/Human-Connection/Human-Connection/issues/407)
**Merged pull requests:**
- fixes\#1305 lastActiveAt [\#1809](https://github.com/Human-Connection/Human-Connection/pull/1809) ([iylim](https://github.com/iylim))
- Show that a Post/Comment has been edited [\#1807](https://github.com/Human-Connection/Human-Connection/pull/1807) ([mattwr18](https://github.com/mattwr18))
- fix invites count calculation for admin dashboard [\#1806](https://github.com/Human-Connection/Human-Connection/pull/1806) ([vbelolapotkov](https://github.com/vbelolapotkov))
- build\(deps\): bump nuxt from 2.9.2 to 2.10.0 in /webapp [\#1804](https://github.com/Human-Connection/Human-Connection/pull/1804) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps\): bump cross-env from 6.0.2 to 6.0.3 in /webapp [\#1803](https://github.com/Human-Connection/Human-Connection/pull/1803) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps\): bump @hapi/joi from 16.1.4 to 16.1.5 in /backend [\#1802](https://github.com/Human-Connection/Human-Connection/pull/1802) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps\): bump cross-env from 6.0.2 to 6.0.3 in /backend [\#1801](https://github.com/Human-Connection/Human-Connection/pull/1801) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps-dev\): bump cross-env from 6.0.2 to 6.0.3 [\#1800](https://github.com/Human-Connection/Human-Connection/pull/1800) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Remove language toggle from email template [\#1798](https://github.com/Human-Connection/Human-Connection/pull/1798) ([alina-beck](https://github.com/alina-beck))
- Max aspect ratio of 1:1 for Post index page [\#1796](https://github.com/Human-Connection/Human-Connection/pull/1796) ([mattwr18](https://github.com/mattwr18))
- build\(deps\): bump cross-env from 6.0.0 to 6.0.2 in /backend [\#1794](https://github.com/Human-Connection/Human-Connection/pull/1794) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps-dev\): bump cross-env from 6.0.0 to 6.0.2 [\#1793](https://github.com/Human-Connection/Human-Connection/pull/1793) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps\): bump metascraper-description from 5.7.5 to 5.7.6 in /backend [\#1792](https://github.com/Human-Connection/Human-Connection/pull/1792) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps\): bump cross-env from 6.0.0 to 6.0.2 in /webapp [\#1791](https://github.com/Human-Connection/Human-Connection/pull/1791) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- build\(deps\): bump @nuxtjs/apollo from 4.0.0-rc13.1 to 4.0.0-rc14 in /webapp [\#1790](https://github.com/Human-Connection/Human-Connection/pull/1790) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Update Post query for blockedByUsers [\#1788](https://github.com/Human-Connection/Human-Connection/pull/1788) ([mattwr18](https://github.com/mattwr18))
- Update neo4j to Enterprise edition [\#1787](https://github.com/Human-Connection/Human-Connection/pull/1787) ([mattwr18](https://github.com/mattwr18))
- Update to 0.1.2 [\#1786](https://github.com/Human-Connection/Human-Connection/pull/1786) ([mattwr18](https://github.com/mattwr18))
- Bump metascraper-logo from 5.7.5 to 5.7.6 in /backend [\#1783](https://github.com/Human-Connection/Human-Connection/pull/1783) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-url from 5.7.5 to 5.7.6 in /backend [\#1782](https://github.com/Human-Connection/Human-Connection/pull/1782) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump graphql-middleware from 3.0.5 to 4.0.1 in /backend [\#1781](https://github.com/Human-Connection/Human-Connection/pull/1781) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump eslint from 6.4.0 to 6.5.1 in /backend [\#1780](https://github.com/Human-Connection/Human-Connection/pull/1780) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-audio from 5.7.5 to 5.7.6 in /backend [\#1779](https://github.com/Human-Connection/Human-Connection/pull/1779) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-publisher from 5.7.4 to 5.7.6 in /backend [\#1778](https://github.com/Human-Connection/Human-Connection/pull/1778) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-youtube from 5.7.5 to 5.7.6 in /backend [\#1777](https://github.com/Human-Connection/Human-Connection/pull/1777) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @babel/preset-env from 7.6.0 to 7.6.2 in /backend [\#1776](https://github.com/Human-Connection/Human-Connection/pull/1776) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-soundcloud from 5.7.4 to 5.7.6 in /backend [\#1775](https://github.com/Human-Connection/Human-Connection/pull/1775) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-author from 5.7.4 to 5.7.6 in /backend [\#1774](https://github.com/Human-Connection/Human-Connection/pull/1774) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Fix failing test [\#1772](https://github.com/Human-Connection/Human-Connection/pull/1772) ([aonomike](https://github.com/aonomike))
- Bump metascraper-date from 5.7.4 to 5.7.6 in /backend [\#1771](https://github.com/Human-Connection/Human-Connection/pull/1771) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump tiptap-extensions from 1.27.0 to 1.28.0 in /webapp [\#1770](https://github.com/Human-Connection/Human-Connection/pull/1770) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump date-fns from 2.3.0 to 2.4.1 in /backend [\#1769](https://github.com/Human-Connection/Human-Connection/pull/1769) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump date-fns from 2.4.0 to 2.4.1 in /webapp [\#1768](https://github.com/Human-Connection/Human-Connection/pull/1768) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump graphql-middleware-sentry from 3.2.0 to 3.2.1 in /backend [\#1767](https://github.com/Human-Connection/Human-Connection/pull/1767) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-lang from 5.7.4 to 5.7.6 in /backend [\#1766](https://github.com/Human-Connection/Human-Connection/pull/1766) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump tiptap from 1.25.0 to 1.26.0 in /webapp [\#1765](https://github.com/Human-Connection/Human-Connection/pull/1765) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-video from 5.7.5 to 5.7.6 in /backend [\#1764](https://github.com/Human-Connection/Human-Connection/pull/1764) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump apollo-server from 2.9.3 to 2.9.4 in /backend [\#1762](https://github.com/Human-Connection/Human-Connection/pull/1762) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-image from 5.7.5 to 5.7.6 in /backend [\#1761](https://github.com/Human-Connection/Human-Connection/pull/1761) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump eslint-loader from 3.0.1 to 3.0.2 in /webapp [\#1760](https://github.com/Human-Connection/Human-Connection/pull/1760) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-title from 5.7.5 to 5.7.6 in /backend [\#1759](https://github.com/Human-Connection/Human-Connection/pull/1759) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump nodemon from 1.19.2 to 1.19.3 in /backend [\#1758](https://github.com/Human-Connection/Human-Connection/pull/1758) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- fix email middleware transport config [\#1757](https://github.com/Human-Connection/Human-Connection/pull/1757) ([vbelolapotkov](https://github.com/vbelolapotkov))
- 1273 fix post page nav suggestions [\#1756](https://github.com/Human-Connection/Human-Connection/pull/1756) ([roschaefer](https://github.com/roschaefer))
- docs: moves storybook into webapp/README.md [\#1755](https://github.com/Human-Connection/Human-Connection/pull/1755) ([roschaefer](https://github.com/roschaefer))
- Bump date-fns from 2.2.1 to 2.4.0 in /webapp [\#1752](https://github.com/Human-Connection/Human-Connection/pull/1752) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- fix: Github's security vulnerability warning [\#1751](https://github.com/Human-Connection/Human-Connection/pull/1751) ([roschaefer](https://github.com/roschaefer))
- update neo4j docker-compose config [\#1750](https://github.com/Human-Connection/Human-Connection/pull/1750) ([vbelolapotkov](https://github.com/vbelolapotkov))
- 🍰 Try to fix VSCode format works against ESLint [\#1749](https://github.com/Human-Connection/Human-Connection/pull/1749) ([Tirokk](https://github.com/Tirokk))
- Bump neo4j from 3.5.9 to 3.5.11 in /neo4j [\#1739](https://github.com/Human-Connection/Human-Connection/pull/1739) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump graphql from 14.5.7 to 14.5.8 in /backend [\#1738](https://github.com/Human-Connection/Human-Connection/pull/1738) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-youtube from 5.7.4 to 5.7.5 in /backend [\#1737](https://github.com/Human-Connection/Human-Connection/pull/1737) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Update to 0.1.1 [\#1734](https://github.com/Human-Connection/Human-Connection/pull/1734) ([roschaefer](https://github.com/roschaefer))
- Update maintenance page email to support@... [\#1732](https://github.com/Human-Connection/Human-Connection/pull/1732) ([mattwr18](https://github.com/mattwr18))
- Bump @babel/register from 7.6.0 to 7.6.2 in /backend [\#1730](https://github.com/Human-Connection/Human-Connection/pull/1730) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump graphql from 14.5.7 to 14.5.8 in /webapp [\#1729](https://github.com/Human-Connection/Human-Connection/pull/1729) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @babel/core from 7.6.0 to 7.6.2 in /backend [\#1728](https://github.com/Human-Connection/Human-Connection/pull/1728) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump eslint-loader from 3.0.0 to 3.0.1 in /webapp [\#1727](https://github.com/Human-Connection/Human-Connection/pull/1727) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump apollo-server-express from 2.9.3 to 2.9.4 in /backend [\#1726](https://github.com/Human-Connection/Human-Connection/pull/1726) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @babel/node from 7.6.1 to 7.6.2 in /backend [\#1725](https://github.com/Human-Connection/Human-Connection/pull/1725) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Fix bug UpdateComment, Fix styling on Comment [\#1719](https://github.com/Human-Connection/Human-Connection/pull/1719) ([mattwr18](https://github.com/mattwr18))
- Bump apollo-server-testing from 2.9.3 to 2.9.4 in /backend [\#1717](https://github.com/Human-Connection/Human-Connection/pull/1717) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-title from 5.7.4 to 5.7.5 in /backend [\#1715](https://github.com/Human-Connection/Human-Connection/pull/1715) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump date-fns from 2.2.1 to 2.3.0 in /backend [\#1714](https://github.com/Human-Connection/Human-Connection/pull/1714) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @babel/cli from 7.6.0 to 7.6.2 in /backend [\#1713](https://github.com/Human-Connection/Human-Connection/pull/1713) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- \[WIP\]1706 refactor shout spec [\#1712](https://github.com/Human-Connection/Human-Connection/pull/1712) ([aonomike](https://github.com/aonomike))
- 407 change your email address [\#1711](https://github.com/Human-Connection/Human-Connection/pull/1711) ([roschaefer](https://github.com/roschaefer))
- Remove repetitive labels from emote button [\#1702](https://github.com/Human-Connection/Human-Connection/pull/1702) ([roschaefer](https://github.com/roschaefer))
- fix the bug with scrolling post comments into view [\#1701](https://github.com/Human-Connection/Human-Connection/pull/1701) ([vbelolapotkov](https://github.com/vbelolapotkov))
- Bump metascraper-description from 5.7.4 to 5.7.5 in /backend [\#1700](https://github.com/Human-Connection/Human-Connection/pull/1700) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-logo from 5.7.4 to 5.7.5 in /backend [\#1698](https://github.com/Human-Connection/Human-Connection/pull/1698) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-video from 5.7.4 to 5.7.5 in /backend [\#1697](https://github.com/Human-Connection/Human-Connection/pull/1697) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @babel/core from 7.6.0 to 7.6.2 in /webapp [\#1696](https://github.com/Human-Connection/Human-Connection/pull/1696) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-image from 5.7.4 to 5.7.5 in /backend [\#1695](https://github.com/Human-Connection/Human-Connection/pull/1695) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @babel/preset-env from 7.6.0 to 7.6.2 in /webapp [\#1694](https://github.com/Human-Connection/Human-Connection/pull/1694) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-audio from 5.7.4 to 5.7.5 in /backend [\#1693](https://github.com/Human-Connection/Human-Connection/pull/1693) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump metascraper-url from 5.7.4 to 5.7.5 in /backend [\#1692](https://github.com/Human-Connection/Human-Connection/pull/1692) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bugfix create user page - missing submit buttons [\#1690](https://github.com/Human-Connection/Human-Connection/pull/1690) ([roschaefer](https://github.com/roschaefer))
- Remove sleep icon from comments list [\#1689](https://github.com/Human-Connection/Human-Connection/pull/1689) ([alina-beck](https://github.com/alina-beck))
- Configure docker to work with storybook [\#1688](https://github.com/Human-Connection/Human-Connection/pull/1688) ([mattwr18](https://github.com/mattwr18))
- Add Comment story, add spacing above user info [\#1685](https://github.com/Human-Connection/Human-Connection/pull/1685) ([mattwr18](https://github.com/mattwr18))
- Fix create account page has no logo, localisation [\#1681](https://github.com/Human-Connection/Human-Connection/pull/1681) ([roschaefer](https://github.com/roschaefer))
- Fix intermittent backend specs [\#1679](https://github.com/Human-Connection/Human-Connection/pull/1679) ([roschaefer](https://github.com/roschaefer))
- Improve comments output [\#1678](https://github.com/Human-Connection/Human-Connection/pull/1678) ([mattwr18](https://github.com/mattwr18))
- Fix intermittent failing test [\#1677](https://github.com/Human-Connection/Human-Connection/pull/1677) ([mattwr18](https://github.com/mattwr18))
- Bump graphql from 14.5.6 to 14.5.7 in /webapp [\#1676](https://github.com/Human-Connection/Human-Connection/pull/1676) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump graphql from 14.5.6 to 14.5.7 in /backend [\#1675](https://github.com/Human-Connection/Human-Connection/pull/1675) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump cookie-universal-nuxt from 2.0.17 to 2.0.18 in /webapp [\#1674](https://github.com/Human-Connection/Human-Connection/pull/1674) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @hapi/joi from 16.1.2 to 16.1.4 in /backend [\#1673](https://github.com/Human-Connection/Human-Connection/pull/1673) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump helmet from 3.21.0 to 3.21.1 in /backend [\#1672](https://github.com/Human-Connection/Human-Connection/pull/1672) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump cypress-file-upload from 3.3.3 to 3.3.4 [\#1671](https://github.com/Human-Connection/Human-Connection/pull/1671) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump codecov from 3.6.0 to 3.6.1 [\#1670](https://github.com/Human-Connection/Human-Connection/pull/1670) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Make Human Connection a Progressive Web App [\#1668](https://github.com/Human-Connection/Human-Connection/pull/1668) ([roschaefer](https://github.com/roschaefer))
- Remove contentExcerpt from comments [\#1667](https://github.com/Human-Connection/Human-Connection/pull/1667) ([mattwr18](https://github.com/mattwr18))
- Remove follow type enum [\#1660](https://github.com/Human-Connection/Human-Connection/pull/1660) ([vbelolapotkov](https://github.com/vbelolapotkov))
- 🍰 Notifications self update and refactoring [\#1658](https://github.com/Human-Connection/Human-Connection/pull/1658) ([Tirokk](https://github.com/Tirokk))
- Bump mustache from 3.0.3 to 3.1.0 in /backend [\#1655](https://github.com/Human-Connection/Human-Connection/pull/1655) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @hapi/joi from 16.1.1 to 16.1.2 in /backend [\#1654](https://github.com/Human-Connection/Human-Connection/pull/1654) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @nuxtjs/apollo from 4.0.0-rc13 to 4.0.0-rc13.1 in /webapp [\#1653](https://github.com/Human-Connection/Human-Connection/pull/1653) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump codecov from 3.5.0 to 3.6.0 [\#1652](https://github.com/Human-Connection/Human-Connection/pull/1652) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Change your own slug [\#1651](https://github.com/Human-Connection/Human-Connection/pull/1651) ([roschaefer](https://github.com/roschaefer))
- Fix bug where short comments scrub links [\#1649](https://github.com/Human-Connection/Human-Connection/pull/1649) ([mattwr18](https://github.com/mattwr18))
- Fix styling issue in comments list [\#1648](https://github.com/Human-Connection/Human-Connection/pull/1648) ([mattwr18](https://github.com/mattwr18))
- Provider list approval hard cut [\#1647](https://github.com/Human-Connection/Human-Connection/pull/1647) ([ogerly](https://github.com/ogerly))
- Point the changelog to Github [\#1646](https://github.com/Human-Connection/Human-Connection/pull/1646) ([roschaefer](https://github.com/roschaefer))
- Bump eslint-plugin-prettier from 3.1.0 to 3.1.1 in /webapp [\#1643](https://github.com/Human-Connection/Human-Connection/pull/1643) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump eslint-plugin-prettier from 3.1.0 to 3.1.1 in /backend [\#1642](https://github.com/Human-Connection/Human-Connection/pull/1642) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Fix \#1639 - No limits for post length [\#1641](https://github.com/Human-Connection/Human-Connection/pull/1641) ([roschaefer](https://github.com/roschaefer))
- 1486 make slug more usable [\#1640](https://github.com/Human-Connection/Human-Connection/pull/1640) ([roschaefer](https://github.com/roschaefer))
- Update de.json [\#1636](https://github.com/Human-Connection/Human-Connection/pull/1636) ([datenbrei](https://github.com/datenbrei))
- Exclude broken maintenance-worker docker image [\#1635](https://github.com/Human-Connection/Human-Connection/pull/1635) ([roschaefer](https://github.com/roschaefer))
- Fix bug where about must not be empty string [\#1630](https://github.com/Human-Connection/Human-Connection/pull/1630) ([mattwr18](https://github.com/mattwr18))
- Bump @storybook/addon-a11y from 5.2.0 to 5.2.1 in /webapp [\#1627](https://github.com/Human-Connection/Human-Connection/pull/1627) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @storybook/addon-actions from 5.2.0 to 5.2.1 in /webapp [\#1625](https://github.com/Human-Connection/Human-Connection/pull/1625) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @storybook/vue from 5.2.0 to 5.2.1 in /webapp [\#1624](https://github.com/Human-Connection/Human-Connection/pull/1624) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump cross-env from 5.2.1 to 6.0.0 in /backend [\#1623](https://github.com/Human-Connection/Human-Connection/pull/1623) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump @hapi/joi from 16.0.1 to 16.1.1 in /backend [\#1622](https://github.com/Human-Connection/Human-Connection/pull/1622) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump cross-env from 5.2.1 to 6.0.0 [\#1621](https://github.com/Human-Connection/Human-Connection/pull/1621) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- 1612 refactor moderator spec [\#1620](https://github.com/Human-Connection/Human-Connection/pull/1620) ([aonomike](https://github.com/aonomike))
- Fix disappearing embeds on comments [\#1618](https://github.com/Human-Connection/Human-Connection/pull/1618) ([mattwr18](https://github.com/mattwr18))
- links\_to\_imprint\_and\_privacy\_policy\_changed\_to\_human-connection.org [\#1615](https://github.com/Human-Connection/Human-Connection/pull/1615) ([ogerly](https://github.com/ogerly))
- Bump metascraper-author from 5.6.6 to 5.7.4 in /backend [\#1610](https://github.com/Human-Connection/Human-Connection/pull/1610) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Configure emails [\#1599](https://github.com/Human-Connection/Human-Connection/pull/1599) ([alina-beck](https://github.com/alina-beck))
- Improve follow/ufollow mutation [\#1596](https://github.com/Human-Connection/Human-Connection/pull/1596) ([vbelolapotkov](https://github.com/vbelolapotkov))
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*

View File

@ -50,17 +50,17 @@ But what do we do when waiting for merge into master \(wanting to keep PRs small
* but what about when you are waiting for merge?
* solutions
* 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
* 2\) prefer to leave exiting PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
* 2\) prefer to leave existing PR until it can be reviewed, and instead go and work on some other part of the codebase that is not impacted by the first PR
### Code Review
* Github setting in place - at least one review is required to merge
- in principle anyone (who is not the PR owner) can review
- but often it will be the core developers (Robert, Ulf, Greg, Wolfgang?)
- but often it will be the core developers (Robert, Wolfgang, Matt, Alina, Alex)
- once there is a review, and presuming no requested changes, PR opener can merge
* CI/tests
- the CI needs to pass
- linting <-- autofix?
- linting (yarn lint --fix)
- tests (unit, feature) (backend, frontend)
- codecoverage

View File

@ -1 +1 @@
0.1.0
0.1.3

View File

@ -72,6 +72,8 @@ To reset the database run:
$ docker-compose exec backend yarn run db:reset
# you could also wipe out your neo4j database and delete all volumes with:
$ docker-compose down -v
# if container is not running, run this command to set up your database indeces and contstraints
$ docker-compose run neo4j db_setup
```
{% endtab %}
@ -88,7 +90,6 @@ $ yarn run db:reset
{% endtab %}
{% endtabs %}
# Testing
**Beware**: We have no multiple database setup at the moment. We clean the

View File

@ -41,30 +41,29 @@
]
},
"dependencies": {
"@hapi/joi": "^16.1.2",
"@hapi/joi": "^16.1.7",
"@sentry/node": "^5.6.2",
"activitystrea.ms": "~2.1.3",
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"apollo-link-context": "~1.0.19",
"apollo-link-http": "~1.5.16",
"apollo-server": "~2.9.3",
"apollo-server-express": "^2.9.0",
"apollo-server": "~2.9.4",
"apollo-server-express": "^2.9.4",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~6.0.0",
"date-fns": "2.3.0",
"cross-env": "~6.0.3",
"date-fns": "2.4.1",
"debug": "~4.1.1",
"dotenv": "~8.1.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.5.7",
"graphql": "^14.5.8",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
"graphql-middleware": "~3.0.5",
"graphql-middleware-sentry": "^3.2.0",
"graphql-middleware": "~4.0.1",
"graphql-middleware-sentry": "^3.2.1",
"graphql-shield": "~6.1.0",
"graphql-tag": "~2.10.1",
"helmet": "~3.21.1",
@ -73,23 +72,23 @@
"lodash": "~4.17.14",
"merge-graphql-schemas": "^1.7.0",
"metascraper": "^4.10.3",
"metascraper-audio": "^5.7.5",
"metascraper-author": "^5.7.4",
"metascraper-audio": "^5.7.6",
"metascraper-author": "^5.7.6",
"metascraper-clearbit-logo": "^5.3.0",
"metascraper-date": "^5.7.4",
"metascraper-description": "^5.7.4",
"metascraper-image": "^5.7.5",
"metascraper-lang": "^5.7.4",
"metascraper-date": "^5.7.6",
"metascraper-description": "^5.7.6",
"metascraper-image": "^5.7.6",
"metascraper-lang": "^5.7.6",
"metascraper-lang-detector": "^4.8.5",
"metascraper-logo": "^5.7.5",
"metascraper-publisher": "^5.7.4",
"metascraper-soundcloud": "^5.7.4",
"metascraper-title": "^5.7.4",
"metascraper-url": "^5.7.5",
"metascraper-video": "^5.7.5",
"metascraper-youtube": "^5.7.4",
"metascraper-logo": "^5.7.6",
"metascraper-publisher": "^5.7.6",
"metascraper-soundcloud": "^5.7.6",
"metascraper-title": "^5.7.6",
"metascraper-url": "^5.7.6",
"metascraper-video": "^5.7.6",
"metascraper-youtube": "^5.7.6",
"minimatch": "^3.0.4",
"mustache": "^3.0.3",
"mustache": "^3.1.0",
"neo4j-driver": "~1.7.6",
"neo4j-graphql-js": "^2.7.2",
"neode": "^0.3.3",
@ -106,20 +105,20 @@
"xregexp": "^4.2.4"
},
"devDependencies": {
"@babel/cli": "~7.6.0",
"@babel/core": "~7.6.0",
"@babel/node": "~7.6.1",
"@babel/cli": "~7.6.2",
"@babel/core": "~7.6.2",
"@babel/node": "~7.6.2",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/preset-env": "~7.6.0",
"@babel/register": "~7.6.0",
"apollo-server-testing": "~2.9.3",
"@babel/preset-env": "~7.6.2",
"@babel/register": "~7.6.2",
"apollo-server-testing": "~2.9.4",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.0.3",
"babel-jest": "~24.9.0",
"chai": "~4.2.0",
"cucumber": "~5.1.0",
"eslint": "~6.4.0",
"eslint-config-prettier": "~6.3.0",
"eslint": "~6.5.1",
"eslint-config-prettier": "~6.4.0",
"eslint-config-standard": "~14.1.0",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.17.0",
@ -129,7 +128,7 @@
"eslint-plugin-standard": "~4.0.1",
"graphql-request": "~1.8.2",
"jest": "~24.9.0",
"nodemon": "~1.19.2",
"nodemon": "~1.19.3",
"prettier": "~1.18.2",
"supertest": "~4.0.2"
}

View File

@ -1,7 +1,9 @@
import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
// import { extractDomainFromUrl, signAndSend } from './utils'
import { extractNameFromId, signAndSend } from './utils'
import { isPublicAddressed } from './utils/activity'
// import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
import request from 'request'
import as from 'activitystrea.ms'
// import as from 'activitystrea.ms'
import NitroDataSource from './NitroDataSource'
import router from './routes'
import Collections from './Collections'
@ -33,71 +35,71 @@ export default class ActivityPub {
}
}
handleFollowActivity(activity) {
debug(`inside FOLLOW ${activity.actor}`)
const toActorName = extractNameFromId(activity.object)
const fromDomain = extractDomainFromUrl(activity.actor)
const dataSource = this.dataSource
// handleFollowActivity(activity) {
// debug(`inside FOLLOW ${activity.actor}`)
// const toActorName = extractNameFromId(activity.object)
// const fromDomain = extractDomainFromUrl(activity.actor)
// const dataSource = this.dataSource
return new Promise((resolve, reject) => {
request(
{
url: activity.actor,
headers: {
Accept: 'application/activity+json',
},
},
async (err, response, toActorObject) => {
if (err) return reject(err)
// save shared inbox
toActorObject = JSON.parse(toActorObject)
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
// return new Promise((resolve, reject) => {
// request(
// {
// url: activity.actor,
// headers: {
// Accept: 'application/activity+json',
// },
// },
// async (err, response, toActorObject) => {
// if (err) return reject(err)
// // save shared inbox
// toActorObject = JSON.parse(toActorObject)
// await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
activity.object,
)
// const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
// activity.object,
// )
const followActivity = as
.follow()
.id(activity.id)
.actor(activity.actor)
.object(activity.object)
// const followActivity = as
// .follow()
// .id(activity.id)
// .actor(activity.actor)
// .object(activity.object)
// add follower if not already in collection
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
debug('follower already in collection!')
debug(`inbox = ${toActorObject.inbox}`)
resolve(
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
} else {
followersCollectionPage.orderedItems.push(activity.actor)
}
debug(`toActorObject = ${toActorObject}`)
toActorObject =
typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
debug(`inbox = ${toActorObject.inbox}`)
debug(`outbox = ${toActorObject.outbox}`)
debug(`followers = ${toActorObject.followers}`)
debug(`following = ${toActorObject.following}`)
// // add follower if not already in collection
// if (followersCollectionPage.orderedItems.includes(activity.actor)) {
// debug('follower already in collection!')
// debug(`inbox = ${toActorObject.inbox}`)
// resolve(
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
// )
// } else {
// followersCollectionPage.orderedItems.push(activity.actor)
// }
// debug(`toActorObject = ${toActorObject}`)
// toActorObject =
// typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
// debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
// debug(`inbox = ${toActorObject.inbox}`)
// debug(`outbox = ${toActorObject.outbox}`)
// debug(`followers = ${toActorObject.followers}`)
// debug(`following = ${toActorObject.following}`)
try {
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
debug('follow activity saved')
resolve(
sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
} catch (e) {
debug('followers update error!', e)
resolve(
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
)
}
},
)
})
}
// try {
// await dataSource.saveFollowersCollectionPage(followersCollectionPage)
// debug('follow activity saved')
// resolve(
// sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
// )
// } catch (e) {
// debug('followers update error!', e)
// resolve(
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
// )
// }
// },
// )
// })
// }
handleUndoActivity(activity) {
debug('inside UNDO')

View File

@ -18,9 +18,9 @@ router.post('/', async function(req, res, next) {
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
// case 'Follow':
// await activityPub.handleFollowActivity(req.body).catch(next)
// break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break

View File

@ -56,9 +56,9 @@ router.post('/:name/inbox', verify, async function(req, res, next) {
case 'Undo':
await activityPub.handleUndoActivity(req.body).catch(next)
break
case 'Follow':
await activityPub.handleFollowActivity(req.body).catch(next)
break
// case 'Follow':
// await activityPub.handleFollowActivity(req.body).catch(next)
// break
case 'Delete':
await activityPub.handleDeleteActivity(req.body).catch(next)
break

View File

@ -1,10 +1,11 @@
import { activityPub } from '../ActivityPub'
import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
import { throwErrorIfApolloErrorOccurred } from './index'
// import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
import crypto from 'crypto'
import as from 'activitystrea.ms'
// import as from 'activitystrea.ms'
import gql from 'graphql-tag'
const debug = require('debug')('ea:utils:activity')
// const debug = require('debug')('ea:utils:activity')
export function createNoteObject(text, name, id, published) {
const createUuid = crypto.randomBytes(16).toString('hex')
@ -62,41 +63,41 @@ export async function getActorId(name) {
}
}
export function sendAcceptActivity(theBody, name, targetDomain, url) {
as.accept()
.id(
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
crypto.randomBytes(16).toString('hex'),
)
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
if (!err) {
return signAndSend(doc, name, targetDomain, url)
} else {
debug(`error serializing Accept object: ${err}`)
throw new Error('error serializing Accept object')
}
})
}
// export function sendAcceptActivity(theBody, name, targetDomain, url) {
// as.accept()
// .id(
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
// crypto.randomBytes(16).toString('hex'),
// )
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
// .object(theBody)
// .prettyWrite((err, doc) => {
// if (!err) {
// return signAndSend(doc, name, targetDomain, url)
// } else {
// debug(`error serializing Accept object: ${err}`)
// throw new Error('error serializing Accept object')
// }
// })
// }
export function sendRejectActivity(theBody, name, targetDomain, url) {
as.reject()
.id(
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
crypto.randomBytes(16).toString('hex'),
)
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
.object(theBody)
.prettyWrite((err, doc) => {
if (!err) {
return signAndSend(doc, name, targetDomain, url)
} else {
debug(`error serializing Accept object: ${err}`)
throw new Error('error serializing Accept object')
}
})
}
// export function sendRejectActivity(theBody, name, targetDomain, url) {
// as.reject()
// .id(
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
// crypto.randomBytes(16).toString('hex'),
// )
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
// .object(theBody)
// .prettyWrite((err, doc) => {
// if (!err) {
// return signAndSend(doc, name, targetDomain, url)
// } else {
// debug(`error serializing Accept object: ${err}`)
// throw new Error('error serializing Accept object')
// }
// })
// }
export function isPublicAddressed(postObject) {
if (typeof postObject.to === 'string') {

View File

@ -14,6 +14,7 @@ export default async (driver, authorizationHeader) => {
const session = driver.session()
const query = `
MATCH (user:User {id: $id, deleted: false, disabled: false })
SET user.lastActiveAt = toString(datetime())
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
LIMIT 1
`

View File

@ -1,9 +1,10 @@
import Factory from '../seed/factories/index'
import { getDriver } from '../bootstrap/neo4j'
import { getDriver, neode as getNeode } from '../bootstrap/neo4j'
import decode from './decode'
const factory = Factory()
const driver = getDriver()
const neode = getNeode()
// here is the decoded JWT token:
// {
@ -85,6 +86,33 @@ describe('decode', () => {
})
})
it('sets `lastActiveAt`', async () => {
let user = await neode.first('User', { id: 'u3' })
await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt')
await decode(driver, authorizationHeader)
user = await neode.first('User', { id: 'u3' })
await expect(user.toJson()).resolves.toMatchObject({
lastActiveAt: expect.any(String),
})
})
it('updates `lastActiveAt` for every authenticated request', async () => {
let user = await neode.first('User', { id: 'u3' })
await user.update({
updatedAt: new Date().toISOString(),
lastActiveAt: '2019-10-03T23:33:08.598Z',
})
await expect(user.toJson()).resolves.toMatchObject({
lastActiveAt: '2019-10-03T23:33:08.598Z',
})
await decode(driver, authorizationHeader)
user = await neode.first('User', { id: 'u3' })
await expect(user.toJson()).resolves.toMatchObject({
// should be a different time by now ;)
lastActiveAt: expect.not.stringContaining('2019-10-03T23:33'),
})
})
describe('but user is deleted', () => {
beforeEach(async () => {
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
@ -92,6 +120,7 @@ describe('decode', () => {
it('returns null', returnsNull)
})
describe('but user is disabled', () => {
beforeEach(async () => {
await user.update({ updatedAt: new Date().toISOString(), disabled: true })

View File

@ -1,51 +1,51 @@
import { generateRsaKeyPair } from '../activitypub/security'
import { activityPub } from '../activitypub/ActivityPub'
import as from 'activitystrea.ms'
// import as from 'activitystrea.ms'
const debug = require('debug')('backend:schema')
// const debug = require('debug')('backend:schema')
export default {
Mutation: {
CreatePost: async (resolve, root, args, context, info) => {
args.activityId = activityPub.generateStatusId(context.user.slug)
args.objectId = activityPub.generateStatusId(context.user.slug)
// CreatePost: async (resolve, root, args, context, info) => {
// args.activityId = activityPub.generateStatusId(context.user.slug)
// args.objectId = activityPub.generateStatusId(context.user.slug)
const post = await resolve(root, args, context, info)
// const post = await resolve(root, args, context, info)
const { user: author } = context
const actorId = author.actorId
debug(`actorId = ${actorId}`)
const createActivity = await new Promise((resolve, reject) => {
as.create()
.id(`${actorId}/status/${args.activityId}`)
.actor(`${actorId}`)
.object(
as
.article()
.id(`${actorId}/status/${post.id}`)
.content(post.content)
.to('https://www.w3.org/ns/activitystreams#Public')
.publishedNow()
.attributedTo(`${actorId}`),
)
.prettyWrite((err, doc) => {
if (err) {
reject(err)
} else {
debug(doc)
const parsedDoc = JSON.parse(doc)
parsedDoc.send = true
resolve(JSON.stringify(parsedDoc))
}
})
})
try {
await activityPub.sendActivity(createActivity)
} catch (e) {
debug(`error sending post activity\n${e}`)
}
return post
},
// const { user: author } = context
// const actorId = author.actorId
// debug(`actorId = ${actorId}`)
// const createActivity = await new Promise((resolve, reject) => {
// as.create()
// .id(`${actorId}/status/${args.activityId}`)
// .actor(`${actorId}`)
// .object(
// as
// .article()
// .id(`${actorId}/status/${post.id}`)
// .content(post.content)
// .to('https://www.w3.org/ns/activitystreams#Public')
// .publishedNow()
// .attributedTo(`${actorId}`),
// )
// .prettyWrite((err, doc) => {
// if (err) {
// reject(err)
// } else {
// debug(doc)
// const parsedDoc = JSON.parse(doc)
// parsedDoc.send = true
// resolve(JSON.stringify(parsedDoc))
// }
// })
// })
// try {
// await activityPub.sendActivity(createActivity)
// } catch (e) {
// debug(`error sending post activity\n${e}`)
// }
// return post
// },
SignupVerification: async (resolve, root, args, context, info) => {
const keys = generateRsaKeyPair()
Object.assign(args, keys)

View File

@ -5,7 +5,8 @@ import {
signupTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
} from './templates/templateBuilder'
emailVerificationTemplate,
} from './templateBuilder'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
@ -21,7 +22,7 @@ if (!hasEmailConfig) {
const transporter = nodemailer.createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS === 'true',
secure: false, // true for 465, false for other ports
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,
@ -57,8 +58,17 @@ const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo)
return true
}
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce, name } = response
await sendMail(emailVerificationTemplate({ email, nonce, name }))
delete response.nonce
return response
}
export default {
Mutation: {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail,
SignupByInvitation: sendSignupMail,

View File

@ -0,0 +1,77 @@
import mustache from 'mustache'
import CONFIG from '../../config'
import * as templates from './templates'
const from = '"Human Connection" <info@human-connection.org>'
const supportUrl = 'https://human-connection.org/en/contact'
export const signupTemplate = ({ email, nonce }) => {
const subject = 'Willkommen, Bienvenue, Welcome to Human Connection!'
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, supportUrl, subject },
{ content: templates.signup },
),
}
}
export const emailVerificationTemplate = ({ email, nonce, name }) => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, name, nonce, supportUrl, subject },
{ content: templates.emailVerification },
),
}
}
export const resetPasswordTemplate = ({ email, nonce, name }) => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, name, nonce, supportUrl, subject },
{ content: templates.passwordReset },
),
}
}
export const wrongAccountTemplate = ({ email }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, supportUrl },
{ content: templates.wrongAccount },
),
}
}

View File

@ -0,0 +1,186 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }}!</h1>
<p style="margin: 0;">Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button
kannst Du Deine neue E-Mail Adresse bestätigen:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">E-Mail
Adresse
bestätigen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht
einfach ignorieren. Mlde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
unserem Support Team</a>, wenn du noch Fragen hast!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein Human Connection Team</p>
</td>
</tr>
<tr>
<td style="display: none;">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }}!</h1>
<p style="margin: 0;">So, you want to change your e-mail? No problem! Just click the button below to verify
your new address:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Verify
e-mail address</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you don't want to change your e-mail address feel free to ignore this message. You
can
also <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> if you have any questions!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The Human Connection Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -0,0 +1,11 @@
import fs from 'fs'
import path from 'path'
const readFile = fileName => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const signup = readFile('./signup.html')
export const passwordReset = readFile('./resetPassword.html')
export const wrongAccount = readFile('./wrongAccount.html')
export const emailVerification = readFile('./emailVerification.html')
export const layout = readFile('./layout.html')

View File

@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>{{ subject }}</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<p style="color:#19c243; font-style: italic; font-family: Lato, sans-serif; font-size: 16px; padding-top: 20px;">English version below!</p>
{{> content}}
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@ -1,233 +1,6 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Neues Passwort | Reset Password</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
width="100%" style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
@ -258,8 +31,7 @@
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
style="margin: auto;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -299,8 +71,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
@ -318,12 +89,12 @@
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
width="100%" style="margin: auto;">
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
@ -358,8 +129,7 @@
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
style="margin: auto;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -398,8 +168,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
@ -412,37 +181,5 @@
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>
</table>
<!-- Email Body English : END -->

View File

@ -1,243 +1,6 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Willkommen, Bienvenue, Welcome to Human Connection</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<!-- VISUALLY HIDDEN PRE-HEADER TEXT -->
<div
style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
Dein Anmeldelink. | Here is your signup link.
</div>
<div
style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;<br>
</div>
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
width="100%" style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
@ -269,8 +32,7 @@
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
style="margin: auto;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -297,8 +59,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Falls Du Dich nicht selbst bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> angemeldet hast, schau doch mal vorbei!
Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.</p>
@ -321,8 +82,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
unserem Support Team</a>, wenn Du Fragen hast.</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
@ -340,12 +100,12 @@
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
width="100%" style="margin: auto;">
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
@ -381,8 +141,7 @@
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
style="margin: auto;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -409,8 +168,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you didn't sign up for <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> we recommend you to check it out!
It's a social network from people for people who want to connect and change the world together.</p>
@ -434,8 +192,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> with any
questions you have.</p>
@ -449,37 +206,5 @@
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>
</table>
<!-- Email Body English : END -->

View File

@ -1,48 +0,0 @@
import fs from 'fs'
import path from 'path'
import mustache from 'mustache'
import CONFIG from '../../../config'
const from = '"Human Connection" <info@human-connection.org>'
const supportUrl = 'https://human-connection.org/en/contact'
const signupHtml = fs.readFileSync(path.join(__dirname, './signup.html'), 'utf-8')
const passwordResetHtml = fs.readFileSync(path.join(__dirname, './resetPassword.html'), 'utf-8')
const wrongAccountHtml = fs.readFileSync(path.join(__dirname, './wrongAccount.html'), 'utf-8')
export const signupTemplate = ({ email, nonce }) => {
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject: 'Willkommen, Bienvenue, Welcome to Human Connection!',
html: mustache.render(signupHtml, { actionUrl, supportUrl }),
}
}
export const resetPasswordTemplate = ({ email, nonce, name }) => {
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject: 'Neues Passwort | Reset Password',
html: mustache.render(passwordResetHtml, { actionUrl, name, nonce, supportUrl }),
}
}
export const wrongAccountTemplate = ({ email }) => {
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
from,
to: email,
subject: 'Falsche Mailadresse? | Wrong E-mail?',
html: mustache.render(wrongAccountHtml, { actionUrl, supportUrl }),
}
}

View File

@ -1,233 +1,6 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Falsche Mailadresse? | Wrong E-mail?</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
width="100%" style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
@ -259,8 +32,7 @@
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
style="margin: auto;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -282,8 +54,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Wenn Du noch keinen Account bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> hast oder Dein Password gar nicht ändern willst,
kannst Du diese E-Mail einfach ignorieren!</p>
@ -318,12 +89,12 @@
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
width="100%" style="margin: auto;">
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
@ -358,8 +129,7 @@
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0"
style="margin: auto;">
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -381,8 +151,7 @@
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you don't have an account at <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> yet or if you didn't want to reset your password,
please ignore this e-mail.</p>
@ -412,37 +181,5 @@
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>
</table>
<!-- Email Body English : END -->

View File

@ -170,6 +170,8 @@ const permissions = shield(
block: isAuthenticated,
unblock: isAuthenticated,
markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,
},
User: {
email: isMyOwn,

View File

@ -0,0 +1,12 @@
module.exports = {
email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true },
belongsTo: {
type: 'relationship',
relationship: 'BELONGS_TO',
target: 'User',
direction: 'out',
eager: true,
},
}

View File

@ -49,6 +49,7 @@ module.exports = {
direction: 'in',
},
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', direction: 'in' },
lastActiveAt: { type: 'string', isoDate: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
updatedAt: {
type: 'string',

View File

@ -5,6 +5,7 @@ export default {
User: require('./User.js'),
InvitationCode: require('./InvitationCode.js'),
EmailAddress: require('./EmailAddress.js'),
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Comment: require('./Comment.js'),

View File

@ -0,0 +1,92 @@
import generateNonce from './helpers/generateNonce'
import Resolver from './helpers/Resolver'
import existingEmailAddress from './helpers/existingEmailAddress'
import { UserInputError } from 'apollo-server'
import Validator from 'neode/build/Services/Validator.js'
export default {
Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response
try {
const { neode } = context
await new Validator(neode, neode.model('UnverifiedEmailAddress'), args)
} catch (e) {
throw new UserInputError('must be a valid email')
}
// check email does not belong to anybody
await existingEmailAddress(_parent, args, context)
const nonce = generateNonce()
const {
user: { id: userId },
} = context
const { email } = args
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async txc => {
const result = await txc.run(
`
MATCH (user:User {id: $userId})
MERGE (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce})
SET email.createdAt = toString(datetime())
RETURN email, user
`,
{ userId, email, nonce },
)
return result.records.map(record => ({
name: record.get('user').properties.name,
...record.get('email').properties,
}))
})
try {
const txResult = await writeTxResultPromise
response = txResult[0]
} finally {
session.close()
}
return response
},
VerifyEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response
const {
user: { id: userId },
} = context
const { nonce, email } = args
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async txc => {
const result = await txc.run(
`
MATCH (user:User {id: $userId})-[:PRIMARY_EMAIL]->(previous:EmailAddress)
MATCH (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce})
MERGE (user)-[:PRIMARY_EMAIL]->(email)
SET email:EmailAddress
SET email.verifiedAt = toString(datetime())
REMOVE email:UnverifiedEmailAddress
DETACH DELETE previous
RETURN email
`,
{ userId, email, nonce },
)
return result.records.map(record => record.get('email').properties)
})
try {
const txResult = await writeTxResultPromise
response = txResult[0]
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('A user account with this email already exists.')
throw new Error(e)
} finally {
session.close()
}
if (!response) throw new UserInputError('Invalid nonce or no email address found.')
return response
},
},
EmailAddress: {
...Resolver('EmailAddress', {
undefinedToNull: ['verifiedAt'],
}),
},
}

View File

@ -0,0 +1,298 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
const neode = getNeode()
let mutate
let authenticatedUser
let user
let variables
const driver = getDriver()
beforeEach(async () => {
variables = {}
})
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('AddEmailAddress', () => {
const mutation = gql`
mutation($email: String!) {
AddEmailAddress(email: $email) {
email
verifiedAt
createdAt
}
}
`
beforeEach(() => {
variables = { ...variables, email: 'new-email@example.org' }
})
describe('unauthenticated', () => {
beforeEach(() => {
authenticatedUser = null
})
it('throws AuthorizationError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: '567', email: 'user@example.org' })
authenticatedUser = await user.toJson()
})
describe('email attribute is not a valid email', () => {
beforeEach(() => {
variables = { ...variables, email: 'foobar' }
})
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'must be a valid email' }],
})
})
})
describe('email attribute is a valid email', () => {
it('creates a new unverified `EmailAddress` node', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: {
AddEmailAddress: {
email: 'new-email@example.org',
verifiedAt: null,
createdAt: expect.any(String),
},
},
errors: undefined,
})
})
it('connects `UnverifiedEmailAddress` to the authenticated user', async () => {
await mutate({ mutation, variables })
const result = await neode.cypher(`
MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"})
MATCH(u:User)<-[:BELONGS_TO]-(e:UnverifiedEmailAddress {email: "new-email@example.org"})
RETURN e
`)
const email = neode.hydrateFirst(result, 'e', neode.model('UnverifiedEmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'new-email@example.org',
nonce: expect.any(String),
})
})
describe('if another `UnverifiedEmailAddress` node already exists with that email', () => {
it('throws no unique constraint violation error', async () => {
await factory.create('UnverifiedEmailAddress', {
createdAt: '2019-09-24T14:00:01.565Z',
email: 'new-email@example.org',
})
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: {
AddEmailAddress: {
email: 'new-email@example.org',
verifiedAt: null,
},
},
errors: undefined,
})
})
})
describe('but if another user owns an `EmailAddress` already with that email', () => {
it('throws UserInputError because of unique constraints', async () => {
await factory.create('User', { email: 'new-email@example.org' })
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
})
})
})
describe('VerifyEmailAddress', () => {
const mutation = gql`
mutation($email: String!, $nonce: String!) {
VerifyEmailAddress(email: $email, nonce: $nonce) {
email
createdAt
verifiedAt
}
}
`
beforeEach(() => {
variables = { ...variables, email: 'to-be-verified@example.org', nonce: '123456' }
})
describe('unauthenticated', () => {
beforeEach(() => {
authenticatedUser = null
})
it('throws AuthorizationError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: '567', email: 'user@example.org' })
authenticatedUser = await user.toJson()
})
describe('if no unverified `EmailAddress` node exists', () => {
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Invalid nonce or no email address found.' }],
})
})
})
describe('given a `UnverifiedEmailAddress`', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await factory.create('UnverifiedEmailAddress', {
nonce: 'abcdef',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
})
})
describe('given invalid nonce', () => {
it('throws UserInputError', async () => {
variables.nonce = 'asdfgh'
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Invalid nonce or no email address found.' }],
})
})
})
describe('given valid nonce for `UnverifiedEmailAddress` node', () => {
beforeEach(() => {
variables = { ...variables, nonce: 'abcdef' }
})
describe('but the address does not belong to the authenticated user', () => {
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Invalid nonce or no email address found.' }],
})
})
})
describe('and the `UnverifiedEmailAddress` belongs to the authenticated user', () => {
beforeEach(async () => {
await emailAddress.relateTo(user, 'belongsTo')
})
it('adds `verifiedAt`', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: {
VerifyEmailAddress: {
email: 'to-be-verified@example.org',
verifiedAt: expect.any(String),
createdAt: expect.any(String),
},
},
errors: undefined,
})
})
it('connects the new `EmailAddress` as PRIMARY', async () => {
await mutate({ mutation, variables })
const result = await neode.cypher(`
MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"})
RETURN e
`)
const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'to-be-verified@example.org',
})
})
it('removes previous PRIMARY relationship', async () => {
const cypherStatement = `
MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "user@example.org"})
RETURN e
`
let result = await neode.cypher(cypherStatement)
let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'user@example.org',
})
await mutate({ mutation, variables })
result = await neode.cypher(cypherStatement)
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email).toBe(false)
})
it('removes previous `EmailAddress` node', async () => {
const cypherStatement = `
MATCH(u:User {id: "567"})<-[:BELONGS_TO]-(e:EmailAddress {email: "user@example.org"})
RETURN e
`
let result = await neode.cypher(cypherStatement)
let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'user@example.org',
})
await mutate({ mutation, variables })
result = await neode.cypher(cypherStatement)
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email).toBe(false)
})
describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email', () => {
beforeEach(async () => {
await factory.create('EmailAddress', { email: 'to-be-verified@example.org' })
})
it('throws UserInputError because of unique constraints', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
})
})
})
})
})

View File

@ -0,0 +1,26 @@
import { UserInputError } from 'apollo-server'
export default async function alreadyExistingMail(_parent, args, context) {
let { email } = args
email = email.toLowerCase()
const cypher = `
MATCH (email:EmailAddress {email: $email})
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
RETURN email, user
`
let transactionRes
const session = context.driver.session()
try {
transactionRes = await session.run(cypher, { email })
} finally {
session.close()
}
const [result] = transactionRes.records.map(record => {
return {
alreadyExistingEmail: record.get('email').properties,
user: record.get('user') && record.get('user').properties,
}
})
const { alreadyExistingEmail, user } = result || {}
if (user) throw new UserInputError('A user account with this email already exists.')
return alreadyExistingEmail
}

View File

@ -0,0 +1,4 @@
import uuid from 'uuid/v4'
export default function generateNonce() {
return uuid().substring(0, 6)
}

View File

@ -1,41 +1,16 @@
import { UserInputError } from 'apollo-server'
import uuid from 'uuid/v4'
import { neode } from '../../bootstrap/neo4j'
import fileUpload from './fileUpload'
import encryptPassword from '../../helpers/encryptPassword'
import generateNonce from './helpers/generateNonce'
import existingEmailAddress from './helpers/existingEmailAddress'
const instance = neode()
const alreadyExistingMail = async (_parent, args, context) => {
let { email } = args
email = email.toLowerCase()
const cypher = `
MATCH (email:EmailAddress {email: $email})
OPTIONAL MATCH (email)-[:PRIMARY_EMAIL]-(user)
RETURN email, user
`
let transactionRes
const session = context.driver.session()
try {
transactionRes = await session.run(cypher, { email })
} finally {
session.close()
}
const [result] = transactionRes.records.map(record => {
return {
alreadyExistingEmail: record.get('email').properties,
user: record.get('user') && record.get('user').properties,
}
})
const { alreadyExistingEmail, user } = result || {}
if (user) throw new UserInputError('User account with this email already exists.')
return alreadyExistingEmail
}
export default {
Mutation: {
CreateInvitationCode: async (_parent, args, context, _resolveInfo) => {
args.token = uuid().substring(0, 6)
args.token = generateNonce()
const {
user: { id: userId },
} = context
@ -54,9 +29,9 @@ export default {
return response
},
Signup: async (_parent, args, context) => {
const nonce = uuid().substring(0, 6)
const nonce = generateNonce()
args.nonce = nonce
let emailAddress = await alreadyExistingMail(_parent, args, context)
let emailAddress = await existingEmailAddress(_parent, args, context)
if (emailAddress) return emailAddress
try {
emailAddress = await instance.create('EmailAddress', args)
@ -67,9 +42,9 @@ export default {
},
SignupByInvitation: async (_parent, args, context) => {
const { token } = args
const nonce = uuid().substring(0, 6)
const nonce = generateNonce()
args.nonce = nonce
let emailAddress = await alreadyExistingMail(_parent, args, context)
let emailAddress = await existingEmailAddress(_parent, args, context)
if (emailAddress) return emailAddress
try {
const result = await instance.cypher(

View File

@ -257,7 +257,7 @@ describe('SignupByInvitation', () => {
it('throws unique violation error', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'User account with this email already exists.' }],
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
@ -307,6 +307,7 @@ describe('Signup', () => {
it('is allowed to signup users by email', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { Signup: { email: 'someuser@example.org' } },
errors: undefined,
})
})
@ -342,7 +343,7 @@ describe('Signup', () => {
it('throws UserInputError error because of unique constraint violation', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'User account with this email already exists.' }],
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
@ -351,6 +352,7 @@ describe('Signup', () => {
it('resolves with the already existing email', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { Signup: { email: 'someuser@example.org' } },
errors: undefined,
})
})
@ -359,6 +361,7 @@ describe('Signup', () => {
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { Signup: { email: 'someuser@example.org' } },
errors: undefined,
})
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
})

View File

@ -1,13 +1,13 @@
import { GraphQLClient } from 'graphql-request'
import { createTestClient } from 'apollo-server-testing'
import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers'
import { neode } from '../../bootstrap/neo4j'
import { gql } from '../../jest/helpers'
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
let clientUser1, clientUser2
let headersUser1, headersUser2
let mutate, query, authenticatedUser, variables
const factory = Factory()
const instance = neode()
const categoryIds = ['cat9']
const instance = getNeode()
const driver = getDriver()
const mutationShoutPost = gql`
mutation($id: ID!) {
@ -19,141 +19,136 @@ const mutationUnshoutPost = gql`
unshout(id: $id, type: Post)
}
`
const createPostMutation = gql`
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) {
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
const queryPost = gql`
query($id: ID!) {
Post(id: $id) {
id
title
content
shoutedBy {
id
}
}
}
`
const createPostVariables = {
id: 'p1234',
title: 'Post Title 1234',
content: 'Some Post Content 1234',
categoryIds,
}
beforeEach(async () => {
await factory.create('User', {
id: 'u1',
email: 'test@example.org',
describe('shout and unshout posts', () => {
let currentUser, postAuthor
beforeAll(() => {
authenticatedUser = undefined
const { server } = createServer({
context: () => {
return {
driver,
neode: instance,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
query = createTestClient(server).query
})
beforeEach(async () => {
currentUser = await factory.create('User', {
id: 'current-user-id',
name: 'Current User',
email: 'current.user@example.org',
password: '1234',
})
await factory.create('User', {
id: 'u2',
email: 'test2@example.org',
postAuthor = await factory.create('User', {
id: 'id-of-another-user',
name: 'Another User',
email: 'another.user@example.org',
password: '1234',
})
await instance.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
})
headersUser1 = await login({ email: 'test@example.org', password: '1234' })
headersUser2 = await login({ email: 'test2@example.org', password: '1234' })
clientUser1 = new GraphQLClient(host, { headers: headersUser1 })
clientUser2 = new GraphQLClient(host, { headers: headersUser2 })
await clientUser1.request(createPostMutation, createPostVariables)
await clientUser2.request(createPostMutation, {
id: 'p12345',
title: 'Post Title 12345',
content: 'Some Post Content 12345',
categoryIds,
})
})
afterEach(async () => {
afterEach(async () => {
await factory.cleanDatabase()
})
})
describe('shout', () => {
describe('shout foreign post', () => {
describe('unauthenticated shout', () => {
describe('shout', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const client = new GraphQLClient(host)
await expect(client.request(mutationShoutPost, { id: 'p1234' })).rejects.toThrow(
'Not Authorised',
)
variables = { id: 'post-to-shout-id' }
authenticatedUser = undefined
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await currentUser.toJson()
await factory.create('Post', {
name: 'Other user post',
id: 'another-user-post-id',
author: postAuthor,
})
await factory.create('Post', {
name: 'current user post',
id: 'current-user-post-id',
author: currentUser,
})
variables = {}
})
it("can shout another user's post", async () => {
variables = { id: 'another-user-post-id' }
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({
data: { shout: true },
})
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
data: { Post: [{ id: 'another-user-post-id', shoutedBy: [{ id: 'current-user-id' }] }] },
errors: undefined,
})
})
it('I shout a post of another user', async () => {
const res = await clientUser1.request(mutationShoutPost, { id: 'p12345' })
const expected = {
shout: true,
}
expect(res).toMatchObject(expected)
const { Post } = await clientUser1.request(gql`
query {
Post(id: "p12345") {
shoutedByCurrentUser
}
}
`)
const expected2 = {
shoutedByCurrentUser: true,
}
expect(Post[0]).toMatchObject(expected2)
it('can not shout my own post', async () => {
variables = { id: 'current-user-post-id' }
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({
data: { shout: false },
})
it('I can`t shout my own post', async () => {
const res = await clientUser1.request(mutationShoutPost, { id: 'p1234' })
const expected = {
shout: false,
}
expect(res).toMatchObject(expected)
const { Post } = await clientUser1.request(gql`
query {
Post(id: "p1234") {
shoutedByCurrentUser
}
}
`)
const expected2 = {
shoutedByCurrentUser: false,
}
expect(Post[0]).toMatchObject(expected2)
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
data: { Post: [{ id: 'current-user-post-id', shoutedBy: [] }] },
errors: undefined,
})
})
describe('unshout foreign post', () => {
describe('unauthenticated shout', () => {
})
})
describe('unshout', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
// shout
await clientUser1.request(mutationShoutPost, { id: 'p12345' })
// unshout
const client = new GraphQLClient(host)
await expect(client.request(mutationUnshoutPost, { id: 'p12345' })).rejects.toThrow(
'Not Authorised',
)
authenticatedUser = undefined
variables = { id: 'post-to-shout-id' }
await expect(mutate({ mutation: mutationUnshoutPost, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
})
})
})
it('I unshout a post of another user', async () => {
// shout
await clientUser1.request(mutationShoutPost, { id: 'p12345' })
const expected = {
unshout: true,
}
// unshout
const res = await clientUser1.request(mutationUnshoutPost, { id: 'p12345' })
expect(res).toMatchObject(expected)
describe('authenticated', () => {
beforeEach(async () => {
authenticatedUser = await currentUser.toJson()
await factory.create('Post', {
name: 'Posted By Another User',
id: 'posted-by-another-user',
author: postAuthor,
})
await mutate({
mutation: mutationShoutPost,
variables: { id: 'posted-by-another-user' },
})
})
const { Post } = await clientUser1.request(gql`
query {
Post(id: "p12345") {
shoutedByCurrentUser
}
}
`)
const expected2 = {
shoutedByCurrentUser: false,
}
expect(Post[0]).toMatchObject(expected2)
it("can unshout another user's post", async () => {
variables = { id: 'posted-by-another-user' }
await expect(mutate({ mutation: mutationUnshoutPost, variables })).resolves.toMatchObject({
data: { unshout: true },
})
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
data: { Post: [{ id: 'posted-by-another-user', shoutedBy: [] }] },
errors: undefined,
})
})
})
})
})

View File

@ -9,7 +9,7 @@ export default {
countPosts: 'Post',
countComments: 'Comment',
countNotifications: 'NOTIFIED',
countInvites: 'InvitationCode',
countEmails: 'EmailAddress',
countFollows: 'FOLLOWS',
countShouts: 'SHOUTED',
}
@ -28,6 +28,11 @@ export default {
const stat = statistics[mapping[key]]
response[key] = stat ? stat.toNumber() : 0
})
/*
* Note: invites count is calculated this way because invitation codes are not in use yet
*/
response.countInvites = response.countEmails - response.countUsers
} finally {
session.close()
}

View File

@ -22,6 +22,7 @@ export const getBlockedUsers = async context => {
}
export const getBlockedByUsers = async context => {
if (context.user.role === 'moderator' || context.user.role === 'admin') return []
const { neode } = context
const userModel = neode.model('User')
let blockedByUsers = neode

View File

@ -20,4 +20,9 @@ type Mutation {
about: String
termsAndConditionsAgreedVersion: String!
): User
AddEmailAddress(email: String!): EmailAddress
VerifyEmailAddress(
nonce: String!
email: String!
): EmailAddress
}

View File

@ -1,8 +1,6 @@
import faker from 'faker'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
export function defaults({ args }) {
const defaults = {
email: faker.internet.email(),
verifiedAt: new Date().toISOString(),
@ -11,6 +9,13 @@ export default function create() {
...defaults,
...args,
}
return args
}
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('EmailAddress', args)
},
}

View File

@ -9,6 +9,7 @@ import createTag from './tags.js'
import createSocialMedia from './socialMedia.js'
import createLocation from './locations.js'
import createEmailAddress from './emailAddresses.js'
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
export const seedServerHost = 'http://127.0.0.1:4001'
@ -32,6 +33,7 @@ const factories = {
SocialMedia: createSocialMedia,
Location: createLocation,
EmailAddress: createEmailAddress,
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
}
export const cleanDatabase = async (options = {}) => {

View File

@ -0,0 +1,10 @@
import { defaults } from './emailAddresses.js'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('UnverifiedEmailAddress', args)
},
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
let lastReportTitle
let davidIrvingPostTitle = 'The Truth about the Holocaust'
let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
let davidIrvingName = 'David Irving'
let annoyingUserWhoBlockedModeratorTitle = 'Fake news'
const savePostTitle = $post => {
return $post
@ -139,7 +139,28 @@ Then('I see all the reported posts including the one from above', () => {
})
})
Then('I see all the reported posts including from the user who blocked me', () => {
cy.get('table tbody').within(() => {
cy.contains('tr', annoyingUserWhoBlockedModeratorTitle)
})
})
Then('each list item links to the post page', () => {
cy.contains(davidIrvingPostTitle).click()
cy.location('pathname').should('contain', '/post')
})
Then('I can visit the post page', () => {
cy.contains(annoyingUserWhoBlockedModeratorTitle).click()
cy.location('pathname').should('contain', '/post')
.get('h3').should('contain', annoyingUserWhoBlockedModeratorTitle)
})
When("they have a post someone has reported", () => {
cy.factory()
.create("Post", {
authorId: 'annnoying-user',
title,
});
})

View File

@ -351,10 +351,12 @@ When("I log in with the following credentials:", table => {
});
When("open the notification menu and click on the first item", () => {
cy.get(".notifications-menu").click();
cy.get(".notifications-menu").invoke('show').click(); // "invoke('show')" because of the delay for show the menu
cy.get(".notification-mention-post")
.first()
.click();
.click({
force: true
});
});
Then("see {int} unread notifications in the top menu", count => {
@ -408,6 +410,20 @@ Given("there is an annoying user called {string}", name => {
});
});
Given("there is an annoying user who has blocked me", () => {
cy.neode()
.first("User", {
role: 'moderator'
})
.then(blocked => {
cy.neode()
.first("User", {
id: 'annoying-user'
})
.relateTo(blocked, "blocked");
});
});
Given("I am on the profile page of the annoying user", name => {
cy.openPage("/profile/annoying-user/spammy-spammer");
});

View File

@ -8,13 +8,15 @@ Feature: Report and Moderate
So I can look into it and decide what to do
Background:
Given we have this user in our database:
Given we have the following user accounts:
| id | name |
| u67 | David Irving|
| u67 | David Irving |
| annoying-user | I'm gonna block Moderators and Admins HA HA HA |
Given we have the following posts in our database:
| authorId | id | title | content |
| u67 | p1 | The Truth about the Holocaust | It never existed! |
| annoying-user | p2 | Fake news | This content is demonstratably infactual in some way |
Scenario Outline: Report a post from various pages
Given I am logged in with a "user" role
When I see David Irving's post on the <Page>
@ -56,6 +58,18 @@ Feature: Report and Moderate
Then I see all the reported posts including the one from above
And each list item links to the post page
Scenario: Review reported posts of a user who's blocked a moderator
Given somebody reported the following posts:
| id |
| p2 |
And my user account has the role "moderator"
And there is an annoying user who has blocked me
And I am logged in
When I click on the avatar menu in the top right corner
And I click on "Moderation"
Then I see all the reported posts including from the user who blocked me
And I can visit the post page
Scenario: Normal user can't see the moderation page
Given I am logged in with a "user" role
When I click on the avatar menu in the top right corner

View File

@ -1,4 +1,4 @@
Feature: Notifications for a mentions
Feature: Notification for a mention
As a user
I want to be notified if sb. mentions me in a post or comment
In order join conversations about or related to me

View File

@ -0,0 +1,57 @@
# Backup (online)
## Online backups are only avaible with a Neo4j Enterprise and a license, see https://neo4j.com/licensing/ for the different licenses available
This tutorial explains how to carry out an online backup of your Neo4J
database in a kubernetes cluster.
One of the benefits of doing an online backup is that the Neo4j database does not need to be stopped, so there is no downtime. Read [the docs](https://neo4j.com/docs/operations-manual/current/backup/performing/)
To use Neo4j Enterprise you must add this line to your configmap, if using, or your deployment `nitro-neo4j` env.
```
NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes"
```
## Create a Backup in Kubernetes
```sh
# Backup the database with one command, this will get the nitro-neo4j pod, ssh into it, and run the backup command
kubectl -n=human-connection exec -it $(kubectl -n=human-connection get pods | grep nitro-neo4j | awk '{ print $1 }') -- neo4j-admin backup --backup-dir=/var/lib/neo4j --name=neo4j-backup
# Download the file from the pod to your computer.
kubectl cp human-connection/$(kubectl -n=human-connection get pods | grep nitro-neo4j | awk '{ print $1 }'):/var/lib/neo4j/neo4j-backup ./neo4j-backup/
```
You should now have a backup of the database locally. If you want, you can simulate disaster recovery by sshing into the nitro-neo4j pod, deleting all data and restoring from backup
## Disaster where database data is gone somehow
```sh
kubectl -n=human-connection exec -it $(kubectl -n=human-connection get pods | grep nitro-neo4j |awk '{ print $1 }') bash
# Enter cypher-shell
cypher-shell
# Delete all data
> MATCH (n) DETACH DELETE (n);
exit
```
## Restore a backup in Kubernetes
Restoration must be done while the database is not running, see [our docs](https://docs.human-connection.org/human-connection/deployment/volumes/neo4j-offline-backup#stop-and-restart-neo-4-j-database-in-kubernetes) for how to stop the database, but keep the container running
After, you have stopped the database, and have the pod running, you can restore the database by running these commands:
```sh
kubectl --namespace=human-connection get pods
# Copy the ID of the pod running Neo4J.
# Then upload your local backup to the pod. Note that once the pod gets deleted
# e.g. if you change the deployment, the backup file is gone with it.
kubectl cp ./neo4j-backup/ human-connection/<POD-ID>:/root/
kubectl --namespace=human-connection exec -it <POD-ID> bash
# Once you're in the pod restore the backup and overwrite the default database
# called `graph.db` with `--force`.
# This will delete all existing data in database `graph.db`!
neo4j-admin restore --from=/root/neo4j-backup --force
exit
```
Revert your changes to deployment `nitro-neo4j` which will restart the database.

View File

@ -10,6 +10,7 @@ services:
- "BUILD_COMMIT=${TRAVIS_COMMIT}"
ports:
- 3000:3000
- 3002:3002
networks:
- hc-network
depends_on:
@ -55,12 +56,13 @@ services:
- hc-network
environment:
- NEO4J_AUTH=none
- NEO4J_dbms_security_procedures_unrestricted=algo.*,apoc.*
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
ports:
- 7687:7687
- 7474:7474
volumes:
- neo4j_data:/data
networks:
hc-network:
volumes:

View File

@ -1,4 +1,4 @@
FROM neo4j:3.5.9
FROM neo4j:3.5.11-enterprise
LABEL Description="Neo4J database of the Social Network Human-Connection.org with preinstalled database constraints and indices" Vendor="Human Connection gGmbH" Version="0.0.1" Maintainer="Human Connection gGmbH (developer@human-connection.org)"
ARG BUILD_COMMIT

View File

@ -21,10 +21,10 @@
"devDependencies": {
"bcryptjs": "^2.4.3",
"codecov": "^3.6.1",
"cross-env": "^6.0.0",
"cross-env": "^6.0.3",
"cypress": "^3.4.1",
"cypress-cucumber-preprocessor": "^1.16.0",
"cypress-file-upload": "^3.3.3",
"cypress-cucumber-preprocessor": "^1.16.1",
"cypress-file-upload": "^3.3.4",
"cypress-plugin-retries": "^1.3.0",
"dotenv": "^8.1.0",
"faker": "Marak/faker.js#master",

View File

@ -1,4 +1,4 @@
FROM node:12.10.0-alpine as base
FROM node:12.11.0-alpine as base
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000

View File

@ -1,4 +1,4 @@
FROM node:12.10.0-alpine as build
FROM node:12.11.0-alpine as build
LABEL Description="Web Frontend of the Social Network Human-Connection.org" Vendor="Human-Connection gGmbH" Version="0.0.1" Maintainer="Human-Connection gGmbH (developer@human-connection.org)"
EXPOSE 3000

View File

@ -33,6 +33,42 @@ $ yarn build
$ yarn start
```
### Storybook
We encourage contributors to use Storybook to test out new components in an isolated way, and benefit from its many features.
See the docs for live examples and answers to FAQ, among other helpful information. ![Storybook docs](https://storybook.js.org/docs/basics/introduction/)
{% tabs %}
{% tab title="Docker" %}
After you have started the application following the instructions above, in another terminal run:
```bash
$ docker-compose exec webapp yarn storybook
```
The output should look similar to this:
![Storybook output](../.gitbook/assets/storybook-output.png)
Click on the link http://localhost:3002/ to open the browser to your interactive storybook.
{% endtab %}
{% tab title="Without Docker" %}
Run the following command:
```bash
# in webapp/
yarn storybook
```
Open http://localhost:3002/ in your browser
{% endtab %}
{% endtabs %}
## Styleguide
All reusable Components \(for example avatar\) should be done inside the [Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide) repository.

View File

@ -0,0 +1,10 @@
export default function(to, from, savedPosition) {
if (savedPosition) return savedPosition
// Edge case: If you click on a notification from a comment and then on the
// post page you click on 'comments', we avoid a "jumping" scroll behavior,
// ie. jump to the top and scroll back from there
if (to.path === from.path && to.hash !== from.hash) return false
return { x: 0, y: 0 }
}

View File

@ -32,6 +32,7 @@ describe('Comment.vue', () => {
truncate: a => a,
removeHtml: a => a,
},
$scrollTo: jest.fn(),
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
@ -51,6 +52,8 @@ describe('Comment.vue', () => {
})
describe('shallowMount', () => {
beforeEach(jest.useFakeTimers)
Wrapper = () => {
const store = new Vuex.Store({
getters,
@ -117,7 +120,35 @@ describe('Comment.vue', () => {
})
})
beforeEach(jest.useFakeTimers)
describe('scrollToAnchor mixin', () => {
describe('$route.hash !== comment.id', () => {
beforeEach(() => {
mocks.$route = {
hash: '',
}
})
it('skips $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).not.toHaveBeenCalled()
})
})
describe('$route.hash === comment.id', () => {
beforeEach(() => {
mocks.$route = {
hash: '#commentId-2',
}
})
it('calls $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).toHaveBeenCalledWith('#commentId-2')
})
})
})
describe('test callbacks', () => {
beforeEach(() => {

View File

@ -10,10 +10,15 @@
</ds-card>
</div>
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-card :id="`commentId-${comment.id}`">
<ds-space margin-bottom="small" margin-top="base">
<hc-user :user="author" :date-time="comment.createdAt" />
<!-- Content Menu (can open Modals) -->
<ds-card :id="anchor">
<ds-space margin-bottom="small" margin-top="small">
<hc-user :user="author" :date-time="comment.createdAt">
<template v-slot:dateTime>
<ds-text v-if="comment.createdAt !== comment.updatedAt">
({{ $t('comment.edited') }})
</ds-text>
</template>
</hc-user>
<client-only>
<content-menu
v-show="!openEditCommentMenu"
@ -37,7 +42,7 @@
@collapse="isCollapsed = true"
/>
</div>
<div v-show="!openEditCommentMenu">
<div v-else>
<content-viewer
v-if="$filters.removeHtml(comment.content).length < 180"
:content="comment.content"
@ -80,8 +85,10 @@ import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
export default {
mixins: [scrollToAnchor],
data: function() {
return {
isCollapsed: true,
@ -109,6 +116,9 @@ export default {
user: 'auth/user',
isModerator: 'auth/isModerator',
}),
anchor() {
return `commentId-${this.comment.id}`
},
displaysComment() {
return !this.unavailable || this.isModerator
},
@ -142,6 +152,9 @@ export default {
},
},
methods: {
checkAnchor(anchor) {
return `#${this.anchor}` === anchor
},
isAuthor(id) {
return this.user.id === id
},

View File

@ -42,6 +42,7 @@ describe('CommentList.vue', () => {
truncate: a => a,
removeHtml: a => a,
},
$scrollTo: jest.fn(),
$apollo: {
queries: {
Post: {
@ -65,12 +66,46 @@ describe('CommentList.vue', () => {
})
}
beforeEach(() => {
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
it('displays a comments counter', () => {
wrapper = Wrapper()
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
})
describe('scrollToAnchor mixin', () => {
beforeEach(jest.useFakeTimers)
describe('$route.hash !== `#comments`', () => {
beforeEach(() => {
mocks.$route = {
hash: '',
}
})
it('skips $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).not.toHaveBeenCalled()
})
})
describe('$route.hash === `#comments`', () => {
beforeEach(() => {
mocks.$route = {
hash: '#comments',
}
})
it('calls $scrollTo', () => {
wrapper = Wrapper()
jest.runAllTimers()
expect(mocks.$scrollTo).toHaveBeenCalledWith('#comments')
})
})
})
})
})

View File

@ -30,8 +30,10 @@
</template>
<script>
import Comment from '~/components/Comment/Comment'
import scrollToAnchor from '~/mixins/scrollToAnchor'
export default {
mixins: [scrollToAnchor],
components: {
Comment,
},
@ -39,6 +41,9 @@ export default {
post: { type: Object, default: () => {} },
},
methods: {
checkAnchor(anchor) {
return anchor === '#comments'
},
updateCommentList(updatedComment) {
this.post.comments = this.post.comments.map(comment => {
return comment.id === updatedComment.id ? updatedComment : comment

View File

@ -75,7 +75,7 @@ export default {
const followedUser = follow ? data.followUser : data.unfollowUser
this.$emit('update', followedUser)
} catch {
} catch (err) {
optimisticResult.followedByCurrentUser = !follow
this.$emit('optimistic', optimisticResult)
}

View File

@ -115,7 +115,7 @@ describe('Signup', () => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: User account with this email already exists.'),
new Error('UserInputError: A user account with this email already exists.'),
)
})

View File

@ -128,7 +128,7 @@ export default {
} catch (err) {
const { message } = err
const mapping = {
'User account with this email already exists': 'email-exists',
'A user account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token',
}
for (const [pattern, key] of Object.entries(mapping)) {

View File

@ -56,7 +56,7 @@ const user = {
storiesOf('User', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('available user', () => ({
.add('available', () => ({
components: { User },
store: helpers.store,
data: () => ({
@ -64,7 +64,21 @@ storiesOf('User', module)
}),
template: '<user :user="user" :trunc="35" :date-time="new Date()" />',
}))
.add('anonymous user', () => ({
.add('has edited something', () => ({
components: { User },
store: helpers.store,
data: () => ({
user,
}),
template: `
<user :user="user" :trunc="35" :date-time="new Date()">
<template v-slot:dateTime>
- HEY! I'm edited
</template>
</user>
`,
}))
.add('anonymous', () => ({
components: { User },
store: helpers.store,
data: () => ({

View File

@ -11,17 +11,17 @@
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
<hc-avatar class="avatar" :user="user" />
<div>
<ds-text align="left">
<ds-text>
<b class="username">{{ userName | truncate(18) }}</b>
<ds-text v-if="dateTime" size="small" color="soft">
<ds-icon name="clock" />
<client-only>
<hc-relative-date-time :date-time="dateTime" />
</client-only>
<slot name="dateTime"></slot>
</ds-text>
</ds-text>
</div>
<!-- Time -->
<ds-text align="left" size="small" color="soft">
{{ userSlug }}
</ds-text>

View File

@ -69,16 +69,16 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.commented_on_post',
'notifications.reason.commented_on_post',
)
})
it('renders title', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the "Comment:"', () => {
it('renders the identifier "notifications.comment"', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('Comment:')
expect(wrapper.text()).toContain('notifications.comment')
})
it('renders the contentExcerpt', () => {
wrapper = Wrapper()
@ -119,7 +119,7 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.mentioned_in_post',
'notifications.reason.mentioned_in_post',
)
})
it('renders title', () => {
@ -169,7 +169,7 @@ describe('Notification', () => {
it('renders reason', () => {
wrapper = Wrapper()
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
'notifications.menu.mentioned_in_comment',
'notifications.reason.mentioned_in_comment',
)
})
it('renders title', () => {
@ -177,9 +177,9 @@ describe('Notification', () => {
expect(wrapper.text()).toContain("It's a post title")
})
it('renders the "Comment:"', () => {
it('renders the identifier "notifications.comment"', () => {
wrapper = Wrapper()
expect(wrapper.text()).toContain('Comment:')
expect(wrapper.text()).toContain('notifications.comment')
})
it('renders the contentExcerpt', () => {

View File

@ -5,7 +5,7 @@
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
</ds-space>
<ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.menu.${notification.reason}`) }}
{{ $t(`notifications.reason.${notification.reason}`) }}
</ds-text>
</client-only>
<ds-space margin-bottom="x-small" />
@ -23,7 +23,9 @@
>
<ds-space margin-bottom="x-small" />
<div>
<span v-if="isComment" class="comment-notification-header">Comment:</span>
<span v-if="isComment" class="comment-notification-header">
{{ $t(`notifications.comment`) }}:
</span>
{{ from.contentExcerpt | removeHtml }}
</div>
</ds-card>

View File

@ -46,11 +46,46 @@ describe('NotificationMenu.vue', () => {
expect(wrapper.contains('.dropdown')).toBe(false)
})
describe('given only unread notifications', () => {
beforeEach(() => {
data = () => {
return {
displayedNotifications: [
{
id: 'notification-41',
read: true,
post: {
id: 'post-1',
title: 'some post title',
contentExcerpt: 'this is a post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe',
},
},
},
],
}
}
})
it('counter displays 0', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('0')
})
it('button is not primary', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').props('primary')).toBe(false)
})
})
describe('given some notifications', () => {
beforeEach(() => {
data = () => {
return {
notifications: [
displayedNotifications: [
{
id: 'notification-41',
read: false,
@ -79,15 +114,34 @@ describe('NotificationMenu.vue', () => {
},
},
},
{
id: 'notification-43',
read: true,
post: {
id: 'post-3',
title: 'read post title',
contentExcerpt: 'this is yet another post content',
author: {
id: 'john-1',
slug: 'john-doe',
name: 'John Doe',
},
},
},
],
}
}
})
it('displays the total number of notifications', () => {
it('displays the number of unread notifications', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').text()).toEqual('2')
})
it('renders primary button', () => {
wrapper = Wrapper()
expect(wrapper.find('ds-button-stub').props('primary')).toBe(true)
})
})
})
})

View File

@ -1,16 +1,16 @@
<template>
<ds-button v-if="totalNotifications <= 0" class="notifications-menu" disabled icon="bell">
{{ totalNotifications }}
<ds-button v-if="!notificationsCount" class="notifications-menu" disabled icon="bell">
{{ unreadNotificationsCount }}
</ds-button>
<dropdown v-else class="notifications-menu" :placement="placement">
<template slot="default" slot-scope="{ toggleMenu }">
<ds-button primary icon="bell" @click.prevent="toggleMenu">
{{ totalNotifications }}
<ds-button :primary="!!unreadNotificationsCount" icon="bell" @click.prevent="toggleMenu">
{{ unreadNotificationsCount }}
</ds-button>
</template>
<template slot="popover">
<div class="notifications-menu-popover">
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
<notification-list :notifications="displayedNotifications" @markAsRead="markAsRead" />
</div>
</template>
</dropdown>
@ -18,6 +18,7 @@
<script>
import Dropdown from '~/components/Dropdown'
import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications'
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import NotificationList from '../NotificationList/NotificationList'
@ -29,6 +30,7 @@ export default {
},
data() {
return {
displayedNotifications: [],
notifications: [],
}
},
@ -46,17 +48,29 @@ export default {
variables,
})
if (!(markAsRead && markAsRead.read === true)) return
this.notifications = this.notifications.map(n => {
return n.from.id === markAsRead.from.id ? markAsRead : n
this.displayedNotifications = this.displayedNotifications.map(n => {
return this.equalNotification(n, markAsRead) ? markAsRead : n
})
} catch (err) {
throw new Error(err)
this.$toast.error(err.message)
}
},
equalNotification(a, b) {
return a.from.id === b.from.id && a.createdAt === b.createdAt && a.reason === b.reason
},
},
computed: {
totalNotifications() {
return (this.notifications || []).length
notificationsCount() {
return (this.displayedNotifications || []).length
},
unreadNotificationsCount() {
let countUnread = 0
if (this.displayedNotifications) {
this.displayedNotifications.forEach(notification => {
if (!notification.read) countUnread++
})
}
return countUnread
},
},
apollo: {
@ -64,6 +78,23 @@ export default {
query() {
return notificationQuery(this.$i18n)
},
pollInterval() {
return NOTIFICATIONS_POLL_INTERVAL
},
update(data) {
const newNotifications = data.notifications.filter(newN => {
return !this.displayedNotifications.find(oldN => this.equalNotification(newN, oldN))
})
this.displayedNotifications = newNotifications
.concat(this.displayedNotifications)
.sort((a, b) => {
return new Date(b.createdAt) - new Date(a.createdAt)
})
return data.notifications
},
error(error) {
this.$toast.error(error)
},
},
},
}

View File

@ -0,0 +1 @@
export const NOTIFICATIONS_POLL_INTERVAL = 60000

View File

@ -10,6 +10,7 @@ export default i18n => {
contentExcerpt
content
createdAt
updatedAt
disabled
deleted
author {
@ -39,6 +40,7 @@ export default i18n => {
contentExcerpt
content
createdAt
updatedAt
disabled
deleted
author {

View File

@ -0,0 +1,20 @@
import gql from 'graphql-tag'
export const AddEmailAddressMutation = gql`
mutation($email: String!) {
AddEmailAddress(email: $email) {
email
createdAt
}
}
`
export const VerifyEmailAddressMutation = gql`
mutation($email: String!, $nonce: String!) {
VerifyEmailAddress(email: $email, nonce: $nonce) {
email
verifiedAt
createdAt
}
}
`

View File

@ -40,6 +40,7 @@ export const postFragment = lang => gql`
content
contentExcerpt
createdAt
updatedAt
disabled
deleted
slug
@ -64,6 +65,7 @@ export const commentFragment = lang => gql`
fragment comment on Comment {
id
createdAt
updatedAt
disabled
deleted
content

View File

@ -135,11 +135,12 @@
}
},
"notifications": {
"menu": {
"reason": {
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
"commented_on_post": "Hat deinen Beitrag kommentiert …"
}
},
"comment": "Kommentar"
},
"search": {
"placeholder": "Suchen",
@ -157,6 +158,27 @@
"labelBio": "Über dich",
"success": "Deine Daten wurden erfolgreich aktualisiert!"
},
"email": {
"validation": {
"same-email": "Das ist deine aktuelle E-Mail Addresse"
},
"name": "Deine E-Mail",
"labelEmail": "E-Mail Adresse ändern",
"labelNewEmail": "Neue E-Mail Adresse",
"labelNonce": "Bestätigungscode eingeben",
"success": "Eine neue E-Mail Addresse wurde registriert.",
"submitted": "Eine E-Mail zur Bestätigung deiner Adresse wurde an <b>{email}</b> gesendet.",
"change-successful": "Deine E-Mail Adresse wurde erfolgreich geändert.",
"verification-error": {
"message": "Deine E-Mail Adresse konnte nicht verifiziert werden.",
"support": "Wenn das Problem weiterhin besteht, kontaktiere uns gerne per E-Mail an",
"explanation": "Das kann verschiedene Ursachen haben:",
"reason": {
"invalid-nonce": "Ist der Bestätigungscode falsch?",
"no-email-request": "Bist du dir sicher, dass du eine Änderung deiner E-Mail Adresse angefragt hattest?"
}
}
},
"validation": {
"slug": {
"regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt.",
@ -326,6 +348,7 @@
"third-party-false": "Es wird <b style='color:red'>kein</b> Service von Drittanbietern automatisch eingebunden.",
"third-party-true": "Das einbinden der Services von Drittanbietern ist <b style='color:red'>dauerhaft zugelassen</b> und gespeichert für komende Sitzungen."
}
"edited": "bearbeitet"
},
"comment": {
"content": {
@ -338,7 +361,8 @@
"show": {
"more": "mehr anzeigen",
"less": "weniger anzeigen"
}
},
"edited": "bearbeitet"
},
"quotes": {
"african": {

View File

@ -136,11 +136,12 @@
}
},
"notifications": {
"menu": {
"reason": {
"mentioned_in_post": "Mentioned you in a post …",
"mentioned_in_comment": "Mentioned you in a comment …",
"commented_on_post": "Commented on your post …"
}
},
"comment": "Comment"
},
"search": {
"placeholder": "Search",
@ -158,6 +159,27 @@
"labelBio": "About You",
"success": "Your data was successfully updated!"
},
"email": {
"validation": {
"same-email": "This is your current email address"
},
"name": "Your email",
"labelEmail": "Change your email address",
"labelNewEmail": "New email Address",
"labelNonce": "Enter your code",
"success": "A new email address has been registered.",
"submitted": "An email to verify your address has been sent to <b>{email}</b>.",
"change-successful": "Your email address has been changed successfully.",
"verification-error": {
"message": "Your email could not be changed.",
"explanation": "This can have different causes:",
"reason": {
"invalid-nonce": "Is the confirmation code invalid?",
"no-email-request": "Are you certain that you requested a change of your email address?"
},
"support": "If the problem persists, please contact us by email at"
}
},
"validation": {
"slug": {
"regex": "Allowed characters are only lowercase letters, numbers, underscores and hyphens.",
@ -253,7 +275,7 @@
"users": {
"name": "Users",
"form": {
"placeholder": "E-Mail, name or description"
"placeholder": "email, name or description"
},
"table": {
"columns": {
@ -327,6 +349,7 @@
"third-party-false": "It automatically integrates <b style='color:red'>no </b> third-party providers' service.",
"third-party-true": "The inclusion of third-party services is <b style='color:red'>permanently allowed</b> and stored for future sessions."
}
"edited": "edited"
},
"comment": {
"content": {
@ -339,7 +362,8 @@
"show": {
"more": "show more",
"less": "show less"
}
},
"edited": "edited"
},
"quotes": {
"african": {

View File

@ -118,7 +118,11 @@
},
"takeAction": {
"name": "Tomar uma ação"
}
},
"comment": {
"submit": "Commentar"
},
"edited": "editado"
},
"quotes": {
"african": {
@ -202,9 +206,19 @@
"delete": "Apagar Contribuição"
},
"comment": {
"content": {
"unavailable-placeholder": "… este commenttário não está disponível"
},
"menu": {
"edit": "Editar Comentário",
"delete": "Apagar Comentário"
},
"show": {
"more": "mostrar mais",
"less": "mostrar menos"
},
"edited": "editado"
},
"followButton": {
"follow": "Seguir",
"following": "Seguindo"

View File

@ -22,8 +22,8 @@
<ds-text>{{ $t('maintenance.explanation') }}</ds-text>
<ds-text>
{{ $t('maintenance.questions') }}
<a href="mailto:info@human-connection.org" class="email-link">
info@human-connection.org
<a href="mailto:support@human-connection.org" class="email-link">
support@human-connection.org
</a>
.
</ds-text>

View File

@ -0,0 +1,23 @@
function scrollToAnchor(anchor, { checkAnchor, $scrollTo }) {
if (typeof checkAnchor !== 'function')
throw new Error(
'You must define `checkAnchor` on the component if you use scrollToAnchor mixin!',
)
if (!checkAnchor(anchor)) return
setTimeout(() => {
$scrollTo(anchor)
}, 250)
}
export default {
watch: {
$route(to, from) {
const anchor = to && to.hash
scrollToAnchor(anchor, this)
},
},
mounted() {
const anchor = this.$route && this.$route.hash
scrollToAnchor(anchor, this)
},
}

View File

@ -124,80 +124,6 @@ export default {
middleware: ['authenticated', 'termsAndConditions'],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior: (to, _from, savedPosition) => {
let position = false
// if no children detected and scrollToTop is not explicitly disabled
if (
to.matched.length < 2 &&
to.matched.every(r => r.components.default.options.scrollToTop !== false)
) {
// scroll to the top of the page
position = {
x: 0,
y: 0,
}
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
// if one of the children has scrollToTop option set to true
position = {
x: 0,
y: 0,
}
}
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
}
return new Promise(resolve => {
// wait for the out transition to complete (if necessary)
window.$nuxt.$once('triggerScroll', () => {
let processInterval = null
let processTime = 0
const callInterval = 100
const callIntervalLimit = 2000
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
processInterval = setInterval(() => {
const hashIsFound = document.querySelector(hash)
if (hashIsFound) {
position = {
selector: hash,
offset: { x: 0, y: -500 },
}
}
processTime += callInterval
if (hashIsFound || processTime >= callIntervalLimit) {
clearInterval(processInterval)
processInterval = null
}
}, callInterval)
} catch (e) {
/* eslint-disable-next-line no-console */
console.warn(
'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).',
)
}
}
let resolveInterval = setInterval(() => {
if (!processInterval) {
clearInterval(resolveInterval)
resolve(position)
}
}, callInterval)
})
})
},
},
/*
@ -216,6 +142,13 @@ export default {
keys: envWhitelist,
},
],
[
'vue-scrollto/nuxt',
{
offset: -100, // to compensate fixed navbar height
duration: 1000,
},
],
'cookie-universal-nuxt',
'@nuxtjs/apollo',
'@nuxtjs/axios',

View File

@ -52,7 +52,7 @@
},
"dependencies": {
"@human-connection/styleguide": "0.5.21",
"@nuxtjs/apollo": "^4.0.0-rc13.1",
"@nuxtjs/apollo": "^4.0.0-rc14",
"@nuxtjs/axios": "~5.6.0",
"@nuxtjs/dotenv": "~1.4.1",
"@nuxtjs/pwa": "^3.0.0-beta.19",
@ -62,27 +62,28 @@
"apollo-cache-inmemory": "~1.6.3",
"apollo-client": "~2.6.4",
"cookie-universal-nuxt": "~2.0.18",
"cross-env": "~6.0.0",
"date-fns": "2.2.1",
"cross-env": "~6.0.3",
"date-fns": "2.4.1",
"express": "~4.17.1",
"graphql": "~14.5.6",
"graphql": "~14.5.8",
"isemail": "^3.2.0",
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.2.0",
"node-fetch": "^2.6.0",
"nuxt": "~2.9.2",
"nuxt": "~2.10.0",
"nuxt-dropzone": "^1.0.4",
"nuxt-env": "~0.1.0",
"stack-utils": "^1.0.2",
"string-hash": "^1.1.3",
"tippy.js": "^4.3.5",
"tiptap": "~1.25.0",
"tiptap-extensions": "~1.27.0",
"tiptap": "~1.26.0",
"tiptap-extensions": "~1.28.0",
"trunc-html": "^1.1.2",
"v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13",
"vue-infinite-scroll": "^2.0.2",
"vue-izitoast": "^1.2.1",
"vue-scrollto": "^2.17.1",
"vue-sweetalert-icons": "~4.2.0",
"vuex-i18n": "~1.13.1",
"xregexp": "^4.2.4",
@ -108,9 +109,9 @@
"core-js": "~2.6.9",
"css-loader": "~3.2.0",
"eslint": "~5.16.0",
"eslint-config-prettier": "~6.3.0",
"eslint-config-prettier": "~6.4.0",
"eslint-config-standard": "~12.0.0",
"eslint-loader": "~3.0.0",
"eslint-loader": "~3.0.2",
"eslint-plugin-import": "~2.18.2",
"eslint-plugin-jest": "~22.17.0",
"eslint-plugin-node": "~10.0.0",

View File

@ -77,17 +77,6 @@ export default {
]
},
},
watch: {
$route(to, from) {
if (to.hash === '#comments') {
window.scroll({
top: document.getElementById('comments').offsetTop,
left: 0,
behavior: 'smooth',
})
}
},
},
}
</script>

View File

@ -6,8 +6,11 @@
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
>
<ds-space margin-bottom="small" />
<hc-user :user="post.author" :date-time="post.createdAt" />
<!-- Content Menu (can open Modals) -->
<hc-user :user="post.author" :date-time="post.createdAt">
<template v-slot:dateTime>
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
</template>
</hc-user>
<client-only>
<content-menu
placement="bottom-end"
@ -189,11 +192,15 @@ export default {
margin-top: $space-small;
position: relative;
}
.ProseMirror {
min-height: 0px;
}
}
.ds-card-image {
img {
max-height: 300px;
max-height: 710px;
object-fit: cover;
object-position: center;
}

View File

@ -23,6 +23,10 @@ export default {
name: this.$t('settings.data.name'),
path: `/settings`,
},
{
name: this.$t('settings.email.name'),
path: `/settings/my-email-address`,
},
{
name: this.$t('settings.security.name'),
path: `/settings/security`,

View File

@ -31,14 +31,7 @@
:placeholder="$t('settings.data.labelBio')"
/>
<template slot="footer">
<ds-button
style="float: right;"
icon="check"
:disabled="errors"
type="submit"
:loading="loadingData"
primary
>
<ds-button icon="check" :disabled="errors" type="submit" :loading="loadingData" primary>
{{ $t('actions.save') }}
</ds-button>
</template>

View File

@ -0,0 +1,53 @@
import { mount, createLocalVue } from '@vue/test-utils'
import EnterNoncePage from './enter-nonce.vue'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('EnterNoncePage', () => {
let mocks
let wrapper
beforeEach(() => {
wrapper = null
mocks = {
$t: jest.fn(t => t),
$route: {
query: {},
},
$router: {
replace: jest.fn(),
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(EnterNoncePage, {
mocks,
localVue,
})
}
describe('form', () => {
describe('submit', () => {
it('renders form errors', () => {
wrapper = Wrapper()
wrapper.find('form').trigger('submit')
expect(mocks.$router.replace).not.toHaveBeenCalled()
})
describe('entering a nonce', () => {
it('redirects to my-email-address/verify', () => {
wrapper = Wrapper()
wrapper.find('#nonce').setValue('foobar')
wrapper.find('form').trigger('submit')
expect(mocks.$router.replace).toHaveBeenCalled()
})
})
})
})
})
})

View File

@ -0,0 +1,59 @@
<template>
<ds-form v-model="form" :schema="formSchema" @submit="submit">
<template slot-scope="{ errors }">
<ds-card :header="$t('settings.email.name')">
<ds-input
id="email"
model="email"
icon="envelope"
disabled
:label="$t('settings.email.labelNewEmail')"
/>
<ds-input
id="nonce"
model="nonce"
icon="question-circle"
:label="$t('settings.email.labelNonce')"
/>
<template slot="footer">
<ds-button class="submit-button" icon="check" :disabled="errors" type="submit" primary>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
</ds-form>
</template>
<script>
export default {
data() {
return {
formSchema: {
nonce: { type: 'string', required: true },
},
}
},
computed: {
form: {
get: function() {
const { email = '', nonce = '' } = this.$route.query
return { email, nonce }
},
set: function(formData) {
this.formData = formData
},
},
},
methods: {
async submit() {
const { email, nonce } = this.formData
this.$router.replace({
path: 'verify',
query: { email, nonce },
})
},
},
}
</script>

View File

@ -0,0 +1,116 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import EmailSettingsIndexPage from './index.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
describe('EmailSettingsIndexPage', () => {
let store
let mocks
let wrapper
beforeEach(() => {
wrapper = null
store = new Vuex.Store({
getters: {
'auth/user': () => {
return { id: 'u23', email: 'some-mail@example.org' }
},
},
})
mocks = {
$t: jest.fn(t => t),
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
$router: {
push: jest.fn(),
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(EmailSettingsIndexPage, {
store,
mocks,
localVue,
})
}
describe('form', () => {
describe('submit', () => {
beforeEach(jest.useFakeTimers)
describe('email unchanged', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('form').trigger('submit')
})
it('displays form errors', () => {
expect(wrapper.text()).not.toContain('settings.email.submitted')
expect(wrapper.text()).toContain('settings.email.validation.same-email')
})
it('does not call $apollo.mutate', () => {
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
})
describe('enter another email', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('#email').setValue('yet-another-email@example.org')
wrapper.find('form').trigger('submit')
})
it('calls $apollo.mutate', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('no form errors', () => {
expect(wrapper.text()).not.toContain('settings.email.validation.same-email')
expect(wrapper.text()).toContain('settings.email.submitted')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('redirects to `my-email-address/enter-nonce`', () => {
expect(mocks.$router.push).toHaveBeenCalledWith({
path: 'my-email-address/enter-nonce',
query: { email: 'yet-another-email@example.org' },
})
})
})
})
describe('if backend responds with unique constraint violation', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockRejectedValue({
message: 'User account already exists',
})
wrapper = Wrapper()
wrapper.find('#email').setValue('already-taken@example.org')
wrapper.find('form').trigger('submit')
})
it('translates error message', () => {
expect(wrapper.text()).toContain('registration.signup.form.errors.email-exists')
})
})
})
})
})
})

View File

@ -0,0 +1,113 @@
<template>
<ds-card centered v-if="success">
<transition name="ds-transition-fade">
<sweetalert-icon icon="info" />
</transition>
<ds-text v-html="submitMessage" />
</ds-card>
<ds-form v-else v-model="form" :schema="formSchema" @submit="submit">
<template slot-scope="{ errors }">
<ds-card :header="$t('settings.email.name')">
<ds-input
id="email"
model="email"
icon="envelope"
:label="$t('settings.email.labelEmail')"
/>
<template slot="footer">
<ds-space class="backendErrors" v-if="backendErrors">
<ds-text align="center" bold color="danger">{{ backendErrors.message }}</ds-text>
</ds-space>
<ds-button icon="check" :disabled="errors" type="submit" primary>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
</ds-form>
</template>
<script>
import { mapGetters } from 'vuex'
import { AddEmailAddressMutation } from '~/graphql/EmailAddress.js'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
},
data() {
return {
backendErrors: null,
success: false,
}
},
computed: {
submitMessage() {
const { email } = this.formData
return this.$t('settings.email.submitted', { email })
},
...mapGetters({
currentUser: 'auth/user',
}),
form: {
get: function() {
const { email } = this.currentUser
return { email }
},
set: function(formData) {
this.formData = formData
},
},
formSchema() {
const { email } = this.currentUser
const sameEmailValidationError = this.$t('settings.email.validation.same-email')
return {
email: [
{ type: 'email', required: true },
{
validator(rule, value, callback, source, options) {
const errors = []
if (email === value) {
errors.push(sameEmailValidationError)
}
return errors
},
},
],
}
},
},
methods: {
async submit() {
const { email } = this.formData
try {
await this.$apollo.mutate({
mutation: AddEmailAddressMutation,
variables: { email },
})
this.$toast.success(this.$t('settings.email.success'))
this.success = true
setTimeout(() => {
this.$router.push({
path: 'my-email-address/enter-nonce',
query: { email },
})
}, 3000)
} catch (err) {
if (err.message.includes('exists')) {
// We cannot use form validation errors here, the backend does not
// have a query to filter for email addresses. This is a privacy
// consideration. We could implement a dedicated query to check that
// but I think it's too much effort for this feature.
this.backendErrors = { message: this.$t('registration.signup.form.errors.email-exists') }
return
}
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -0,0 +1,164 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import EmailVerifyPage from './verify.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
describe('EmailVerifyPage', () => {
let store
let mocks
let wrapper
let setUser
beforeEach(() => {
setUser = jest.fn()
wrapper = null
store = new Vuex.Store({
getters: {
'auth/user': () => {
return { id: 'u23', email: 'some-mail@example.org' }
},
},
mutations: {
'auth/SET_USER': setUser,
},
})
mocks = {
$t: jest.fn(t => t),
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$router: {
replace: jest.fn(),
},
store,
}
})
describe('asyncData', () => {
const asyncDataAction = () => {
const context = {
store: mocks.store,
query: {},
app: {
apolloProvider: {
defaultClient: mocks.$apollo,
},
},
}
return EmailVerifyPage.asyncData(context)
}
describe('backend sends successful response', () => {
beforeEach(() => {
mocks = {
...mocks,
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
VerifyEmailAddress: {
email: 'verified-email@example.org',
},
},
}),
},
}
})
it('sets `success` to true', async () => {
await expect(asyncDataAction()).resolves.toEqual({
success: true,
})
})
it("updates current user's email", async () => {
await asyncDataAction()
expect(setUser).toHaveBeenCalledWith({}, { id: 'u23', email: 'verified-email@example.org' })
})
})
describe('backend sends unsuccessful response', () => {
beforeEach(() => {
mocks = {
...mocks,
$apollo: {
mutate: jest.fn().mockRejectedValue({
data: { VerifyEmailAddress: null },
errors: [{ message: 'User account already exists with that email' }],
}),
},
}
})
it('sets `success` to false', async () => {
await expect(asyncDataAction()).resolves.toEqual({
success: false,
})
})
it('does not updates current user', async () => {
await asyncDataAction()
expect(setUser).not.toHaveBeenCalled()
})
})
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
const Wrapper = () => {
return mount(EmailVerifyPage, {
store,
mocks,
localVue,
})
}
describe('given successful verification', () => {
beforeEach(() => {
mocks = { ...mocks, success: true }
wrapper = Wrapper()
})
it('shows success message', () => {
expect(wrapper.text()).toContain('settings.email.change-successful')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('redirects to email settings page', () => {
expect(mocks.$router.replace).toHaveBeenCalledWith({
name: 'settings-my-email-address',
})
})
})
})
describe('given unsuccessful verification', () => {
beforeEach(() => {
mocks = { ...mocks, success: false }
wrapper = Wrapper()
})
it('shows success message', () => {
expect(wrapper.text()).toContain('settings.email.verification-error')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('does not redirect', () => {
expect(mocks.$router.replace).not.toHaveBeenCalledWith()
})
})
})
})
})

View File

@ -0,0 +1,99 @@
<template>
<ds-card>
<transition name="ds-transition-fade">
<client-only>
<sweetalert-icon :icon="sweetAlertIcon" />
</client-only>
</transition>
<ds-space v-if="success">
<ds-text bold align="center">
{{ $t(`settings.email.change-successful`) }}
</ds-text>
</ds-space>
<template v-else>
<ds-text bold align="center">
{{ $t(`settings.email.verification-error.message`) }}
</ds-text>
<ds-space class="message">
<client-only>
<ds-text>
<ds-space margin-top="large" margin-bottom="small">
{{ $t(`settings.email.verification-error.explanation`) }}
</ds-space>
<ds-list>
<ds-list-item>
{{ $t(`settings.email.verification-error.reason.invalid-nonce`) }}
</ds-list-item>
<ds-list-item>
{{ $t(`settings.email.verification-error.reason.no-email-request`) }}
</ds-list-item>
</ds-list>
{{ $t('settings.email.verification-error.support') }}
<a href="mailto:support@human-connection.org">support@human-connection.org</a>
</ds-text>
</client-only>
</ds-space>
</template>
</ds-card>
</template>
<script>
import { VerifyEmailAddressMutation } from '~/graphql/EmailAddress.js'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
},
computed: {
sweetAlertIcon() {
return this.success ? 'success' : 'error'
},
},
created() {
if (this.success) {
setTimeout(() => {
this.$router.replace({ name: 'settings-my-email-address' })
}, 3000)
}
},
async asyncData(context) {
const {
store,
query,
app: { apolloProvider },
} = context
const client = apolloProvider.defaultClient
let success
const { email = '', nonce = '' } = query
const currentUser = store.getters['auth/user']
try {
const response = await client.mutate({
mutation: VerifyEmailAddressMutation,
variables: { email, nonce },
})
const {
data: { VerifyEmailAddress },
} = response
success = true
store.commit(
'auth/SET_USER',
{ ...currentUser, email: VerifyEmailAddress.email },
{ root: true },
)
} catch (error) {
success = false
}
return { success }
},
}
</script>
<style lang="scss" scoped>
.message {
display: flex;
justify-content: space-around;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -741,9 +741,9 @@
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.2.tgz#c3d6e41b304ef10dcf13777a33e7694ec4a9a6dd"
integrity sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg==
dependencies:
regenerator-runtime "^0.13.2"
@ -1772,10 +1772,10 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cross-env@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.0.tgz#3c8e71440ea20aa6faaf5aec541235efc565dac6"
integrity sha512-G/B6gtkjgthT8AP/xN1wdj5Xe18fVyk58JepK8GxpUbqcz3hyWxegocMbvnZK+KoTslwd0ACZ3woi/DVUdVjyQ==
cross-env@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941"
integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==
dependencies:
cross-spawn "^7.0.0"
@ -1877,10 +1877,10 @@ cucumber@^4.2.1:
util-arity "^1.0.2"
verror "^1.9.0"
cypress-cucumber-preprocessor@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.16.0.tgz#c73b3b72ea95ba90cd1ed8fb2e586b1380440edd"
integrity sha512-U3V15iMEKkb7qIePEn22QyDcOjsT+/HRTS6cdKBB2BgtYBCnkWZJ1jfUhf3rFDMMoXFkExSNZG/i00ljF/DUkA==
cypress-cucumber-preprocessor@^1.16.1:
version "1.16.1"
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.16.1.tgz#2ac7e0e53396334c052aeed8b5e61e08616f73a2"
integrity sha512-m8Z5t9hSc10kv47qK0fV/JlCboCwQVxgTa+WhnCjOPB7YBnX/en4f0O8l27yaZbZyHan7JBoJvpfzINlaOKafg==
dependencies:
"@cypress/browserify-preprocessor" "^2.1.1"
chai "^4.1.2"
@ -1892,12 +1892,13 @@ cypress-cucumber-preprocessor@^1.16.0:
debug "^3.0.1"
gherkin "^5.1.0"
glob "^7.1.2"
js-string-escape "^1.0.1"
through "^2.3.8"
cypress-file-upload@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.3.3.tgz#119188fa78e9cfc00904c52d76d4ca56d34950df"
integrity sha512-CmXGRMHonoyCa8EcF6jomxqMAe56HvKfnW7S69EmTga8ecYmvQUI6gYttcHO+5UTmFQOFl7xbABV3+AbnI4btA==
cypress-file-upload@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.3.4.tgz#cbeb8a7a07150a1c60f2873666979e48b6335070"
integrity sha512-kfdrQ6cWBw82G7EbHSqZJiOQWRh9cGz9K1mjePNZax00gBL0qOdRTjfkAnR2vEmmJyCfnN3efryjfhFeLrGWVw==
cypress-plugin-retries@^1.3.0:
version "1.3.0"
@ -3024,6 +3025,11 @@ js-levenshtein@^1.1.3:
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
js-string-escape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"