mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' into Allow_embedded_code_in_posts_permanent_memory
This commit is contained in:
commit
39b3d71be4
BIN
.gitbook/assets/storybook-output.png
Normal file
BIN
.gitbook/assets/storybook-output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -7,6 +7,6 @@
|
|||||||
"autoFix": true
|
"autoFix": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": false,
|
||||||
"eslint.autoFixOnSave": true
|
"eslint.autoFixOnSave": true
|
||||||
}
|
}
|
||||||
|
|||||||
166
CHANGELOG.md
Normal file
166
CHANGELOG.md
Normal 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)*
|
||||||
@ -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?
|
* but what about when you are waiting for merge?
|
||||||
* solutions
|
* solutions
|
||||||
* 1\) put 2nd PR into branch that the first PR is hitting - but requires update after merging
|
* 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
|
### Code Review
|
||||||
* Github setting in place - at least one review is required to merge
|
* Github setting in place - at least one review is required to merge
|
||||||
- in principle anyone (who is not the PR owner) can review
|
- 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
|
- once there is a review, and presuming no requested changes, PR opener can merge
|
||||||
|
|
||||||
* CI/tests
|
* CI/tests
|
||||||
- the CI needs to pass
|
- the CI needs to pass
|
||||||
- linting <-- autofix?
|
- linting (yarn lint --fix)
|
||||||
- tests (unit, feature) (backend, frontend)
|
- tests (unit, feature) (backend, frontend)
|
||||||
- codecoverage
|
- codecoverage
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,8 @@ To reset the database run:
|
|||||||
$ docker-compose exec backend yarn run db:reset
|
$ docker-compose exec backend yarn run db:reset
|
||||||
# you could also wipe out your neo4j database and delete all volumes with:
|
# you could also wipe out your neo4j database and delete all volumes with:
|
||||||
$ docker-compose down -v
|
$ 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 %}
|
{% endtab %}
|
||||||
|
|
||||||
@ -88,7 +90,6 @@ $ yarn run db:reset
|
|||||||
{% endtab %}
|
{% endtab %}
|
||||||
{% endtabs %}
|
{% endtabs %}
|
||||||
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
**Beware**: We have no multiple database setup at the moment. We clean the
|
**Beware**: We have no multiple database setup at the moment. We clean the
|
||||||
|
|||||||
@ -41,30 +41,29 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/joi": "^16.1.2",
|
"@hapi/joi": "^16.1.7",
|
||||||
"@sentry/node": "^5.6.2",
|
"@sentry/node": "^5.6.2",
|
||||||
"activitystrea.ms": "~2.1.3",
|
|
||||||
"apollo-cache-inmemory": "~1.6.3",
|
"apollo-cache-inmemory": "~1.6.3",
|
||||||
"apollo-client": "~2.6.4",
|
"apollo-client": "~2.6.4",
|
||||||
"apollo-link-context": "~1.0.19",
|
"apollo-link-context": "~1.0.19",
|
||||||
"apollo-link-http": "~1.5.16",
|
"apollo-link-http": "~1.5.16",
|
||||||
"apollo-server": "~2.9.3",
|
"apollo-server": "~2.9.4",
|
||||||
"apollo-server-express": "^2.9.0",
|
"apollo-server-express": "^2.9.4",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"cross-env": "~6.0.0",
|
"cross-env": "~6.0.3",
|
||||||
"date-fns": "2.3.0",
|
"date-fns": "2.4.1",
|
||||||
"debug": "~4.1.1",
|
"debug": "~4.1.1",
|
||||||
"dotenv": "~8.1.0",
|
"dotenv": "~8.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
"graphql": "^14.5.7",
|
"graphql": "^14.5.8",
|
||||||
"graphql-custom-directives": "~0.2.14",
|
"graphql-custom-directives": "~0.2.14",
|
||||||
"graphql-iso-date": "~3.6.1",
|
"graphql-iso-date": "~3.6.1",
|
||||||
"graphql-middleware": "~3.0.5",
|
"graphql-middleware": "~4.0.1",
|
||||||
"graphql-middleware-sentry": "^3.2.0",
|
"graphql-middleware-sentry": "^3.2.1",
|
||||||
"graphql-shield": "~6.1.0",
|
"graphql-shield": "~6.1.0",
|
||||||
"graphql-tag": "~2.10.1",
|
"graphql-tag": "~2.10.1",
|
||||||
"helmet": "~3.21.1",
|
"helmet": "~3.21.1",
|
||||||
@ -73,23 +72,23 @@
|
|||||||
"lodash": "~4.17.14",
|
"lodash": "~4.17.14",
|
||||||
"merge-graphql-schemas": "^1.7.0",
|
"merge-graphql-schemas": "^1.7.0",
|
||||||
"metascraper": "^4.10.3",
|
"metascraper": "^4.10.3",
|
||||||
"metascraper-audio": "^5.7.5",
|
"metascraper-audio": "^5.7.6",
|
||||||
"metascraper-author": "^5.7.4",
|
"metascraper-author": "^5.7.6",
|
||||||
"metascraper-clearbit-logo": "^5.3.0",
|
"metascraper-clearbit-logo": "^5.3.0",
|
||||||
"metascraper-date": "^5.7.4",
|
"metascraper-date": "^5.7.6",
|
||||||
"metascraper-description": "^5.7.4",
|
"metascraper-description": "^5.7.6",
|
||||||
"metascraper-image": "^5.7.5",
|
"metascraper-image": "^5.7.6",
|
||||||
"metascraper-lang": "^5.7.4",
|
"metascraper-lang": "^5.7.6",
|
||||||
"metascraper-lang-detector": "^4.8.5",
|
"metascraper-lang-detector": "^4.8.5",
|
||||||
"metascraper-logo": "^5.7.5",
|
"metascraper-logo": "^5.7.6",
|
||||||
"metascraper-publisher": "^5.7.4",
|
"metascraper-publisher": "^5.7.6",
|
||||||
"metascraper-soundcloud": "^5.7.4",
|
"metascraper-soundcloud": "^5.7.6",
|
||||||
"metascraper-title": "^5.7.4",
|
"metascraper-title": "^5.7.6",
|
||||||
"metascraper-url": "^5.7.5",
|
"metascraper-url": "^5.7.6",
|
||||||
"metascraper-video": "^5.7.5",
|
"metascraper-video": "^5.7.6",
|
||||||
"metascraper-youtube": "^5.7.4",
|
"metascraper-youtube": "^5.7.6",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.0.4",
|
||||||
"mustache": "^3.0.3",
|
"mustache": "^3.1.0",
|
||||||
"neo4j-driver": "~1.7.6",
|
"neo4j-driver": "~1.7.6",
|
||||||
"neo4j-graphql-js": "^2.7.2",
|
"neo4j-graphql-js": "^2.7.2",
|
||||||
"neode": "^0.3.3",
|
"neode": "^0.3.3",
|
||||||
@ -106,20 +105,20 @@
|
|||||||
"xregexp": "^4.2.4"
|
"xregexp": "^4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "~7.6.0",
|
"@babel/cli": "~7.6.2",
|
||||||
"@babel/core": "~7.6.0",
|
"@babel/core": "~7.6.2",
|
||||||
"@babel/node": "~7.6.1",
|
"@babel/node": "~7.6.2",
|
||||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.6.0",
|
"@babel/preset-env": "~7.6.2",
|
||||||
"@babel/register": "~7.6.0",
|
"@babel/register": "~7.6.2",
|
||||||
"apollo-server-testing": "~2.9.3",
|
"apollo-server-testing": "~2.9.4",
|
||||||
"babel-core": "~7.0.0-0",
|
"babel-core": "~7.0.0-0",
|
||||||
"babel-eslint": "~10.0.3",
|
"babel-eslint": "~10.0.3",
|
||||||
"babel-jest": "~24.9.0",
|
"babel-jest": "~24.9.0",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"cucumber": "~5.1.0",
|
"cucumber": "~5.1.0",
|
||||||
"eslint": "~6.4.0",
|
"eslint": "~6.5.1",
|
||||||
"eslint-config-prettier": "~6.3.0",
|
"eslint-config-prettier": "~6.4.0",
|
||||||
"eslint-config-standard": "~14.1.0",
|
"eslint-config-standard": "~14.1.0",
|
||||||
"eslint-plugin-import": "~2.18.2",
|
"eslint-plugin-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~22.17.0",
|
"eslint-plugin-jest": "~22.17.0",
|
||||||
@ -129,7 +128,7 @@
|
|||||||
"eslint-plugin-standard": "~4.0.1",
|
"eslint-plugin-standard": "~4.0.1",
|
||||||
"graphql-request": "~1.8.2",
|
"graphql-request": "~1.8.2",
|
||||||
"jest": "~24.9.0",
|
"jest": "~24.9.0",
|
||||||
"nodemon": "~1.19.2",
|
"nodemon": "~1.19.3",
|
||||||
"prettier": "~1.18.2",
|
"prettier": "~1.18.2",
|
||||||
"supertest": "~4.0.2"
|
"supertest": "~4.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { extractNameFromId, extractDomainFromUrl, signAndSend } from './utils'
|
// import { extractDomainFromUrl, signAndSend } from './utils'
|
||||||
import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
|
import { extractNameFromId, signAndSend } from './utils'
|
||||||
|
import { isPublicAddressed } from './utils/activity'
|
||||||
|
// import { isPublicAddressed, sendAcceptActivity, sendRejectActivity } from './utils/activity'
|
||||||
import request from 'request'
|
import request from 'request'
|
||||||
import as from 'activitystrea.ms'
|
// import as from 'activitystrea.ms'
|
||||||
import NitroDataSource from './NitroDataSource'
|
import NitroDataSource from './NitroDataSource'
|
||||||
import router from './routes'
|
import router from './routes'
|
||||||
import Collections from './Collections'
|
import Collections from './Collections'
|
||||||
@ -33,71 +35,71 @@ export default class ActivityPub {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFollowActivity(activity) {
|
// handleFollowActivity(activity) {
|
||||||
debug(`inside FOLLOW ${activity.actor}`)
|
// debug(`inside FOLLOW ${activity.actor}`)
|
||||||
const toActorName = extractNameFromId(activity.object)
|
// const toActorName = extractNameFromId(activity.object)
|
||||||
const fromDomain = extractDomainFromUrl(activity.actor)
|
// const fromDomain = extractDomainFromUrl(activity.actor)
|
||||||
const dataSource = this.dataSource
|
// const dataSource = this.dataSource
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// return new Promise((resolve, reject) => {
|
||||||
request(
|
// request(
|
||||||
{
|
// {
|
||||||
url: activity.actor,
|
// url: activity.actor,
|
||||||
headers: {
|
// headers: {
|
||||||
Accept: 'application/activity+json',
|
// Accept: 'application/activity+json',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
async (err, response, toActorObject) => {
|
// async (err, response, toActorObject) => {
|
||||||
if (err) return reject(err)
|
// if (err) return reject(err)
|
||||||
// save shared inbox
|
// // save shared inbox
|
||||||
toActorObject = JSON.parse(toActorObject)
|
// toActorObject = JSON.parse(toActorObject)
|
||||||
await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
|
// await this.dataSource.addSharedInboxEndpoint(toActorObject.endpoints.sharedInbox)
|
||||||
|
|
||||||
const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
|
// const followersCollectionPage = await this.dataSource.getFollowersCollectionPage(
|
||||||
activity.object,
|
// activity.object,
|
||||||
)
|
// )
|
||||||
|
|
||||||
const followActivity = as
|
// const followActivity = as
|
||||||
.follow()
|
// .follow()
|
||||||
.id(activity.id)
|
// .id(activity.id)
|
||||||
.actor(activity.actor)
|
// .actor(activity.actor)
|
||||||
.object(activity.object)
|
// .object(activity.object)
|
||||||
|
|
||||||
// add follower if not already in collection
|
// // add follower if not already in collection
|
||||||
if (followersCollectionPage.orderedItems.includes(activity.actor)) {
|
// if (followersCollectionPage.orderedItems.includes(activity.actor)) {
|
||||||
debug('follower already in collection!')
|
// debug('follower already in collection!')
|
||||||
debug(`inbox = ${toActorObject.inbox}`)
|
// debug(`inbox = ${toActorObject.inbox}`)
|
||||||
resolve(
|
// resolve(
|
||||||
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||||
)
|
// )
|
||||||
} else {
|
// } else {
|
||||||
followersCollectionPage.orderedItems.push(activity.actor)
|
// followersCollectionPage.orderedItems.push(activity.actor)
|
||||||
}
|
// }
|
||||||
debug(`toActorObject = ${toActorObject}`)
|
// debug(`toActorObject = ${toActorObject}`)
|
||||||
toActorObject =
|
// toActorObject =
|
||||||
typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
|
// typeof toActorObject !== 'object' ? JSON.parse(toActorObject) : toActorObject
|
||||||
debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
|
// debug(`followers = ${JSON.stringify(followersCollectionPage.orderedItems, null, 2)}`)
|
||||||
debug(`inbox = ${toActorObject.inbox}`)
|
// debug(`inbox = ${toActorObject.inbox}`)
|
||||||
debug(`outbox = ${toActorObject.outbox}`)
|
// debug(`outbox = ${toActorObject.outbox}`)
|
||||||
debug(`followers = ${toActorObject.followers}`)
|
// debug(`followers = ${toActorObject.followers}`)
|
||||||
debug(`following = ${toActorObject.following}`)
|
// debug(`following = ${toActorObject.following}`)
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
await dataSource.saveFollowersCollectionPage(followersCollectionPage)
|
// await dataSource.saveFollowersCollectionPage(followersCollectionPage)
|
||||||
debug('follow activity saved')
|
// debug('follow activity saved')
|
||||||
resolve(
|
// resolve(
|
||||||
sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
// sendAcceptActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||||
)
|
// )
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
debug('followers update error!', e)
|
// debug('followers update error!', e)
|
||||||
resolve(
|
// resolve(
|
||||||
sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
// sendRejectActivity(followActivity, toActorName, fromDomain, toActorObject.inbox),
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
)
|
// )
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
handleUndoActivity(activity) {
|
handleUndoActivity(activity) {
|
||||||
debug('inside UNDO')
|
debug('inside UNDO')
|
||||||
|
|||||||
@ -18,9 +18,9 @@ router.post('/', async function(req, res, next) {
|
|||||||
case 'Undo':
|
case 'Undo':
|
||||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||||
break
|
break
|
||||||
case 'Follow':
|
// case 'Follow':
|
||||||
await activityPub.handleFollowActivity(req.body).catch(next)
|
// await activityPub.handleFollowActivity(req.body).catch(next)
|
||||||
break
|
// break
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||||
break
|
break
|
||||||
|
|||||||
@ -56,9 +56,9 @@ router.post('/:name/inbox', verify, async function(req, res, next) {
|
|||||||
case 'Undo':
|
case 'Undo':
|
||||||
await activityPub.handleUndoActivity(req.body).catch(next)
|
await activityPub.handleUndoActivity(req.body).catch(next)
|
||||||
break
|
break
|
||||||
case 'Follow':
|
// case 'Follow':
|
||||||
await activityPub.handleFollowActivity(req.body).catch(next)
|
// await activityPub.handleFollowActivity(req.body).catch(next)
|
||||||
break
|
// break
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
await activityPub.handleDeleteActivity(req.body).catch(next)
|
await activityPub.handleDeleteActivity(req.body).catch(next)
|
||||||
break
|
break
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { activityPub } from '../ActivityPub'
|
import { activityPub } from '../ActivityPub'
|
||||||
import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
|
import { throwErrorIfApolloErrorOccurred } from './index'
|
||||||
|
// import { signAndSend, throwErrorIfApolloErrorOccurred } from './index'
|
||||||
|
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import as from 'activitystrea.ms'
|
// import as from 'activitystrea.ms'
|
||||||
import gql from 'graphql-tag'
|
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) {
|
export function createNoteObject(text, name, id, published) {
|
||||||
const createUuid = crypto.randomBytes(16).toString('hex')
|
const createUuid = crypto.randomBytes(16).toString('hex')
|
||||||
@ -62,41 +63,41 @@ export async function getActorId(name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendAcceptActivity(theBody, name, targetDomain, url) {
|
// export function sendAcceptActivity(theBody, name, targetDomain, url) {
|
||||||
as.accept()
|
// as.accept()
|
||||||
.id(
|
// .id(
|
||||||
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||||
crypto.randomBytes(16).toString('hex'),
|
// crypto.randomBytes(16).toString('hex'),
|
||||||
)
|
// )
|
||||||
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||||
.object(theBody)
|
// .object(theBody)
|
||||||
.prettyWrite((err, doc) => {
|
// .prettyWrite((err, doc) => {
|
||||||
if (!err) {
|
// if (!err) {
|
||||||
return signAndSend(doc, name, targetDomain, url)
|
// return signAndSend(doc, name, targetDomain, url)
|
||||||
} else {
|
// } else {
|
||||||
debug(`error serializing Accept object: ${err}`)
|
// debug(`error serializing Accept object: ${err}`)
|
||||||
throw new Error('error serializing Accept object')
|
// throw new Error('error serializing Accept object')
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function sendRejectActivity(theBody, name, targetDomain, url) {
|
// export function sendRejectActivity(theBody, name, targetDomain, url) {
|
||||||
as.reject()
|
// as.reject()
|
||||||
.id(
|
// .id(
|
||||||
`${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
// `${activityPub.endpoint}/activitypub/users/${name}/status/` +
|
||||||
crypto.randomBytes(16).toString('hex'),
|
// crypto.randomBytes(16).toString('hex'),
|
||||||
)
|
// )
|
||||||
.actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
// .actor(`${activityPub.endpoint}/activitypub/users/${name}`)
|
||||||
.object(theBody)
|
// .object(theBody)
|
||||||
.prettyWrite((err, doc) => {
|
// .prettyWrite((err, doc) => {
|
||||||
if (!err) {
|
// if (!err) {
|
||||||
return signAndSend(doc, name, targetDomain, url)
|
// return signAndSend(doc, name, targetDomain, url)
|
||||||
} else {
|
// } else {
|
||||||
debug(`error serializing Accept object: ${err}`)
|
// debug(`error serializing Accept object: ${err}`)
|
||||||
throw new Error('error serializing Accept object')
|
// throw new Error('error serializing Accept object')
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
export function isPublicAddressed(postObject) {
|
export function isPublicAddressed(postObject) {
|
||||||
if (typeof postObject.to === 'string') {
|
if (typeof postObject.to === 'string') {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export default async (driver, authorizationHeader) => {
|
|||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const query = `
|
const query = `
|
||||||
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
MATCH (user:User {id: $id, deleted: false, disabled: false })
|
||||||
|
SET user.lastActiveAt = toString(datetime())
|
||||||
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId}
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import Factory from '../seed/factories/index'
|
import Factory from '../seed/factories/index'
|
||||||
import { getDriver } from '../bootstrap/neo4j'
|
import { getDriver, neode as getNeode } from '../bootstrap/neo4j'
|
||||||
import decode from './decode'
|
import decode from './decode'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
const neode = getNeode()
|
||||||
|
|
||||||
// here is the decoded JWT token:
|
// 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', () => {
|
describe('but user is deleted', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
await user.update({ updatedAt: new Date().toISOString(), deleted: true })
|
||||||
@ -92,6 +120,7 @@ describe('decode', () => {
|
|||||||
|
|
||||||
it('returns null', returnsNull)
|
it('returns null', returnsNull)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('but user is disabled', () => {
|
describe('but user is disabled', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await user.update({ updatedAt: new Date().toISOString(), disabled: true })
|
await user.update({ updatedAt: new Date().toISOString(), disabled: true })
|
||||||
|
|||||||
@ -1,51 +1,51 @@
|
|||||||
import { generateRsaKeyPair } from '../activitypub/security'
|
import { generateRsaKeyPair } from '../activitypub/security'
|
||||||
import { activityPub } from '../activitypub/ActivityPub'
|
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 {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreatePost: async (resolve, root, args, context, info) => {
|
// CreatePost: async (resolve, root, args, context, info) => {
|
||||||
args.activityId = activityPub.generateStatusId(context.user.slug)
|
// args.activityId = activityPub.generateStatusId(context.user.slug)
|
||||||
args.objectId = 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 { user: author } = context
|
||||||
const actorId = author.actorId
|
// const actorId = author.actorId
|
||||||
debug(`actorId = ${actorId}`)
|
// debug(`actorId = ${actorId}`)
|
||||||
const createActivity = await new Promise((resolve, reject) => {
|
// const createActivity = await new Promise((resolve, reject) => {
|
||||||
as.create()
|
// as.create()
|
||||||
.id(`${actorId}/status/${args.activityId}`)
|
// .id(`${actorId}/status/${args.activityId}`)
|
||||||
.actor(`${actorId}`)
|
// .actor(`${actorId}`)
|
||||||
.object(
|
// .object(
|
||||||
as
|
// as
|
||||||
.article()
|
// .article()
|
||||||
.id(`${actorId}/status/${post.id}`)
|
// .id(`${actorId}/status/${post.id}`)
|
||||||
.content(post.content)
|
// .content(post.content)
|
||||||
.to('https://www.w3.org/ns/activitystreams#Public')
|
// .to('https://www.w3.org/ns/activitystreams#Public')
|
||||||
.publishedNow()
|
// .publishedNow()
|
||||||
.attributedTo(`${actorId}`),
|
// .attributedTo(`${actorId}`),
|
||||||
)
|
// )
|
||||||
.prettyWrite((err, doc) => {
|
// .prettyWrite((err, doc) => {
|
||||||
if (err) {
|
// if (err) {
|
||||||
reject(err)
|
// reject(err)
|
||||||
} else {
|
// } else {
|
||||||
debug(doc)
|
// debug(doc)
|
||||||
const parsedDoc = JSON.parse(doc)
|
// const parsedDoc = JSON.parse(doc)
|
||||||
parsedDoc.send = true
|
// parsedDoc.send = true
|
||||||
resolve(JSON.stringify(parsedDoc))
|
// resolve(JSON.stringify(parsedDoc))
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
try {
|
// try {
|
||||||
await activityPub.sendActivity(createActivity)
|
// await activityPub.sendActivity(createActivity)
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
debug(`error sending post activity\n${e}`)
|
// debug(`error sending post activity\n${e}`)
|
||||||
}
|
// }
|
||||||
return post
|
// return post
|
||||||
},
|
// },
|
||||||
SignupVerification: async (resolve, root, args, context, info) => {
|
SignupVerification: async (resolve, root, args, context, info) => {
|
||||||
const keys = generateRsaKeyPair()
|
const keys = generateRsaKeyPair()
|
||||||
Object.assign(args, keys)
|
Object.assign(args, keys)
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {
|
|||||||
signupTemplate,
|
signupTemplate,
|
||||||
resetPasswordTemplate,
|
resetPasswordTemplate,
|
||||||
wrongAccountTemplate,
|
wrongAccountTemplate,
|
||||||
} from './templates/templateBuilder'
|
emailVerificationTemplate,
|
||||||
|
} from './templateBuilder'
|
||||||
|
|
||||||
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
|
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
|
||||||
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
|
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
|
||||||
@ -21,7 +22,7 @@ if (!hasEmailConfig) {
|
|||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: CONFIG.SMTP_HOST,
|
host: CONFIG.SMTP_HOST,
|
||||||
port: CONFIG.SMTP_PORT,
|
port: CONFIG.SMTP_PORT,
|
||||||
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
|
ignoreTLS: CONFIG.SMTP_IGNORE_TLS === 'true',
|
||||||
secure: false, // true for 465, false for other ports
|
secure: false, // true for 465, false for other ports
|
||||||
auth: hasAuthData && {
|
auth: hasAuthData && {
|
||||||
user: CONFIG.SMTP_USERNAME,
|
user: CONFIG.SMTP_USERNAME,
|
||||||
@ -57,8 +58,17 @@ const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo)
|
|||||||
return true
|
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 {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
AddEmailAddress: sendEmailVerificationMail,
|
||||||
requestPasswordReset: sendPasswordResetMail,
|
requestPasswordReset: sendPasswordResetMail,
|
||||||
Signup: sendSignupMail,
|
Signup: sendSignupMail,
|
||||||
SignupByInvitation: sendSignupMail,
|
SignupByInvitation: sendSignupMail,
|
||||||
|
|||||||
77
backend/src/middleware/email/templateBuilder.js
Normal file
77
backend/src/middleware/email/templateBuilder.js
Normal 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 },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
186
backend/src/middleware/email/templates/emailVerification.html
Normal file
186
backend/src/middleware/email/templates/emailVerification.html
Normal 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 -->
|
||||||
11
backend/src/middleware/email/templates/index.js
Normal file
11
backend/src/middleware/email/templates/index.js
Normal 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')
|
||||||
196
backend/src/middleware/email/templates/layout.html
Normal file
196
backend/src/middleware/email/templates/layout.html
Normal 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>
|
||||||
@ -1,448 +1,185 @@
|
|||||||
<!DOCTYPE html>
|
<!-- Email Body German : BEGIN -->
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
style="margin: auto;">
|
||||||
|
|
||||||
<head>
|
<!-- Hero Image, Flush : BEGIN -->
|
||||||
<meta charset="utf-8">
|
<tr>
|
||||||
<meta name="viewport" content="width=device-width">
|
<td style="background-color: #ffffff;">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<img
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
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"
|
||||||
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
|
width="600" height="" alt="Human Connection community logo" border="0"
|
||||||
<title>Neues Passwort | Reset Password</title>
|
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 -->
|
||||||
|
|
||||||
<!--[if mso]>
|
<!-- 1 Column Text + Button : BEGIN -->
|
||||||
<style>
|
<tr>
|
||||||
* {
|
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||||
font-family: sans-serif !important;
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||||
}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 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 hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
|
|
||||||
kannst Du innerhalb der nächsten 24 Stunden Dein Passwort zurücksetzen:</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;">Passwort
|
|
||||||
zurücksetzen</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 kein neues Passwort angefordert hast, kannst Du diese E-Mail einfach
|
|
||||||
ignorieren. Wenn Du noch Fragen hast, melde Dich gerne <a href="{{{ supportUrl }}}"
|
|
||||||
style="color: #17b53e;">bei
|
|
||||||
unserem Support Team</a>!</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 forgot your password? No problem! Just click the button below to reset
|
|
||||||
it within the next 24 hours:</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;">Reset
|
|
||||||
password</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 didn't request a new password feel free to ignore this e-mail. 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 -->
|
|
||||||
|
|
||||||
<!-- Email Footer : BEGIN -->
|
|
||||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
|
||||||
style="margin: auto;">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
|
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||||
<br><br>
|
<h1
|
||||||
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
|
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||||
Teck<br>Germany</span>
|
Hallo {{ name }}!</h1>
|
||||||
<br><br>
|
<p style="margin: 0;">Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
|
||||||
|
kannst Du innerhalb der nächsten 24 Stunden Dein Passwort zurücksetzen:</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;">Passwort
|
||||||
|
zurücksetzen</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- Button : END -->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<!-- Email Footer : END -->
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- 1 Column Text + Button : END -->
|
||||||
|
|
||||||
<!--[if mso]>
|
<!-- 1 Column Text : BEGIN -->
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||||
</table>
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||||
<![endif]-->
|
<tr>
|
||||||
</div>
|
<td
|
||||||
|
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||||
<!--[if mso | IE]>
|
<p style="margin: 0;">Falls Du kein neues Passwort angefordert hast, kannst Du diese E-Mail einfach
|
||||||
|
ignorieren. Wenn Du noch Fragen hast, melde Dich gerne <a href="{{{ supportUrl }}}"
|
||||||
|
style="color: #17b53e;">bei
|
||||||
|
unserem Support Team</a>!</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<![endif]-->
|
</td>
|
||||||
</center>
|
</tr>
|
||||||
</body>
|
<!-- 1 Column Text : END -->
|
||||||
|
|
||||||
</html>
|
<!-- 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 forgot your password? No problem! Just click the button below to reset
|
||||||
|
it within the next 24 hours:</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;">Reset
|
||||||
|
password</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 didn't request a new password feel free to ignore this e-mail. 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 -->
|
||||||
|
|||||||
@ -1,485 +1,210 @@
|
|||||||
<!DOCTYPE html>
|
<!-- Email Body German : BEGIN -->
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
style="margin: auto;">
|
||||||
|
|
||||||
<head>
|
<!-- Hero Image, Flush : BEGIN -->
|
||||||
<meta charset="utf-8">
|
<tr>
|
||||||
<meta name="viewport" content="width=device-width">
|
<td style="background-color: #ffffff;">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<img
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
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"
|
||||||
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
|
width="600" height="" alt="Human Connection community logo" border="0"
|
||||||
<title>Willkommen, Bienvenue, Welcome to Human Connection</title>
|
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 -->
|
||||||
|
|
||||||
<!--[if mso]>
|
<!-- 1 Column Text + Button : BEGIN -->
|
||||||
<style>
|
<tr>
|
||||||
* {
|
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||||
font-family: sans-serif !important;
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||||
}
|
|
||||||
</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;">
|
|
||||||
‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ <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>
|
|
||||||
|
|
||||||
<!-- 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;">
|
|
||||||
Willkommen bei Human Connection!</h1>
|
|
||||||
<p style="margin: 0;">Danke, dass Du dich angemeldet hast – wir freuen uns, Dich dabei zu haben. Jetzt
|
|
||||||
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können ... Bitte bestätige
|
|
||||||
Deine E-Mail Adresse:</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;">Bestätige
|
|
||||||
Deine E-Mail Adresse</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<!-- Button : END -->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: center; color :#17b53e">
|
|
||||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
|
||||||
</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; 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>
|
|
||||||
<p style="margin: 0; margin-top: 10px;">PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese
|
|
||||||
E-Mail einfach ignorieren. ;)</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: center; color :#17b53e">
|
|
||||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</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;">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"
|
|
||||||
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;">
|
|
||||||
Welcome to Human Connection!</h1>
|
|
||||||
<p style="margin: 0;">Thank you for joining our cause – it's awesome to have you on board. There's
|
|
||||||
just one tiny step missing before we can start shaping the world together ... Please confirm your
|
|
||||||
e-mail address by clicking the button below:</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;">Confirm
|
|
||||||
your e-mail address</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<!-- Button : END -->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: center; color :#17b53e">
|
|
||||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
|
||||||
</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; 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>
|
|
||||||
<p style="margin: 0; margin-top: 10px;">PS: If you ignore this e-mail we will not create an account
|
|
||||||
for
|
|
||||||
you. ;)</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align: center; color :#17b53e">
|
|
||||||
<p>–––––––––––––––––––––––––––––––––––––––––––––––</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;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
|
|
||||||
support team</a> with any
|
|
||||||
questions you have.</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 -->
|
|
||||||
|
|
||||||
<!-- Email Footer : BEGIN -->
|
|
||||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
|
||||||
style="margin: auto;">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
|
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||||
<br><br>
|
<h1
|
||||||
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
|
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||||
Teck<br>Germany</span>
|
Willkommen bei Human Connection!</h1>
|
||||||
<br><br>
|
<p style="margin: 0;">Danke, dass Du dich angemeldet hast – wir freuen uns, Dich dabei zu haben. Jetzt
|
||||||
|
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können ... Bitte bestätige
|
||||||
|
Deine E-Mail Adresse:</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;">Bestätige
|
||||||
|
Deine E-Mail Adresse</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- Button : END -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center; color :#17b53e">
|
||||||
|
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<!-- Email Footer : END -->
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- 1 Column Text + Button : END -->
|
||||||
|
|
||||||
<!--[if mso]>
|
<!-- 1 Column Text : BEGIN -->
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||||
</table>
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||||
<![endif]-->
|
<tr>
|
||||||
</div>
|
<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"
|
||||||
<!--[if mso | IE]>
|
style="color: #17b53e;">Human Connection</a> angemeldet hast, schau doch mal vorbei!
|
||||||
|
Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.</p>
|
||||||
|
<p style="margin: 0; margin-top: 10px;">PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese
|
||||||
|
E-Mail einfach ignorieren. ;)</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center; color :#17b53e">
|
||||||
|
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<![endif]-->
|
</td>
|
||||||
</center>
|
</tr>
|
||||||
</body>
|
<!-- 1 Column Text : END -->
|
||||||
|
|
||||||
</html>
|
<!-- 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;">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"
|
||||||
|
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;">
|
||||||
|
Welcome to Human Connection!</h1>
|
||||||
|
<p style="margin: 0;">Thank you for joining our cause – it's awesome to have you on board. There's
|
||||||
|
just one tiny step missing before we can start shaping the world together ... Please confirm your
|
||||||
|
e-mail address by clicking the button below:</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;">Confirm
|
||||||
|
your e-mail address</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- Button : END -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center; color :#17b53e">
|
||||||
|
<p>–––––––––––––––––––––––––––––––––––––––––––––––</p>
|
||||||
|
</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; 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>
|
||||||
|
<p style="margin: 0; margin-top: 10px;">PS: If you ignore this e-mail we will not create an account
|
||||||
|
for
|
||||||
|
you. ;)</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center; color :#17b53e">
|
||||||
|
<p>–––––––––––––––––––––––––––––––––––––––––––––––</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;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
|
||||||
|
support team</a> with any
|
||||||
|
questions you have.</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 -->
|
||||||
|
|||||||
@ -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 }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,448 +1,185 @@
|
|||||||
<!DOCTYPE html>
|
<!-- Email Body German : BEGIN -->
|
||||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
style="margin: auto;">
|
||||||
|
|
||||||
<head>
|
<!-- Hero Image, Flush : BEGIN -->
|
||||||
<meta charset="utf-8">
|
<tr>
|
||||||
<meta name="viewport" content="width=device-width">
|
<td style="background-color: #ffffff;">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<img
|
||||||
<meta name="x-apple-disable-message-reformatting">
|
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"
|
||||||
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
|
width="600" height="" alt="Human Connection community logo" border="0"
|
||||||
<title>Falsche Mailadresse? | Wrong E-mail?</title>
|
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 -->
|
||||||
|
|
||||||
<!--[if mso]>
|
<!-- 1 Column Text + Button : BEGIN -->
|
||||||
<style>
|
<tr>
|
||||||
* {
|
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||||
font-family: sans-serif !important;
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||||
}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- 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!</h1>
|
|
||||||
<p style="margin: 0;">Du hast bei uns ein neues Password angefordert – leider haben wir aber keinen
|
|
||||||
Account mit Deiner E-Mailadresse gefunden. Kann es sein, dass Du mit einer anderen Adresse bei uns
|
|
||||||
angemeldet bist?</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;">Versuch'
|
|
||||||
es mit einer anderen E-Mail</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; 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>
|
|
||||||
</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; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
|
||||||
<p style="margin: 0;">Ansonsten hilft Dir <a href="{{{ supportUrl }}}" style="color: #17b53e;">unser
|
|
||||||
Support Team</a> gerne weiter.</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!</h1>
|
|
||||||
<p style="margin: 0;">You requested a password reset but unfortunately we couldn't find an account
|
|
||||||
associated with your e-mail address. Did you maybe use another one when you signed up?</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;">Try
|
|
||||||
a different e-mail</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; 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>
|
|
||||||
</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; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
|
||||||
<p style="margin: 0;">Otherwise <a href="{{{ supportUrl }}}" style="color: #17b53e;">our
|
|
||||||
support team</a> will be happy to help you out.</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 -->
|
|
||||||
|
|
||||||
<!-- Email Footer : BEGIN -->
|
|
||||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
|
||||||
style="margin: auto;">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
|
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||||
<br><br>
|
<h1
|
||||||
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
|
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
|
||||||
Teck<br>Germany</span>
|
Hallo!</h1>
|
||||||
<br><br>
|
<p style="margin: 0;">Du hast bei uns ein neues Password angefordert – leider haben wir aber keinen
|
||||||
|
Account mit Deiner E-Mailadresse gefunden. Kann es sein, dass Du mit einer anderen Adresse bei uns
|
||||||
|
angemeldet bist?</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;">Versuch'
|
||||||
|
es mit einer anderen E-Mail</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- Button : END -->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<!-- Email Footer : END -->
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- 1 Column Text + Button : END -->
|
||||||
|
|
||||||
<!--[if mso]>
|
<!-- 1 Column Text : BEGIN -->
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td style="background-color: #ffffff; padding: 0 20px;">
|
||||||
</table>
|
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||||
<![endif]-->
|
<tr>
|
||||||
</div>
|
<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"
|
||||||
<!--[if mso | IE]>
|
style="color: #17b53e;">Human Connection</a> hast oder Dein Password gar nicht ändern willst,
|
||||||
|
kannst Du diese E-Mail einfach ignorieren!</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<![endif]-->
|
</td>
|
||||||
</center>
|
</tr>
|
||||||
</body>
|
<!-- 1 Column Text : END -->
|
||||||
|
|
||||||
</html>
|
<!-- 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-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||||
|
<p style="margin: 0;">Ansonsten hilft Dir <a href="{{{ supportUrl }}}" style="color: #17b53e;">unser
|
||||||
|
Support Team</a> gerne weiter.</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!</h1>
|
||||||
|
<p style="margin: 0;">You requested a password reset but unfortunately we couldn't find an account
|
||||||
|
associated with your e-mail address. Did you maybe use another one when you signed up?</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;">Try
|
||||||
|
a different e-mail</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; 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>
|
||||||
|
</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; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
|
||||||
|
<p style="margin: 0;">Otherwise <a href="{{{ supportUrl }}}" style="color: #17b53e;">our
|
||||||
|
support team</a> will be happy to help you out.</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 -->
|
||||||
|
|||||||
@ -170,6 +170,8 @@ const permissions = shield(
|
|||||||
block: isAuthenticated,
|
block: isAuthenticated,
|
||||||
unblock: isAuthenticated,
|
unblock: isAuthenticated,
|
||||||
markAsRead: isAuthenticated,
|
markAsRead: isAuthenticated,
|
||||||
|
AddEmailAddress: isAuthenticated,
|
||||||
|
VerifyEmailAddress: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: isMyOwn,
|
email: isMyOwn,
|
||||||
|
|||||||
12
backend/src/models/UnverifiedEmailAddress.js
Normal file
12
backend/src/models/UnverifiedEmailAddress.js
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -49,6 +49,7 @@ module.exports = {
|
|||||||
direction: 'in',
|
direction: 'in',
|
||||||
},
|
},
|
||||||
invitedBy: { type: 'relationship', relationship: 'INVITED', target: 'User', 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() },
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export default {
|
|||||||
User: require('./User.js'),
|
User: require('./User.js'),
|
||||||
InvitationCode: require('./InvitationCode.js'),
|
InvitationCode: require('./InvitationCode.js'),
|
||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
|
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'),
|
||||||
SocialMedia: require('./SocialMedia.js'),
|
SocialMedia: require('./SocialMedia.js'),
|
||||||
Post: require('./Post.js'),
|
Post: require('./Post.js'),
|
||||||
Comment: require('./Comment.js'),
|
Comment: require('./Comment.js'),
|
||||||
|
|||||||
92
backend/src/schema/resolvers/emails.js
Normal file
92
backend/src/schema/resolvers/emails.js
Normal 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'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
298
backend/src/schema/resolvers/emails.spec.js
Normal file
298
backend/src/schema/resolvers/emails.spec.js
Normal 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.' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
26
backend/src/schema/resolvers/helpers/existingEmailAddress.js
Normal file
26
backend/src/schema/resolvers/helpers/existingEmailAddress.js
Normal 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
|
||||||
|
}
|
||||||
4
backend/src/schema/resolvers/helpers/generateNonce.js
Normal file
4
backend/src/schema/resolvers/helpers/generateNonce.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
export default function generateNonce() {
|
||||||
|
return uuid().substring(0, 6)
|
||||||
|
}
|
||||||
@ -1,41 +1,16 @@
|
|||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
import uuid from 'uuid/v4'
|
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
import fileUpload from './fileUpload'
|
import fileUpload from './fileUpload'
|
||||||
import encryptPassword from '../../helpers/encryptPassword'
|
import encryptPassword from '../../helpers/encryptPassword'
|
||||||
|
import generateNonce from './helpers/generateNonce'
|
||||||
|
import existingEmailAddress from './helpers/existingEmailAddress'
|
||||||
|
|
||||||
const instance = neode()
|
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 {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateInvitationCode: async (_parent, args, context, _resolveInfo) => {
|
CreateInvitationCode: async (_parent, args, context, _resolveInfo) => {
|
||||||
args.token = uuid().substring(0, 6)
|
args.token = generateNonce()
|
||||||
const {
|
const {
|
||||||
user: { id: userId },
|
user: { id: userId },
|
||||||
} = context
|
} = context
|
||||||
@ -54,9 +29,9 @@ export default {
|
|||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
Signup: async (_parent, args, context) => {
|
Signup: async (_parent, args, context) => {
|
||||||
const nonce = uuid().substring(0, 6)
|
const nonce = generateNonce()
|
||||||
args.nonce = nonce
|
args.nonce = nonce
|
||||||
let emailAddress = await alreadyExistingMail(_parent, args, context)
|
let emailAddress = await existingEmailAddress(_parent, args, context)
|
||||||
if (emailAddress) return emailAddress
|
if (emailAddress) return emailAddress
|
||||||
try {
|
try {
|
||||||
emailAddress = await instance.create('EmailAddress', args)
|
emailAddress = await instance.create('EmailAddress', args)
|
||||||
@ -67,9 +42,9 @@ export default {
|
|||||||
},
|
},
|
||||||
SignupByInvitation: async (_parent, args, context) => {
|
SignupByInvitation: async (_parent, args, context) => {
|
||||||
const { token } = args
|
const { token } = args
|
||||||
const nonce = uuid().substring(0, 6)
|
const nonce = generateNonce()
|
||||||
args.nonce = nonce
|
args.nonce = nonce
|
||||||
let emailAddress = await alreadyExistingMail(_parent, args, context)
|
let emailAddress = await existingEmailAddress(_parent, args, context)
|
||||||
if (emailAddress) return emailAddress
|
if (emailAddress) return emailAddress
|
||||||
try {
|
try {
|
||||||
const result = await instance.cypher(
|
const result = await instance.cypher(
|
||||||
|
|||||||
@ -257,7 +257,7 @@ describe('SignupByInvitation', () => {
|
|||||||
|
|
||||||
it('throws unique violation error', async () => {
|
it('throws unique violation error', async () => {
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
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 () => {
|
it('is allowed to signup users by email', async () => {
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
data: { Signup: { email: 'someuser@example.org' } },
|
data: { Signup: { email: 'someuser@example.org' } },
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -342,7 +343,7 @@ describe('Signup', () => {
|
|||||||
|
|
||||||
it('throws UserInputError error because of unique constraint violation', async () => {
|
it('throws UserInputError error because of unique constraint violation', async () => {
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
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 () => {
|
it('resolves with the already existing email', async () => {
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
data: { Signup: { email: 'someuser@example.org' } },
|
data: { Signup: { email: 'someuser@example.org' } },
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -359,6 +361,7 @@ describe('Signup', () => {
|
|||||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
data: { Signup: { email: 'someuser@example.org' } },
|
data: { Signup: { email: 'someuser@example.org' } },
|
||||||
|
errors: undefined,
|
||||||
})
|
})
|
||||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login, gql } from '../../jest/helpers'
|
import { gql } from '../../jest/helpers'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import createServer from '../../server'
|
||||||
|
|
||||||
let clientUser1, clientUser2
|
let mutate, query, authenticatedUser, variables
|
||||||
let headersUser1, headersUser2
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
const instance = neode()
|
const instance = getNeode()
|
||||||
const categoryIds = ['cat9']
|
const driver = getDriver()
|
||||||
|
|
||||||
const mutationShoutPost = gql`
|
const mutationShoutPost = gql`
|
||||||
mutation($id: ID!) {
|
mutation($id: ID!) {
|
||||||
@ -19,141 +19,136 @@ const mutationUnshoutPost = gql`
|
|||||||
unshout(id: $id, type: Post)
|
unshout(id: $id, type: Post)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const createPostMutation = gql`
|
const queryPost = gql`
|
||||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) {
|
query($id: ID!) {
|
||||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
Post(id: $id) {
|
||||||
id
|
id
|
||||||
title
|
shoutedBy {
|
||||||
content
|
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',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
await factory.create('User', {
|
|
||||||
id: 'u2',
|
|
||||||
email: 'test2@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)
|
describe('shout and unshout posts', () => {
|
||||||
await clientUser2.request(createPostMutation, {
|
let currentUser, postAuthor
|
||||||
id: 'p12345',
|
beforeAll(() => {
|
||||||
title: 'Post Title 12345',
|
authenticatedUser = undefined
|
||||||
content: 'Some Post Content 12345',
|
const { server } = createServer({
|
||||||
categoryIds,
|
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',
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
postAuthor = await factory.create('User', {
|
||||||
await factory.cleanDatabase()
|
id: 'id-of-another-user',
|
||||||
})
|
name: 'Another User',
|
||||||
|
email: 'another.user@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
describe('shout', () => {
|
describe('shout', () => {
|
||||||
describe('shout foreign post', () => {
|
describe('unauthenticated', () => {
|
||||||
describe('unauthenticated shout', () => {
|
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
const client = new GraphQLClient(host)
|
variables = { id: 'post-to-shout-id' }
|
||||||
await expect(client.request(mutationShoutPost, { id: 'p1234' })).rejects.toThrow(
|
authenticatedUser = undefined
|
||||||
'Not Authorised',
|
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('can not shout my own post', async () => {
|
||||||
|
variables = { id: 'current-user-post-id' }
|
||||||
|
await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({
|
||||||
|
data: { shout: false },
|
||||||
|
})
|
||||||
|
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
|
||||||
|
data: { Post: [{ id: 'current-user-post-id', shoutedBy: [] }] },
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('unshout', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
authenticatedUser = undefined
|
||||||
|
variables = { id: 'post-to-shout-id' }
|
||||||
|
await expect(mutate({ mutation: mutationUnshoutPost, variables })).resolves.toMatchObject({
|
||||||
|
errors: [{ message: 'Not Authorised!' }],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('I shout a post of another user', async () => {
|
describe('authenticated', () => {
|
||||||
const res = await clientUser1.request(mutationShoutPost, { id: 'p12345' })
|
beforeEach(async () => {
|
||||||
const expected = {
|
authenticatedUser = await currentUser.toJson()
|
||||||
shout: true,
|
await factory.create('Post', {
|
||||||
}
|
name: 'Posted By Another User',
|
||||||
expect(res).toMatchObject(expected)
|
id: 'posted-by-another-user',
|
||||||
|
author: postAuthor,
|
||||||
const { Post } = await clientUser1.request(gql`
|
})
|
||||||
query {
|
await mutate({
|
||||||
Post(id: "p12345") {
|
mutation: mutationShoutPost,
|
||||||
shoutedByCurrentUser
|
variables: { id: 'posted-by-another-user' },
|
||||||
}
|
})
|
||||||
}
|
|
||||||
`)
|
|
||||||
const expected2 = {
|
|
||||||
shoutedByCurrentUser: true,
|
|
||||||
}
|
|
||||||
expect(Post[0]).toMatchObject(expected2)
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('unshout foreign post', () => {
|
|
||||||
describe('unauthenticated shout', () => {
|
|
||||||
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',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
it('I unshout a post of another user', async () => {
|
it("can unshout another user's post", async () => {
|
||||||
// shout
|
variables = { id: 'posted-by-another-user' }
|
||||||
await clientUser1.request(mutationShoutPost, { id: 'p12345' })
|
await expect(mutate({ mutation: mutationUnshoutPost, variables })).resolves.toMatchObject({
|
||||||
const expected = {
|
data: { unshout: true },
|
||||||
unshout: true,
|
})
|
||||||
}
|
await expect(query({ query: queryPost, variables })).resolves.toMatchObject({
|
||||||
// unshout
|
data: { Post: [{ id: 'posted-by-another-user', shoutedBy: [] }] },
|
||||||
const res = await clientUser1.request(mutationUnshoutPost, { id: 'p12345' })
|
errors: undefined,
|
||||||
expect(res).toMatchObject(expected)
|
})
|
||||||
|
})
|
||||||
const { Post } = await clientUser1.request(gql`
|
|
||||||
query {
|
|
||||||
Post(id: "p12345") {
|
|
||||||
shoutedByCurrentUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
const expected2 = {
|
|
||||||
shoutedByCurrentUser: false,
|
|
||||||
}
|
|
||||||
expect(Post[0]).toMatchObject(expected2)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default {
|
|||||||
countPosts: 'Post',
|
countPosts: 'Post',
|
||||||
countComments: 'Comment',
|
countComments: 'Comment',
|
||||||
countNotifications: 'NOTIFIED',
|
countNotifications: 'NOTIFIED',
|
||||||
countInvites: 'InvitationCode',
|
countEmails: 'EmailAddress',
|
||||||
countFollows: 'FOLLOWS',
|
countFollows: 'FOLLOWS',
|
||||||
countShouts: 'SHOUTED',
|
countShouts: 'SHOUTED',
|
||||||
}
|
}
|
||||||
@ -28,6 +28,11 @@ export default {
|
|||||||
const stat = statistics[mapping[key]]
|
const stat = statistics[mapping[key]]
|
||||||
response[key] = stat ? stat.toNumber() : 0
|
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 {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export const getBlockedUsers = async context => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getBlockedByUsers = async context => {
|
export const getBlockedByUsers = async context => {
|
||||||
|
if (context.user.role === 'moderator' || context.user.role === 'admin') return []
|
||||||
const { neode } = context
|
const { neode } = context
|
||||||
const userModel = neode.model('User')
|
const userModel = neode.model('User')
|
||||||
let blockedByUsers = neode
|
let blockedByUsers = neode
|
||||||
|
|||||||
@ -20,4 +20,9 @@ type Mutation {
|
|||||||
about: String
|
about: String
|
||||||
termsAndConditionsAgreedVersion: String!
|
termsAndConditionsAgreedVersion: String!
|
||||||
): User
|
): User
|
||||||
|
AddEmailAddress(email: String!): EmailAddress
|
||||||
|
VerifyEmailAddress(
|
||||||
|
nonce: String!
|
||||||
|
email: String!
|
||||||
|
): EmailAddress
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,21 @@
|
|||||||
import faker from 'faker'
|
import faker from 'faker'
|
||||||
|
|
||||||
|
export function defaults({ args }) {
|
||||||
|
const defaults = {
|
||||||
|
email: faker.internet.email(),
|
||||||
|
verifiedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
args = {
|
||||||
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
export default function create() {
|
export default function create() {
|
||||||
return {
|
return {
|
||||||
factory: async ({ args, neodeInstance }) => {
|
factory: async ({ args, neodeInstance }) => {
|
||||||
const defaults = {
|
args = defaults({ args })
|
||||||
email: faker.internet.email(),
|
|
||||||
verifiedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
args = {
|
|
||||||
...defaults,
|
|
||||||
...args,
|
|
||||||
}
|
|
||||||
return neodeInstance.create('EmailAddress', args)
|
return neodeInstance.create('EmailAddress', args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import createTag from './tags.js'
|
|||||||
import createSocialMedia from './socialMedia.js'
|
import createSocialMedia from './socialMedia.js'
|
||||||
import createLocation from './locations.js'
|
import createLocation from './locations.js'
|
||||||
import createEmailAddress from './emailAddresses.js'
|
import createEmailAddress from './emailAddresses.js'
|
||||||
|
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
|
||||||
|
|
||||||
export const seedServerHost = 'http://127.0.0.1:4001'
|
export const seedServerHost = 'http://127.0.0.1:4001'
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ const factories = {
|
|||||||
SocialMedia: createSocialMedia,
|
SocialMedia: createSocialMedia,
|
||||||
Location: createLocation,
|
Location: createLocation,
|
||||||
EmailAddress: createEmailAddress,
|
EmailAddress: createEmailAddress,
|
||||||
|
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cleanDatabase = async (options = {}) => {
|
export const cleanDatabase = async (options = {}) => {
|
||||||
|
|||||||
10
backend/src/seed/factories/unverifiedEmailAddresses.js
Normal file
10
backend/src/seed/factories/unverifiedEmailAddresses.js
Normal 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
@ -5,7 +5,7 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
|
|||||||
let lastReportTitle
|
let lastReportTitle
|
||||||
let davidIrvingPostTitle = 'The Truth about the Holocaust'
|
let davidIrvingPostTitle = 'The Truth about the Holocaust'
|
||||||
let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
|
let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
|
||||||
let davidIrvingName = 'David Irving'
|
let annoyingUserWhoBlockedModeratorTitle = 'Fake news'
|
||||||
|
|
||||||
const savePostTitle = $post => {
|
const savePostTitle = $post => {
|
||||||
return $post
|
return $post
|
||||||
@ -116,7 +116,7 @@ When(/^I confirm the reporting dialog .*:$/, message => {
|
|||||||
Given('somebody reported the following posts:', table => {
|
Given('somebody reported the following posts:', table => {
|
||||||
table.hashes().forEach(({ id }) => {
|
table.hashes().forEach(({ id }) => {
|
||||||
const submitter = {
|
const submitter = {
|
||||||
email: `submitter${id}@example.org`,
|
email: `submitter${id}@example.org`,
|
||||||
password: '1234'
|
password: '1234'
|
||||||
}
|
}
|
||||||
cy.factory()
|
cy.factory()
|
||||||
@ -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', () => {
|
Then('each list item links to the post page', () => {
|
||||||
cy.contains(davidIrvingPostTitle).click()
|
cy.contains(davidIrvingPostTitle).click()
|
||||||
cy.location('pathname').should('contain', '/post')
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|||||||
@ -351,10 +351,12 @@ When("I log in with the following credentials:", table => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When("open the notification menu and click on the first item", () => {
|
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")
|
cy.get(".notification-mention-post")
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click({
|
||||||
|
force: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("see {int} unread notifications in the top menu", count => {
|
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 => {
|
Given("I am on the profile page of the annoying user", name => {
|
||||||
cy.openPage("/profile/annoying-user/spammy-spammer");
|
cy.openPage("/profile/annoying-user/spammy-spammer");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,13 +8,15 @@ Feature: Report and Moderate
|
|||||||
So I can look into it and decide what to do
|
So I can look into it and decide what to do
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
Given we have this user in our database:
|
Given we have the following user accounts:
|
||||||
| id | name |
|
| 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:
|
Given we have the following posts in our database:
|
||||||
| authorId | id | title | content |
|
| authorId | id | title | content |
|
||||||
| u67 | p1 | The Truth about the Holocaust | It never existed! |
|
| 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
|
Scenario Outline: Report a post from various pages
|
||||||
Given I am logged in with a "user" role
|
Given I am logged in with a "user" role
|
||||||
When I see David Irving's post on the <Page>
|
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
|
Then I see all the reported posts including the one from above
|
||||||
And each list item links to the post page
|
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
|
Scenario: Normal user can't see the moderation page
|
||||||
Given I am logged in with a "user" role
|
Given I am logged in with a "user" role
|
||||||
When I click on the avatar menu in the top right corner
|
When I click on the avatar menu in the top right corner
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
Feature: Notifications for a mentions
|
Feature: Notification for a mention
|
||||||
As a user
|
As a user
|
||||||
I want to be notified if sb. mentions me in a post or comment
|
I want to be notified if sb. mentions me in a post or comment
|
||||||
In order join conversations about or related to me
|
In order join conversations about or related to me
|
||||||
|
|||||||
57
deployment/volumes/neo4j-online-backup/README.md
Normal file
57
deployment/volumes/neo4j-online-backup/README.md
Normal 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.
|
||||||
@ -10,6 +10,7 @@ services:
|
|||||||
- "BUILD_COMMIT=${TRAVIS_COMMIT}"
|
- "BUILD_COMMIT=${TRAVIS_COMMIT}"
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
|
- 3002:3002
|
||||||
networks:
|
networks:
|
||||||
- hc-network
|
- hc-network
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -55,12 +56,13 @@ services:
|
|||||||
- hc-network
|
- hc-network
|
||||||
environment:
|
environment:
|
||||||
- NEO4J_AUTH=none
|
- NEO4J_AUTH=none
|
||||||
|
- NEO4J_dbms_security_procedures_unrestricted=algo.*,apoc.*
|
||||||
|
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
|
||||||
ports:
|
ports:
|
||||||
- 7687:7687
|
- 7687:7687
|
||||||
- 7474:7474
|
- 7474:7474
|
||||||
volumes:
|
volumes:
|
||||||
- neo4j_data:/data
|
- neo4j_data:/data
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
hc-network:
|
hc-network:
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@ -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)"
|
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
|
ARG BUILD_COMMIT
|
||||||
|
|||||||
@ -21,10 +21,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"codecov": "^3.6.1",
|
"codecov": "^3.6.1",
|
||||||
"cross-env": "^6.0.0",
|
"cross-env": "^6.0.3",
|
||||||
"cypress": "^3.4.1",
|
"cypress": "^3.4.1",
|
||||||
"cypress-cucumber-preprocessor": "^1.16.0",
|
"cypress-cucumber-preprocessor": "^1.16.1",
|
||||||
"cypress-file-upload": "^3.3.3",
|
"cypress-file-upload": "^3.3.4",
|
||||||
"cypress-plugin-retries": "^1.3.0",
|
"cypress-plugin-retries": "^1.3.0",
|
||||||
"dotenv": "^8.1.0",
|
"dotenv": "^8.1.0",
|
||||||
"faker": "Marak/faker.js#master",
|
"faker": "Marak/faker.js#master",
|
||||||
|
|||||||
@ -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)"
|
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
|
EXPOSE 3000
|
||||||
|
|||||||
@ -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)"
|
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
|
EXPOSE 3000
|
||||||
|
|||||||
@ -33,6 +33,42 @@ $ yarn build
|
|||||||
$ yarn start
|
$ 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. 
|
||||||
|
|
||||||
|
{% 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
## Styleguide
|
||||||
|
|
||||||
All reusable Components \(for example avatar\) should be done inside the [Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide) repository.
|
All reusable Components \(for example avatar\) should be done inside the [Nitro-Styleguide](https://github.com/Human-Connection/Nitro-Styleguide) repository.
|
||||||
|
|||||||
10
webapp/app/router.scrollBehavior.js
Normal file
10
webapp/app/router.scrollBehavior.js
Normal 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 }
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ describe('Comment.vue', () => {
|
|||||||
truncate: a => a,
|
truncate: a => a,
|
||||||
removeHtml: a => a,
|
removeHtml: a => a,
|
||||||
},
|
},
|
||||||
|
$scrollTo: jest.fn(),
|
||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest.fn().mockResolvedValue({
|
mutate: jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@ -51,6 +52,8 @@ describe('Comment.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('shallowMount', () => {
|
describe('shallowMount', () => {
|
||||||
|
beforeEach(jest.useFakeTimers)
|
||||||
|
|
||||||
Wrapper = () => {
|
Wrapper = () => {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
getters,
|
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', () => {
|
describe('test callbacks', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -10,10 +10,15 @@
|
|||||||
</ds-card>
|
</ds-card>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
|
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
|
||||||
<ds-card :id="`commentId-${comment.id}`">
|
<ds-card :id="anchor">
|
||||||
<ds-space margin-bottom="small" margin-top="base">
|
<ds-space margin-bottom="small" margin-top="small">
|
||||||
<hc-user :user="author" :date-time="comment.createdAt" />
|
<hc-user :user="author" :date-time="comment.createdAt">
|
||||||
<!-- Content Menu (can open Modals) -->
|
<template v-slot:dateTime>
|
||||||
|
<ds-text v-if="comment.createdAt !== comment.updatedAt">
|
||||||
|
({{ $t('comment.edited') }})
|
||||||
|
</ds-text>
|
||||||
|
</template>
|
||||||
|
</hc-user>
|
||||||
<client-only>
|
<client-only>
|
||||||
<content-menu
|
<content-menu
|
||||||
v-show="!openEditCommentMenu"
|
v-show="!openEditCommentMenu"
|
||||||
@ -37,7 +42,7 @@
|
|||||||
@collapse="isCollapsed = true"
|
@collapse="isCollapsed = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!openEditCommentMenu">
|
<div v-else>
|
||||||
<content-viewer
|
<content-viewer
|
||||||
v-if="$filters.removeHtml(comment.content).length < 180"
|
v-if="$filters.removeHtml(comment.content).length < 180"
|
||||||
:content="comment.content"
|
:content="comment.content"
|
||||||
@ -80,8 +85,10 @@ import ContentMenu from '~/components/ContentMenu'
|
|||||||
import ContentViewer from '~/components/Editor/ContentViewer'
|
import ContentViewer from '~/components/Editor/ContentViewer'
|
||||||
import HcCommentForm from '~/components/CommentForm/CommentForm'
|
import HcCommentForm from '~/components/CommentForm/CommentForm'
|
||||||
import CommentMutations from '~/graphql/CommentMutations'
|
import CommentMutations from '~/graphql/CommentMutations'
|
||||||
|
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [scrollToAnchor],
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
@ -109,6 +116,9 @@ export default {
|
|||||||
user: 'auth/user',
|
user: 'auth/user',
|
||||||
isModerator: 'auth/isModerator',
|
isModerator: 'auth/isModerator',
|
||||||
}),
|
}),
|
||||||
|
anchor() {
|
||||||
|
return `commentId-${this.comment.id}`
|
||||||
|
},
|
||||||
displaysComment() {
|
displaysComment() {
|
||||||
return !this.unavailable || this.isModerator
|
return !this.unavailable || this.isModerator
|
||||||
},
|
},
|
||||||
@ -142,6 +152,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkAnchor(anchor) {
|
||||||
|
return `#${this.anchor}` === anchor
|
||||||
|
},
|
||||||
isAuthor(id) {
|
isAuthor(id) {
|
||||||
return this.user.id === id
|
return this.user.id === id
|
||||||
},
|
},
|
||||||
|
|||||||
@ -42,6 +42,7 @@ describe('CommentList.vue', () => {
|
|||||||
truncate: a => a,
|
truncate: a => a,
|
||||||
removeHtml: a => a,
|
removeHtml: a => a,
|
||||||
},
|
},
|
||||||
|
$scrollTo: jest.fn(),
|
||||||
$apollo: {
|
$apollo: {
|
||||||
queries: {
|
queries: {
|
||||||
Post: {
|
Post: {
|
||||||
@ -65,12 +66,46 @@ describe('CommentList.vue', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
it('displays a comments counter', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
|
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays a comments counter', () => {
|
it('displays a comments counter', () => {
|
||||||
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('span.ds-tag').text()).toEqual('1')
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -30,8 +30,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Comment from '~/components/Comment/Comment'
|
import Comment from '~/components/Comment/Comment'
|
||||||
|
import scrollToAnchor from '~/mixins/scrollToAnchor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [scrollToAnchor],
|
||||||
components: {
|
components: {
|
||||||
Comment,
|
Comment,
|
||||||
},
|
},
|
||||||
@ -39,6 +41,9 @@ export default {
|
|||||||
post: { type: Object, default: () => {} },
|
post: { type: Object, default: () => {} },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkAnchor(anchor) {
|
||||||
|
return anchor === '#comments'
|
||||||
|
},
|
||||||
updateCommentList(updatedComment) {
|
updateCommentList(updatedComment) {
|
||||||
this.post.comments = this.post.comments.map(comment => {
|
this.post.comments = this.post.comments.map(comment => {
|
||||||
return comment.id === updatedComment.id ? updatedComment : comment
|
return comment.id === updatedComment.id ? updatedComment : comment
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export default {
|
|||||||
|
|
||||||
const followedUser = follow ? data.followUser : data.unfollowUser
|
const followedUser = follow ? data.followUser : data.unfollowUser
|
||||||
this.$emit('update', followedUser)
|
this.$emit('update', followedUser)
|
||||||
} catch {
|
} catch (err) {
|
||||||
optimisticResult.followedByCurrentUser = !follow
|
optimisticResult.followedByCurrentUser = !follow
|
||||||
this.$emit('optimistic', optimisticResult)
|
this.$emit('optimistic', optimisticResult)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,7 +115,7 @@ describe('Signup', () => {
|
|||||||
mocks.$apollo.mutate = jest
|
mocks.$apollo.mutate = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValue(
|
.mockRejectedValue(
|
||||||
new Error('UserInputError: User account with this email already exists.'),
|
new Error('UserInputError: A user account with this email already exists.'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export default {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const { message } = err
|
const { message } = err
|
||||||
const mapping = {
|
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',
|
'Invitation code already used or does not exist': 'invalid-invitation-token',
|
||||||
}
|
}
|
||||||
for (const [pattern, key] of Object.entries(mapping)) {
|
for (const [pattern, key] of Object.entries(mapping)) {
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const user = {
|
|||||||
storiesOf('User', module)
|
storiesOf('User', module)
|
||||||
.addDecorator(withA11y)
|
.addDecorator(withA11y)
|
||||||
.addDecorator(helpers.layout)
|
.addDecorator(helpers.layout)
|
||||||
.add('available user', () => ({
|
.add('available', () => ({
|
||||||
components: { User },
|
components: { User },
|
||||||
store: helpers.store,
|
store: helpers.store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -64,7 +64,21 @@ storiesOf('User', module)
|
|||||||
}),
|
}),
|
||||||
template: '<user :user="user" :trunc="35" :date-time="new Date()" />',
|
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 },
|
components: { User },
|
||||||
store: helpers.store,
|
store: helpers.store,
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
|||||||
@ -11,17 +11,17 @@
|
|||||||
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
|
<div @mouseover="openMenu(true)" @mouseleave="closeMenu(true)">
|
||||||
<hc-avatar class="avatar" :user="user" />
|
<hc-avatar class="avatar" :user="user" />
|
||||||
<div>
|
<div>
|
||||||
<ds-text align="left">
|
<ds-text>
|
||||||
<b class="username">{{ userName | truncate(18) }}</b>
|
<b class="username">{{ userName | truncate(18) }}</b>
|
||||||
<ds-text v-if="dateTime" size="small" color="soft">
|
<ds-text v-if="dateTime" size="small" color="soft">
|
||||||
<ds-icon name="clock" />
|
<ds-icon name="clock" />
|
||||||
<client-only>
|
<client-only>
|
||||||
<hc-relative-date-time :date-time="dateTime" />
|
<hc-relative-date-time :date-time="dateTime" />
|
||||||
</client-only>
|
</client-only>
|
||||||
|
<slot name="dateTime"></slot>
|
||||||
</ds-text>
|
</ds-text>
|
||||||
</ds-text>
|
</ds-text>
|
||||||
</div>
|
</div>
|
||||||
<!-- Time -->
|
|
||||||
<ds-text align="left" size="small" color="soft">
|
<ds-text align="left" size="small" color="soft">
|
||||||
{{ userSlug }}
|
{{ userSlug }}
|
||||||
</ds-text>
|
</ds-text>
|
||||||
|
|||||||
@ -69,16 +69,16 @@ describe('Notification', () => {
|
|||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||||
'notifications.menu.commented_on_post',
|
'notifications.reason.commented_on_post',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
it('renders title', () => {
|
it('renders title', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.text()).toContain("It's a post title")
|
expect(wrapper.text()).toContain("It's a post title")
|
||||||
})
|
})
|
||||||
it('renders the "Comment:"', () => {
|
it('renders the identifier "notifications.comment"', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.text()).toContain('Comment:')
|
expect(wrapper.text()).toContain('notifications.comment')
|
||||||
})
|
})
|
||||||
it('renders the contentExcerpt', () => {
|
it('renders the contentExcerpt', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
@ -119,7 +119,7 @@ describe('Notification', () => {
|
|||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||||
'notifications.menu.mentioned_in_post',
|
'notifications.reason.mentioned_in_post',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
it('renders title', () => {
|
it('renders title', () => {
|
||||||
@ -169,7 +169,7 @@ describe('Notification', () => {
|
|||||||
it('renders reason', () => {
|
it('renders reason', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||||
'notifications.menu.mentioned_in_comment',
|
'notifications.reason.mentioned_in_comment',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
it('renders title', () => {
|
it('renders title', () => {
|
||||||
@ -177,9 +177,9 @@ describe('Notification', () => {
|
|||||||
expect(wrapper.text()).toContain("It's a post title")
|
expect(wrapper.text()).toContain("It's a post title")
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the "Comment:"', () => {
|
it('renders the identifier "notifications.comment"', () => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.text()).toContain('Comment:')
|
expect(wrapper.text()).toContain('notifications.comment')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the contentExcerpt', () => {
|
it('renders the contentExcerpt', () => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
|
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<ds-text class="reason-text-for-test" color="soft">
|
<ds-text class="reason-text-for-test" color="soft">
|
||||||
{{ $t(`notifications.menu.${notification.reason}`) }}
|
{{ $t(`notifications.reason.${notification.reason}`) }}
|
||||||
</ds-text>
|
</ds-text>
|
||||||
</client-only>
|
</client-only>
|
||||||
<ds-space margin-bottom="x-small" />
|
<ds-space margin-bottom="x-small" />
|
||||||
@ -23,7 +23,9 @@
|
|||||||
>
|
>
|
||||||
<ds-space margin-bottom="x-small" />
|
<ds-space margin-bottom="x-small" />
|
||||||
<div>
|
<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 }}
|
{{ from.contentExcerpt | removeHtml }}
|
||||||
</div>
|
</div>
|
||||||
</ds-card>
|
</ds-card>
|
||||||
|
|||||||
@ -46,11 +46,46 @@ describe('NotificationMenu.vue', () => {
|
|||||||
expect(wrapper.contains('.dropdown')).toBe(false)
|
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', () => {
|
describe('given some notifications', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
data = () => {
|
data = () => {
|
||||||
return {
|
return {
|
||||||
notifications: [
|
displayedNotifications: [
|
||||||
{
|
{
|
||||||
id: 'notification-41',
|
id: 'notification-41',
|
||||||
read: false,
|
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()
|
wrapper = Wrapper()
|
||||||
expect(wrapper.find('ds-button-stub').text()).toEqual('2')
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-button v-if="totalNotifications <= 0" class="notifications-menu" disabled icon="bell">
|
<ds-button v-if="!notificationsCount" class="notifications-menu" disabled icon="bell">
|
||||||
{{ totalNotifications }}
|
{{ unreadNotificationsCount }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
<dropdown v-else class="notifications-menu" :placement="placement">
|
<dropdown v-else class="notifications-menu" :placement="placement">
|
||||||
<template slot="default" slot-scope="{ toggleMenu }">
|
<template slot="default" slot-scope="{ toggleMenu }">
|
||||||
<ds-button primary icon="bell" @click.prevent="toggleMenu">
|
<ds-button :primary="!!unreadNotificationsCount" icon="bell" @click.prevent="toggleMenu">
|
||||||
{{ totalNotifications }}
|
{{ unreadNotificationsCount }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
</template>
|
</template>
|
||||||
<template slot="popover">
|
<template slot="popover">
|
||||||
<div class="notifications-menu-popover">
|
<div class="notifications-menu-popover">
|
||||||
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
|
<notification-list :notifications="displayedNotifications" @markAsRead="markAsRead" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</dropdown>
|
</dropdown>
|
||||||
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications'
|
||||||
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
|
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
|
||||||
import NotificationList from '../NotificationList/NotificationList'
|
import NotificationList from '../NotificationList/NotificationList'
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
displayedNotifications: [],
|
||||||
notifications: [],
|
notifications: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -46,17 +48,29 @@ export default {
|
|||||||
variables,
|
variables,
|
||||||
})
|
})
|
||||||
if (!(markAsRead && markAsRead.read === true)) return
|
if (!(markAsRead && markAsRead.read === true)) return
|
||||||
this.notifications = this.notifications.map(n => {
|
this.displayedNotifications = this.displayedNotifications.map(n => {
|
||||||
return n.from.id === markAsRead.from.id ? markAsRead : n
|
return this.equalNotification(n, markAsRead) ? markAsRead : n
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} 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: {
|
computed: {
|
||||||
totalNotifications() {
|
notificationsCount() {
|
||||||
return (this.notifications || []).length
|
return (this.displayedNotifications || []).length
|
||||||
|
},
|
||||||
|
unreadNotificationsCount() {
|
||||||
|
let countUnread = 0
|
||||||
|
if (this.displayedNotifications) {
|
||||||
|
this.displayedNotifications.forEach(notification => {
|
||||||
|
if (!notification.read) countUnread++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return countUnread
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
@ -64,6 +78,23 @@ export default {
|
|||||||
query() {
|
query() {
|
||||||
return notificationQuery(this.$i18n)
|
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)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
1
webapp/constants/notifications.js
Normal file
1
webapp/constants/notifications.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const NOTIFICATIONS_POLL_INTERVAL = 60000
|
||||||
@ -10,6 +10,7 @@ export default i18n => {
|
|||||||
contentExcerpt
|
contentExcerpt
|
||||||
content
|
content
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
author {
|
author {
|
||||||
@ -39,6 +40,7 @@ export default i18n => {
|
|||||||
contentExcerpt
|
contentExcerpt
|
||||||
content
|
content
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
author {
|
author {
|
||||||
|
|||||||
20
webapp/graphql/EmailAddress.js
Normal file
20
webapp/graphql/EmailAddress.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -40,6 +40,7 @@ export const postFragment = lang => gql`
|
|||||||
content
|
content
|
||||||
contentExcerpt
|
contentExcerpt
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
slug
|
slug
|
||||||
@ -64,6 +65,7 @@ export const commentFragment = lang => gql`
|
|||||||
fragment comment on Comment {
|
fragment comment on Comment {
|
||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
disabled
|
disabled
|
||||||
deleted
|
deleted
|
||||||
content
|
content
|
||||||
|
|||||||
@ -135,11 +135,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"menu": {
|
"reason": {
|
||||||
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
|
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
|
||||||
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
||||||
"commented_on_post": "Hat deinen Beitrag kommentiert …"
|
"commented_on_post": "Hat deinen Beitrag kommentiert …"
|
||||||
}
|
},
|
||||||
|
"comment": "Kommentar"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Suchen",
|
"placeholder": "Suchen",
|
||||||
@ -157,6 +158,27 @@
|
|||||||
"labelBio": "Über dich",
|
"labelBio": "Über dich",
|
||||||
"success": "Deine Daten wurden erfolgreich aktualisiert!"
|
"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": {
|
"validation": {
|
||||||
"slug": {
|
"slug": {
|
||||||
"regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt.",
|
"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-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."
|
"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": {
|
"comment": {
|
||||||
"content": {
|
"content": {
|
||||||
@ -338,10 +361,11 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"more": "mehr anzeigen",
|
"more": "mehr anzeigen",
|
||||||
"less": "weniger anzeigen"
|
"less": "weniger anzeigen"
|
||||||
}
|
},
|
||||||
|
"edited": "bearbeitet"
|
||||||
},
|
},
|
||||||
"quotes": {
|
"quotes": {
|
||||||
"african": {
|
"african": {
|
||||||
"quote": "Viele kleine Leute an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.",
|
"quote": "Viele kleine Leute an vielen kleinen Orten, die viele kleine Dinge tun, werden das Antlitz dieser Welt verändern.",
|
||||||
"author": "Afrikanisches Sprichwort"
|
"author": "Afrikanisches Sprichwort"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,11 +136,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"menu": {
|
"reason": {
|
||||||
"mentioned_in_post": "Mentioned you in a post …",
|
"mentioned_in_post": "Mentioned you in a post …",
|
||||||
"mentioned_in_comment": "Mentioned you in a comment …",
|
"mentioned_in_comment": "Mentioned you in a comment …",
|
||||||
"commented_on_post": "Commented on your post …"
|
"commented_on_post": "Commented on your post …"
|
||||||
}
|
},
|
||||||
|
"comment": "Comment"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Search",
|
"placeholder": "Search",
|
||||||
@ -158,6 +159,27 @@
|
|||||||
"labelBio": "About You",
|
"labelBio": "About You",
|
||||||
"success": "Your data was successfully updated!"
|
"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": {
|
"validation": {
|
||||||
"slug": {
|
"slug": {
|
||||||
"regex": "Allowed characters are only lowercase letters, numbers, underscores and hyphens.",
|
"regex": "Allowed characters are only lowercase letters, numbers, underscores and hyphens.",
|
||||||
@ -253,7 +275,7 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"name": "Users",
|
"name": "Users",
|
||||||
"form": {
|
"form": {
|
||||||
"placeholder": "E-Mail, name or description"
|
"placeholder": "email, name or description"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"columns": {
|
"columns": {
|
||||||
@ -327,6 +349,7 @@
|
|||||||
"third-party-false": "It automatically integrates <b style='color:red'>no </b> third-party providers' service.",
|
"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."
|
"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": {
|
"comment": {
|
||||||
"content": {
|
"content": {
|
||||||
@ -339,7 +362,8 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"more": "show more",
|
"more": "show more",
|
||||||
"less": "show less"
|
"less": "show less"
|
||||||
}
|
},
|
||||||
|
"edited": "edited"
|
||||||
},
|
},
|
||||||
"quotes": {
|
"quotes": {
|
||||||
"african": {
|
"african": {
|
||||||
|
|||||||
@ -118,7 +118,11 @@
|
|||||||
},
|
},
|
||||||
"takeAction": {
|
"takeAction": {
|
||||||
"name": "Tomar uma ação"
|
"name": "Tomar uma ação"
|
||||||
}
|
},
|
||||||
|
"comment": {
|
||||||
|
"submit": "Commentar"
|
||||||
|
},
|
||||||
|
"edited": "editado"
|
||||||
},
|
},
|
||||||
"quotes": {
|
"quotes": {
|
||||||
"african": {
|
"african": {
|
||||||
@ -202,8 +206,18 @@
|
|||||||
"delete": "Apagar Contribuição"
|
"delete": "Apagar Contribuição"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"edit": "Editar Comentário",
|
"content": {
|
||||||
"delete": "Apagar Comentário"
|
"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": {
|
"followButton": {
|
||||||
"follow": "Seguir",
|
"follow": "Seguir",
|
||||||
@ -212,4 +226,4 @@
|
|||||||
"shoutButton": {
|
"shoutButton": {
|
||||||
"shouted": "Aclamou"
|
"shouted": "Aclamou"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,8 @@
|
|||||||
<ds-text>{{ $t('maintenance.explanation') }}</ds-text>
|
<ds-text>{{ $t('maintenance.explanation') }}</ds-text>
|
||||||
<ds-text>
|
<ds-text>
|
||||||
{{ $t('maintenance.questions') }}
|
{{ $t('maintenance.questions') }}
|
||||||
<a href="mailto:info@human-connection.org" class="email-link">
|
<a href="mailto:support@human-connection.org" class="email-link">
|
||||||
info@human-connection.org
|
support@human-connection.org
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</ds-text>
|
</ds-text>
|
||||||
|
|||||||
23
webapp/mixins/scrollToAnchor.js
Normal file
23
webapp/mixins/scrollToAnchor.js
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -124,80 +124,6 @@ export default {
|
|||||||
middleware: ['authenticated', 'termsAndConditions'],
|
middleware: ['authenticated', 'termsAndConditions'],
|
||||||
linkActiveClass: 'router-link-active',
|
linkActiveClass: 'router-link-active',
|
||||||
linkExactActiveClass: 'router-link-exact-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,
|
keys: envWhitelist,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'vue-scrollto/nuxt',
|
||||||
|
{
|
||||||
|
offset: -100, // to compensate fixed navbar height
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
'cookie-universal-nuxt',
|
'cookie-universal-nuxt',
|
||||||
'@nuxtjs/apollo',
|
'@nuxtjs/apollo',
|
||||||
'@nuxtjs/axios',
|
'@nuxtjs/axios',
|
||||||
|
|||||||
@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@human-connection/styleguide": "0.5.21",
|
"@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/axios": "~5.6.0",
|
||||||
"@nuxtjs/dotenv": "~1.4.1",
|
"@nuxtjs/dotenv": "~1.4.1",
|
||||||
"@nuxtjs/pwa": "^3.0.0-beta.19",
|
"@nuxtjs/pwa": "^3.0.0-beta.19",
|
||||||
@ -62,27 +62,28 @@
|
|||||||
"apollo-cache-inmemory": "~1.6.3",
|
"apollo-cache-inmemory": "~1.6.3",
|
||||||
"apollo-client": "~2.6.4",
|
"apollo-client": "~2.6.4",
|
||||||
"cookie-universal-nuxt": "~2.0.18",
|
"cookie-universal-nuxt": "~2.0.18",
|
||||||
"cross-env": "~6.0.0",
|
"cross-env": "~6.0.3",
|
||||||
"date-fns": "2.2.1",
|
"date-fns": "2.4.1",
|
||||||
"express": "~4.17.1",
|
"express": "~4.17.1",
|
||||||
"graphql": "~14.5.6",
|
"graphql": "~14.5.8",
|
||||||
"isemail": "^3.2.0",
|
"isemail": "^3.2.0",
|
||||||
"jsonwebtoken": "~8.5.1",
|
"jsonwebtoken": "~8.5.1",
|
||||||
"linkify-it": "~2.2.0",
|
"linkify-it": "~2.2.0",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"nuxt": "~2.9.2",
|
"nuxt": "~2.10.0",
|
||||||
"nuxt-dropzone": "^1.0.4",
|
"nuxt-dropzone": "^1.0.4",
|
||||||
"nuxt-env": "~0.1.0",
|
"nuxt-env": "~0.1.0",
|
||||||
"stack-utils": "^1.0.2",
|
"stack-utils": "^1.0.2",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"tippy.js": "^4.3.5",
|
"tippy.js": "^4.3.5",
|
||||||
"tiptap": "~1.25.0",
|
"tiptap": "~1.26.0",
|
||||||
"tiptap-extensions": "~1.27.0",
|
"tiptap-extensions": "~1.28.0",
|
||||||
"trunc-html": "^1.1.2",
|
"trunc-html": "^1.1.2",
|
||||||
"v-tooltip": "~2.0.2",
|
"v-tooltip": "~2.0.2",
|
||||||
"vue-count-to": "~1.0.13",
|
"vue-count-to": "~1.0.13",
|
||||||
"vue-infinite-scroll": "^2.0.2",
|
"vue-infinite-scroll": "^2.0.2",
|
||||||
"vue-izitoast": "^1.2.1",
|
"vue-izitoast": "^1.2.1",
|
||||||
|
"vue-scrollto": "^2.17.1",
|
||||||
"vue-sweetalert-icons": "~4.2.0",
|
"vue-sweetalert-icons": "~4.2.0",
|
||||||
"vuex-i18n": "~1.13.1",
|
"vuex-i18n": "~1.13.1",
|
||||||
"xregexp": "^4.2.4",
|
"xregexp": "^4.2.4",
|
||||||
@ -108,9 +109,9 @@
|
|||||||
"core-js": "~2.6.9",
|
"core-js": "~2.6.9",
|
||||||
"css-loader": "~3.2.0",
|
"css-loader": "~3.2.0",
|
||||||
"eslint": "~5.16.0",
|
"eslint": "~5.16.0",
|
||||||
"eslint-config-prettier": "~6.3.0",
|
"eslint-config-prettier": "~6.4.0",
|
||||||
"eslint-config-standard": "~12.0.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-import": "~2.18.2",
|
||||||
"eslint-plugin-jest": "~22.17.0",
|
"eslint-plugin-jest": "~22.17.0",
|
||||||
"eslint-plugin-node": "~10.0.0",
|
"eslint-plugin-node": "~10.0.0",
|
||||||
|
|||||||
@ -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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,11 @@
|
|||||||
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
|
:class="{ 'post-card': true, 'disabled-content': post.disabled }"
|
||||||
>
|
>
|
||||||
<ds-space margin-bottom="small" />
|
<ds-space margin-bottom="small" />
|
||||||
<hc-user :user="post.author" :date-time="post.createdAt" />
|
<hc-user :user="post.author" :date-time="post.createdAt">
|
||||||
<!-- Content Menu (can open Modals) -->
|
<template v-slot:dateTime>
|
||||||
|
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
|
||||||
|
</template>
|
||||||
|
</hc-user>
|
||||||
<client-only>
|
<client-only>
|
||||||
<content-menu
|
<content-menu
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@ -189,11 +192,15 @@ export default {
|
|||||||
margin-top: $space-small;
|
margin-top: $space-small;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
min-height: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ds-card-image {
|
.ds-card-image {
|
||||||
img {
|
img {
|
||||||
max-height: 300px;
|
max-height: 710px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,10 @@ export default {
|
|||||||
name: this.$t('settings.data.name'),
|
name: this.$t('settings.data.name'),
|
||||||
path: `/settings`,
|
path: `/settings`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: this.$t('settings.email.name'),
|
||||||
|
path: `/settings/my-email-address`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: this.$t('settings.security.name'),
|
name: this.$t('settings.security.name'),
|
||||||
path: `/settings/security`,
|
path: `/settings/security`,
|
||||||
|
|||||||
@ -31,14 +31,7 @@
|
|||||||
:placeholder="$t('settings.data.labelBio')"
|
:placeholder="$t('settings.data.labelBio')"
|
||||||
/>
|
/>
|
||||||
<template slot="footer">
|
<template slot="footer">
|
||||||
<ds-button
|
<ds-button icon="check" :disabled="errors" type="submit" :loading="loadingData" primary>
|
||||||
style="float: right;"
|
|
||||||
icon="check"
|
|
||||||
:disabled="errors"
|
|
||||||
type="submit"
|
|
||||||
:loading="loadingData"
|
|
||||||
primary
|
|
||||||
>
|
|
||||||
{{ $t('actions.save') }}
|
{{ $t('actions.save') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
53
webapp/pages/settings/my-email-address/enter-nonce.spec.js
Normal file
53
webapp/pages/settings/my-email-address/enter-nonce.spec.js
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
59
webapp/pages/settings/my-email-address/enter-nonce.vue
Normal file
59
webapp/pages/settings/my-email-address/enter-nonce.vue
Normal 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>
|
||||||
116
webapp/pages/settings/my-email-address/index.spec.js
Normal file
116
webapp/pages/settings/my-email-address/index.spec.js
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
113
webapp/pages/settings/my-email-address/index.vue
Normal file
113
webapp/pages/settings/my-email-address/index.vue
Normal 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>
|
||||||
164
webapp/pages/settings/my-email-address/verify.spec.js
Normal file
164
webapp/pages/settings/my-email-address/verify.spec.js
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
99
webapp/pages/settings/my-email-address/verify.vue
Normal file
99
webapp/pages/settings/my-email-address/verify.vue
Normal 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>
|
||||||
1313
webapp/yarn.lock
1313
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
36
yarn.lock
36
yarn.lock
@ -741,9 +741,9 @@
|
|||||||
regenerator-runtime "^0.12.0"
|
regenerator-runtime "^0.12.0"
|
||||||
|
|
||||||
"@babel/runtime@^7.5.5":
|
"@babel/runtime@^7.5.5":
|
||||||
version "7.5.5"
|
version "7.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.2.tgz#c3d6e41b304ef10dcf13777a33e7694ec4a9a6dd"
|
||||||
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
integrity sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.2"
|
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"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
cross-env@^6.0.0:
|
cross-env@^6.0.3:
|
||||||
version "6.0.0"
|
version "6.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.0.tgz#3c8e71440ea20aa6faaf5aec541235efc565dac6"
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941"
|
||||||
integrity sha512-G/B6gtkjgthT8AP/xN1wdj5Xe18fVyk58JepK8GxpUbqcz3hyWxegocMbvnZK+KoTslwd0ACZ3woi/DVUdVjyQ==
|
integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn "^7.0.0"
|
cross-spawn "^7.0.0"
|
||||||
|
|
||||||
@ -1877,10 +1877,10 @@ cucumber@^4.2.1:
|
|||||||
util-arity "^1.0.2"
|
util-arity "^1.0.2"
|
||||||
verror "^1.9.0"
|
verror "^1.9.0"
|
||||||
|
|
||||||
cypress-cucumber-preprocessor@^1.16.0:
|
cypress-cucumber-preprocessor@^1.16.1:
|
||||||
version "1.16.0"
|
version "1.16.1"
|
||||||
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.16.0.tgz#c73b3b72ea95ba90cd1ed8fb2e586b1380440edd"
|
resolved "https://registry.yarnpkg.com/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-1.16.1.tgz#2ac7e0e53396334c052aeed8b5e61e08616f73a2"
|
||||||
integrity sha512-U3V15iMEKkb7qIePEn22QyDcOjsT+/HRTS6cdKBB2BgtYBCnkWZJ1jfUhf3rFDMMoXFkExSNZG/i00ljF/DUkA==
|
integrity sha512-m8Z5t9hSc10kv47qK0fV/JlCboCwQVxgTa+WhnCjOPB7YBnX/en4f0O8l27yaZbZyHan7JBoJvpfzINlaOKafg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cypress/browserify-preprocessor" "^2.1.1"
|
"@cypress/browserify-preprocessor" "^2.1.1"
|
||||||
chai "^4.1.2"
|
chai "^4.1.2"
|
||||||
@ -1892,12 +1892,13 @@ cypress-cucumber-preprocessor@^1.16.0:
|
|||||||
debug "^3.0.1"
|
debug "^3.0.1"
|
||||||
gherkin "^5.1.0"
|
gherkin "^5.1.0"
|
||||||
glob "^7.1.2"
|
glob "^7.1.2"
|
||||||
|
js-string-escape "^1.0.1"
|
||||||
through "^2.3.8"
|
through "^2.3.8"
|
||||||
|
|
||||||
cypress-file-upload@^3.3.3:
|
cypress-file-upload@^3.3.4:
|
||||||
version "3.3.3"
|
version "3.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.3.3.tgz#119188fa78e9cfc00904c52d76d4ca56d34950df"
|
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.3.4.tgz#cbeb8a7a07150a1c60f2873666979e48b6335070"
|
||||||
integrity sha512-CmXGRMHonoyCa8EcF6jomxqMAe56HvKfnW7S69EmTga8ecYmvQUI6gYttcHO+5UTmFQOFl7xbABV3+AbnI4btA==
|
integrity sha512-kfdrQ6cWBw82G7EbHSqZJiOQWRh9cGz9K1mjePNZax00gBL0qOdRTjfkAnR2vEmmJyCfnN3efryjfhFeLrGWVw==
|
||||||
|
|
||||||
cypress-plugin-retries@^1.3.0:
|
cypress-plugin-retries@^1.3.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
|
||||||
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
|
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:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user