Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into helfa.social-code-of-conduct-etc-tryout

# Conflicts:
#	webapp/constants/links.js
#	webapp/locales/html/de/code-of-conduct.html
#	webapp/locales/html/de/data-privacy.html
#	webapp/locales/html/de/faq.html
#	webapp/locales/html/de/imprint.html
#	webapp/locales/html/de/support.html
#	webapp/locales/html/de/terms-and-conditions.html
#	webapp/locales/html/en/code-of-conduct.html
#	webapp/locales/html/en/data-privacy.html
#	webapp/locales/html/en/donate.html
#	webapp/locales/html/en/faq.html
#	webapp/locales/html/en/imprint.html
#	webapp/locales/html/en/organization.html
#	webapp/locales/html/en/support.html
#	webapp/locales/html/en/terms-and-conditions.html
This commit is contained in:
Wolfgang Huß 2022-06-28 11:48:19 +02:00
commit ec28b20d0d
73 changed files with 1444 additions and 566 deletions

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v17.9.0

View File

@ -4,8 +4,27 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.0.7](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.6...1.0.7)
- Bump rosie from 2.0.1 to 2.1.0 [`#4520`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4520)
- fix: 🍰 Renew JWT In Decode Test [`#4798`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4798)
- docs: 🍰 Refine Main README.md With Test Tech Stack And Video Link [`#4772`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4772)
- docs: 🍰 Change README.md DB Commands For Docker [`#4765`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4765)
- Bump date-fns from 2.23.0 to 2.25.0 [`#4753`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4753)
- Bump neo4j-driver from 4.0.2 to 4.3.4 [`#4729`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4729)
- chore: 🍰 Update Neode To v0.4.7 [`#4751`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4751)
- doc: 🍰 Update README.md Etc. [`#4733`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4733)
- feat: 🍰 New CSS For Internal Pages [`#4741`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4741)
- fix: 🍰 Change Notification E-Mails Settings Page Link [`#4742`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4742)
- Refactor internal pages to new CSS [`acad80c`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/acad80c3c8262934dd2e38961c08c0fde769099a)
- Renew JWT in decode test [`46eb6b8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/46eb6b82ea802d4d6ca7294cd32d1fe16425bfea)
- Revert "Renew JWT in decode test" only for changing the Neode version [`a0d92b4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a0d92b4853d09d725c1fb7886cbfed2a00e1f05c)
#### [1.0.6](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.5...1.0.6)
> 4 October 2021
- chore: 🍰 Set 'sendNotification' Emails Init Admin [`#4690`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4690)
- feat: 🍰 Send Notification E-Mail [`#4623`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4623)
- feat: 🍰 Implement Progress Bar Again [`#4357`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4357)
- Bump nodemailer-html-to-text from 3.1.0 to 3.2.0 in /backend [`#4531`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4531)

View File

@ -1,41 +1,43 @@
# CONTRIBUTING
Thank you so much for thinking of contributing to the Human Connection project! It's awesome you're here, we really appreciate it. :-\)
Thank you so much for thinking of contributing to the [ocelot.social](https://ocelot.social) project! It's awesome you're here, we really appreciate it. :-\)
## Getting Set Up
Instructions for how to install all the necessary software and some code guidelines can be found in our main [Readme](/README.md) or in our [documentation](https://docs.human-connection.org/human-connection/).
Instructions for how to install all the necessary software and some code guidelines can be found in our main [Readme](/README.md) or in our [documentation](/SUMMARY.md).
To get you started we recommend that you join forces with a regular contributor. Please join [our Discord instance](https://discord.gg/AJSX9DCSUA) to chat with developers or just get in touch directly on an issue on either [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues) or [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089):
![](https://dl.dropbox.com/s/vbmcihkduy9dhko/Screenshot%202019-01-03%2015.50.11.png?dl=0)
We also have regular pair programming sessions that you are very welcome to join! We feel this is often the best way to get to know both the project and the team. Most developers are also available for spontaneous sessions if the times listed below don't work for you just ping us on Discord.
We also can have pair programming sessions for you! We feel this is often the best way to get to know both the project and the team. Most developers are also available for spontaneous sessions.
## Development Flow
We operate in two week sprints that are planned, estimated and prioritised on [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089). All issues are also linked to and synced with [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues). Look for the `good first issue` label if you're not sure where to start!
We try to discuss all questions directly related to a feature or bug in the respective issue, in order to preserve it for the future and for other developers. We use Discord for real-time communication.
We try to discuss all questions directly related to a feature or bug in the respective issue, in order to preserve it for the future and for other developers. We use [Discord](https://discord.gg/AJSX9DCSUA) for real-time communication.
This is how we solve bugs and implement features, step by step:
1. We find an issue we want to work on, usually during the sprint planning but as an open source contributor this can happen at any time.
2. We communicate with the team to see if the issue is still available. (When you comment on an issue but don't get an answer there within 1-2 days try to mention @Human-Connection/hc-dev-team to make sure we check in.)
2. We communicate with the team to see if the issue is still available. (When you comment on an issue but don't get an answer there within 1-2 days try to mention @Ocelot-Social-Community/core-team to make sure we check in.)
3. We make sure we understand the issue in detail what problem is it solving and how should it be implemented?
4. We assign ourselves to the issue and move it to `In Progress` on [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089).
5. We start working on it in a `new branch` and open a `pull request` prefixed with `[WIP]` (work in progress) to which we regularly push our changes.
6. When questions come up we clarify them with the team (directly in the issue on Github).
7. When we are happy with our work and our PR is passing all tests we remove the `[WIP]` from the PR description and ask for reviews (if you're not sure who to ask there is @Human-Connection/hc-dev-team which pings all core developers).
7. When we are happy with our work and our PR is passing all tests we remove the `[WIP]` from the PR description and ask for reviews (if you're not sure who to ask there is @Ocelot-Social-Community/core-team which pings all core developers).
8. We then incorporate the suggestions from the reviews into our work and once it has been approved it can be merged into master!
Every pull request needs to:
* fix an issue (if there is something you want to work on but there is no issue for it, create one first and discuss it with the team)
* include tests for the code that is added or changed
* pass all tests (linter, backend, frontend, end-to-end)
* pass all tests (linter, backend, webapp, code coverage, end-to-end)
* be approved by at least 1 developer who is not the owner of the PR (when more than 10 files were changed it needs 2 approvals)
## Contribution Flow For Open Source Contributors
See [contributing in main README.md](/README.md#contributing)
## The Team
There are many volunteers all around the world helping us build this network and without their contributions we wouldn't be where we are today. Big thank you to all of you!
@ -55,7 +57,7 @@ You can talk to our core team on [Discord](https://discord.gg/AJSX9DCSUA). And o
## Meetings and Pair Programming Sessions
Times below refer to **German Time** that's CET (GMT+1) in winter and CEST (GMT+2) in summer because most Human Connection core team members are living in Germany.
Times below refer to **German Time** that's CET (GMT+1) in winter and CEST (GMT+2) in summer because most ocelot.social Community core team members are living in Germany.
Daily standup
@ -109,12 +111,13 @@ We practise [collective code ownership](http://www.extremeprogramming.org/rules/
* we avoid blocking because someone else isn't working, so we sometimes take over PRs from other developers
* everyone should always push their code to branches so others can see it
We believe in open source contributions as a learning experience everyone is welcome to join our team of volunteers and to contribute to the project, no matter their background or level of experience.
We believe in open source contributions as a learning experience everyone is welcome to join our team of volunteers and to contribute to the project, no matter their background or level of experience. To support your learning experience we founded the charity association [busFaktor() e.V.](https://www.busfaktor.org/en).
We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively.
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
<!--
## Open-Source Bounties
There are so many good reasons to contribute to ocelot.social
@ -145,3 +148,4 @@ us your invoice as .pdf file attached to an E-Mail once you are done.
Our Open-Source bounty program is a work-in-progress. Based on our future
experience we will make changes and improvements. So keep an eye on this
contribution guide.
-->

View File

@ -2,7 +2,7 @@
MIT License
Copyright \(c\) 2018 Human-Connection gGmbH
Copyright \(c\) 2018-2021 [Ocelot.Social Community](https://github.com/Ocelot-Social-Community)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files \(the "Software"\), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

145
README.md
View File

@ -1,36 +1,33 @@
# ocelot.social
# Ocelot.Social
[![Build Status](https://travis-ci.com/Human-Connection/Human-Connection.svg?branch=master)](https://travis-ci.com/Human-Connection/Human-Connection)
[![Codecov Coverage](https://img.shields.io/codecov/c/github/Human-Connection/Human-Connection/master.svg?style=flat-square)](https://codecov.io/gh/Human-Connection/Human-Connection/)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Human-Connection/Nitro-Backend/blob/backend/LICENSE.md)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discordapp.com/invite/DFSjPaX)
[![Open Source Helpers](https://www.codetriage.com/human-connection/human-connection/badges/users.svg)](https://www.codetriage.com/human-connection/human-connection)
[![Build Status Test](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions/workflows/test.yml/badge.svg)](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions)
[![Build Status Publish](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions/workflows/publish.yml/badge.svg)](https://github.com/Ocelot-Social-Community/Ocelot-Social/actions)
[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/LICENSE.md)
[![Discord Channel](https://img.shields.io/discord/489522408076738561.svg)](https://discord.gg/AJSX9DCSUA)
[![Open Source Helpers](https://www.codetriage.com/ocelot-social-community/ocelot-social/badges/users.svg)](https://www.codetriage.com/ocelot-social-community/ocelot-social)
ocelot.social is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
* **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk**
* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
[ocelot.social](https://ocelot.social) is free and open source software program code to run social networks. Its development is supported by a community of programmers and interested network operators.
<p align="center">
<img src="webapp/static/img/custom/logo-squared.svg" alt="ocelot.social" width="40%" height="40%">
<a href="https://ocelot.social" target="_blank"><img src="webapp/static/img/custom/logo-squared.svg" alt="ocelot.social" width="40%" height="40%"></a>
</p>
## Live demo
Our goal is to enable people to participate fairly and equally in online social networks. The equality of opportunity applies both to the fundamental equality of all people and to the possibility of letting their diverse voices be heard.
__Try out our deployed [development environment](https://develop.human-connection.org/).__
We therefore consider it desirable that operators offer such networks so that people can choose where they want to be on the move.
Logins:
At the same time, it should be possible in the future to link these networks with each other (ActivityPub, Fediverse), so that users can also connect with people from other networks - for example by making friends or following posts or other contributions.
| email | password | role |
| :--- | :--- | :--- |
| `user@example.org` | 1234 | user |
| `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin |
In other words, we are interested in a network of networks and in keeping the data as close as possible to the user and the operator they trusts.
## Introduction
Have a look into our short video:
[ocelot.social - GitHub - Developer Welcome - Tutorial (english)](https://www.youtube.com/watch?v=gZSL6KvBIiY&list=PLFMD5liPP01kbuReHxYXxv_1fI5rIgS1f&index=1)
## Directory Layout
There are four important directories:
There are three important directories:
* [Backend](./backend) runs on the server and is a middleware between database and frontend
* [Frontend](./webapp) is a server-side-rendered and client-side-rendered web frontend
@ -41,8 +38,8 @@ setup **frontend** and **backend**.
There are two approaches:
1. Local installation, which means you have to take care of dependencies yourself
2. **Or** Install everything through Docker which takes care of dependencies for you
1. [Local](#local-installation) installation, which means you have to take care of dependencies yourself.
2. **Or** Install everything through [Docker](#docker-installation) which takes care of dependencies for you.
## Installation
@ -68,6 +65,18 @@ Change into the new folder.
$ cd Ocelot-Social
```
### Login
<!-- Try out our deployed [development environment](https://develop.human-connection.org/). -->
Logins in the browser after the following installations:
| email | password | role |
| :--- | :--- | :--- |
| `user@example.org` | 1234 | user |
| `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin |
### Docker Installation
Docker is a software development container tool that combines software and its dependencies into one standardized unit that contains everything needed to run it. This helps us to avoid problems with dependencies and makes installation easier.
@ -91,34 +100,94 @@ docker-compose version 1.23.2
#### Start Ocelot-Social via Docker-Compose
Prepare ENVs once beforehand:
```bash
# in folder webapp/
$ cp .env.template .env
# in folder backend/
$ cp .env.template .env
```
For Development:
```bash
# in main folder
$ docker-compose up
```
For Production:
```bash
# in main folder
$ docker-compose -f docker-compose.yml up
```
This will start all required Docker containers
This will start all required Docker containers.
Make sure your database is running on `http://localhost:7474/browser/`.
Prepare database once before you start by running the following command in a second terminal:
```bash
# in main folder while docker-compose is up
$ docker-compose exec backend yarn run db:migrate init
```
Then clear and seed database by running the following command as well in the second terminal:
```bash
# in main folder while docker-compose is up
$ docker-compose exec backend yarn run db:reset
$ docker-compose exec backend yarn run db:seed
```
For a closer description see [backend README.md](./backend/README.md).
For a full documentation see [SUMMARY](./SUMMARY.md).
### Local Installation
For a full documentation see [SUMMARY](./SUMMARY.md).
## Contributing
Choose an issue (consider our label [good-first-issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)) and leave a comment there. We will then invite you to join our volunteers team.
To have the necessary permission to push directly to this repository, please accept our invitation to join our volunteers team, you will receive via the email, Github will send you, once invited. If we did not invite you yet, please request an invitation via Discord.
We are happy if you fork our repository, but we don't recommend it for development. You do not need a fork.
Clone this repository locally as [described above](#clone-the-repository), create your branch named `<issue-number>-<description>`, add your code and push your branch to this repository. Then create a PR by comparing it to our `master`.
Please run the following commands before you push:
```bash
# in folder backend/
$ yarn lint --fix
$ yarn test
```
```bash
# in folder webapp/
$ yarn lint --fix
$ yarn locales --fix
$ yarn test
```
Check out our [contribution guideline](./CONTRIBUTING.md), too!
### Developer Chat
Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat:
Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Mentors` to get you onboard :neckbeard:
We give write permissions to every developer who asks for it. Just text us on
[Discord](https://discord.gg/AJSX9DCSUA).
## Deployment
Deployment methods can be found in the [Ocelot-Social-Deploy-Rebranding](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding) repository.
The only deployment method in this repository is `docker-compose` for development purposes as described above.
## Developer Chat
Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat:
Just introduce yourself at `#introduce-yourself` and mention `@@Mentor` to get you onboard :neckbeard:
Check out the [contribution guideline](./CONTRIBUTING.md), too!
We give write permissions to every developer who asks for it. Just text us on
[Discord](https://discord.gg/AJSX9DCSUA).
The only deployment method in this repository for development purposes as described above is `docker-compose`.
## Technology Stack
@ -128,6 +197,14 @@ We give write permissions to every developer who asks for it. Just text us on
* [NodeJS](https://nodejs.org/en/)
* [Neo4J](https://neo4j.com/)
### For Testing
* [Cypress](https://docs.cypress.io/)
* [Storybook](https://storybook.js.org/)
* [Jest](https://jestjs.io/)
* [Vue Test Utils](https://vue-test-utils.vuejs.org/)
* [ESLint](https://eslint.org/)
## Attributions
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/).

1
backend/.nvmrc Normal file
View File

@ -0,0 +1 @@
v12.19.0

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "1.0.6",
"version": "1.0.7",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -66,7 +66,6 @@
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
"faker": "Marak/faker.js#master",
"graphql": "^14.6.0",
"graphql-custom-directives": "~0.2.14",
"graphql-iso-date": "~3.6.1",
@ -104,7 +103,7 @@
"mustache": "^4.2.0",
"neo4j-driver": "^4.0.2",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7",
"neode": "^0.4.7",
"node-fetch": "~2.6.1",
"nodemailer": "^6.4.4",
"nodemailer-html-to-text": "^3.2.0",
@ -120,6 +119,7 @@
"xregexp": "^4.3.0"
},
"devDependencies": {
"@faker-js/faker": "5.1.0",
"apollo-server-testing": "~2.11.0",
"chai": "~4.2.0",
"cucumber": "~6.0.5",

View File

@ -0,0 +1,5 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
export default {
NONCE_LENGTH: 5,
INVITE_CODE_LENGTH: 6,
}

View File

@ -1,8 +1,8 @@
import { v4 as uuid } from 'uuid'
import faker from 'faker'
import slugify from 'slug'
import { hashSync } from 'bcryptjs'
import { Factory } from 'rosie'
import faker from '@faker-js/faker'
import { getDriver, getNeode } from './neo4j'
import CONFIG from '../config/index.js'
import generateInviteCode from '../schema/resolvers/helpers/generateInviteCode.js'

View File

@ -19,8 +19,8 @@ export async function up(next) {
SET donationInfo.createdAt = toString(datetime())
SET donationInfo.updatedAt = donationInfo.createdAt
SET donationInfo.showDonations = false
SET donationInfo.goal = 15000
SET donationInfo.progress = 1200
SET donationInfo.goal = 15000.0
SET donationInfo.progress = 1200.0
RETURN donationInfo {.*}
`,
{ donationId },

View File

@ -1,7 +1,7 @@
import faker from 'faker'
import sample from 'lodash/sample'
import { createTestClient } from 'apollo-server-testing'
import createServer from '../server'
import faker from '@faker-js/faker'
import Factory from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import { gql } from '../helpers/jest'
@ -537,7 +537,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
await Factory.build(
'inviteCode',
{
code: 'AAAAAA',
code: 'ABCDEF',
},
{
generatedBy: jennyRostock,

View File

@ -21,8 +21,14 @@ const neode = getNeode()
// iss: 'http://localhost:4000',
// sub: 'u3'
// }
// !!! if the token expires go into the GraphQL Playground in the browser at 'http://localhost:4000' with a running backend and a seeded Neo4j database
// now do the login mutation:
// mutation {
// login(email:"user@example.org", password:"1234")
// }
// replace this token here with the one you received as the result
export const validAuthorizationHeader =
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc'
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTYzNzY0NDMwMCwiZXhwIjoxNzAwNzU5NTAwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.ispIfRfgkXuYoIhKx7x2jPxgvHDJVv1ogMycLmfUnsk'
beforeAll(async () => {
await cleanDatabase()
@ -47,6 +53,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = null
})
it('returns null', returnsNull)
})
@ -54,6 +61,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = undefined
})
it('returns null', returnsNull)
})
@ -61,6 +69,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = 'blah'
})
it('returns null', returnsNull)
})
@ -68,6 +77,7 @@ describe('decode', () => {
beforeEach(() => {
authorizationHeader = validAuthorizationHeader
})
it('returns null', returnsNull)
describe('and corresponding user in the database', () => {

View File

@ -0,0 +1,86 @@
import sanitizeHtml from 'sanitize-html'
import linkifyHtml from 'linkifyjs/html'
const standardSanitizeHtmlOptions = {
allowedTags: [
'img',
'p',
'h3',
'h4',
'br',
'hr',
'b',
'i',
'u',
'em',
'strong',
'a',
'pre',
'ul',
'li',
'ol',
's',
'strike',
'span',
'blockquote',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
span: ['contenteditable', 'class', 'data-*'],
img: ['src'],
},
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
parser: {
lowerCaseTags: true,
},
transformTags: {
h1: 'h3',
h2: 'h3',
h3: 'h3',
h4: 'h4',
h5: 'strong',
i: 'em',
a: (tagName, attribs) => {
return {
tagName: 'a',
attribs: {
...attribs,
href: attribs.href || '',
target: '_blank',
rel: 'noopener noreferrer nofollow',
},
}
},
b: 'strong',
s: 'strike',
},
}
export function cleanHtml(dirty, _key, sanitizeHtmlOptions = standardSanitizeHtmlOptions) {
if (!dirty) {
return dirty
}
dirty = linkifyHtml(dirty)
dirty = sanitizeHtml(dirty, sanitizeHtmlOptions)
// remove empty html tags and duplicated linebreaks and returns
dirty = dirty
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
// replace all p tags with line breaks (and spaces) only by single linebreaks
// limit linebreaks to max 2 (equivalent to html "br" linebreak)
.replace(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim, '</p><p>')
// remove additional linebreaks inside p tags
.replace(/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '')
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
.replace(/(\s*<br ?\/?>\s*)+<\/p+>/gim, '</p>')
return dirty
}

View File

@ -1,4 +1,5 @@
import CONFIG from '../../../config'
import { cleanHtml } from '../../../middleware/helpers/cleanHtml.js'
import nodemailer from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'
@ -10,6 +11,27 @@ if (!hasEmailConfig) {
if (!CONFIG.TEST) {
// eslint-disable-next-line no-console
console.log('Warning: Middlewares will not try to send mails.')
// TODO: disable e-mail logging on database seeding?
// TODO: implement general logging like 'log4js', see Gradido project: https://github.com/gradido/gradido/blob/master/backend/log4js-config.json
sendMailCallback = async (templateArgs) => {
// eslint-disable-next-line no-console
console.log('--- Log Unsend E-Mail ---')
// eslint-disable-next-line no-console
console.log('To: ' + templateArgs.to)
// eslint-disable-next-line no-console
console.log('From: ' + templateArgs.from)
// eslint-disable-next-line no-console
console.log('Subject: ' + templateArgs.subject)
// eslint-disable-next-line no-console
console.log('Content:')
// eslint-disable-next-line no-console
console.log(
cleanHtml(templateArgs.html, 'dummyKey', {
allowedTags: ['a'],
allowedAttributes: { a: ['href'] },
}).replace(/&amp;/g, '&'),
)
}
}
} else {
sendMailCallback = async (templateArgs) => {

View File

@ -19,9 +19,9 @@ const defaultParams = {
}
const englishHint = 'English version below!'
export const signupTemplate = ({ email, nonce, inviteCode = null }) => {
export const signupTemplate = ({ email, variables: { nonce, inviteCode = null } }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
// dev format example: http://localhost:3000/registration?method=invite-mail&email=wolle.huss%40pjannto.com&nonce=64853
// dev format example: http://localhost:3000/registration?method=invite-mail&email=huss%40pjannto.com&nonce=64853
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', email)
actionUrl.searchParams.set('nonce', nonce)
@ -31,74 +31,63 @@ export const signupTemplate = ({ email, nonce, inviteCode = null }) => {
} else {
actionUrl.searchParams.set('method', 'invite-mail')
}
const renderParams = { ...defaultParams, englishHint, actionUrl, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ ...defaultParams, englishHint, actionUrl, nonce, subject },
{ content: templates.signup },
),
html: mustache.render(templates.layout, renderParams, { content: templates.signup }),
}
}
export const emailVerificationTemplate = ({ email, nonce, name }) => {
export const emailVerificationTemplate = ({ email, variables: { 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('email', email)
actionUrl.searchParams.set('nonce', nonce)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ ...defaultParams, englishHint, actionUrl, name, nonce, subject },
{ content: templates.emailVerification },
),
html: mustache.render(templates.layout, renderParams, { content: templates.emailVerification }),
}
}
export const resetPasswordTemplate = ({ email, nonce, name }) => {
export const resetPasswordTemplate = ({ email, variables: { 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)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ ...defaultParams, englishHint, actionUrl, name, nonce, subject },
{ content: templates.passwordReset },
),
html: mustache.render(templates.layout, renderParams, { content: templates.passwordReset }),
}
}
export const wrongAccountTemplate = ({ email }) => {
export const wrongAccountTemplate = ({ email, _variables = {} }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, englishHint, actionUrl }
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ ...defaultParams, englishHint, actionUrl },
{ content: templates.wrongAccount },
),
html: mustache.render(templates.layout, renderParams, { content: templates.wrongAccount }),
}
}
export const notificationTemplate = ({ email, notification }) => {
export const notificationTemplate = ({ email, variables: { notification } }) => {
const actionUrl = new URL('/notifications', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, name: notification.to.name, actionUrl }
const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, name: notification.to.name, settingsUrl, actionUrl }
let content
switch (notification.to.locale) {
case 'de':

View File

@ -0,0 +1,246 @@
import CONFIG from '../../../config'
import logosWebapp from '../../../config/logos.js'
import {
signupTemplate,
emailVerificationTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
notificationTemplate,
} from './templateBuilder'
const englishHint = 'English version below!'
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const supportUrl = CONFIG.SUPPORT_URL.toString()
let actionUrl, name, settingsUrl
const signupTemplateData = () => ({
email: 'test@example.org',
variables: {
nonce: '12345',
inviteCode: 'AAAAAA',
},
})
const emailVerificationTemplateData = () => ({
email: 'test@example.org',
variables: {
nonce: '12345',
name: 'Mr Example',
},
})
const resetPasswordTemplateData = () => ({
email: 'test@example.org',
variables: {
nonce: '12345',
name: 'Mr Example',
},
})
const wrongAccountTemplateData = () => ({
email: 'test@example.org',
variables: {},
})
const notificationTemplateData = (locale) => ({
email: 'test@example.org',
variables: {
notification: {
to: { name: 'Mr Example', locale },
},
},
})
const textsStandard = [
{
templPropName: 'from',
isContaining: false,
text: CONFIG.EMAIL_DEFAULT_SENDER,
},
{
templPropName: 'to',
isContaining: false,
text: 'test@example.org',
},
// is contained in html
welcomeImageUrl.toString(),
CONFIG.ORGANIZATION_URL,
CONFIG.APPLICATION_NAME,
]
const testEmailData = (emailTemplate, templateBuilder, templateData, texts) => {
if (!emailTemplate) {
emailTemplate = templateBuilder(templateData)
}
texts.forEach((element) => {
if (typeof element === 'object') {
if (element.isContaining) {
expect(emailTemplate[element.templPropName]).toEqual(expect.stringContaining(element.text))
} else {
expect(emailTemplate[element.templPropName]).toEqual(element.text)
}
} else {
expect(emailTemplate.html).toEqual(expect.stringContaining(element))
}
})
return emailTemplate
}
// beforeAll(async () => {
// await cleanDatabase()
// })
// afterAll(async () => {
// await cleanDatabase()
// })
describe('templateBuilder', () => {
describe('signupTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI).toString()
const theSignupTemplateData = signupTemplateData()
const enContent = "Thank you for joining our cause it's awesome to have you on board."
const deContent =
'Danke, dass Du dich angemeldet hast wir freuen uns, Dich dabei zu haben.'
testEmailData(null, signupTemplate, theSignupTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
theSignupTemplateData.variables.nonce,
theSignupTemplateData.variables.inviteCode,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('emailVerificationTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI).toString()
const theEmailVerificationTemplateData = emailVerificationTemplateData()
const enContent = 'So, you want to change your e-mail? No problem!'
const deContent = 'Du möchtest also deine E-Mail ändern? Kein Problem!'
testEmailData(null, emailVerificationTemplate, theEmailVerificationTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
theEmailVerificationTemplateData.variables.nonce,
theEmailVerificationTemplateData.variables.name,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('resetPasswordTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI).toString()
const theResetPasswordTemplateData = resetPasswordTemplateData()
const enContent = 'So, you forgot your password? No problem!'
const deContent = 'Du hast also dein Passwort vergessen? Kein Problem!'
testEmailData(null, resetPasswordTemplate, theResetPasswordTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
theResetPasswordTemplateData.variables.nonce,
theResetPasswordTemplateData.variables.name,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('wrongAccountTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI).toString()
const theWrongAccountTemplateData = wrongAccountTemplateData()
const enContent =
"You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address."
const deContent =
'Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen Account mit Deiner E-Mailadresse gefunden.'
testEmailData(null, wrongAccountTemplate, theWrongAccountTemplateData, [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
englishHint,
actionUrl,
enContent,
deContent,
supportUrl,
])
})
})
})
describe('notificationTemplate', () => {
beforeEach(() => {
actionUrl = new URL('/notifications', CONFIG.CLIENT_URI).toString()
name = notificationTemplateData('en').variables.notification.to.name
settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
})
describe('en', () => {
it('e-mail is build with all data', () => {
const subject = `${CONFIG.APPLICATION_NAME} Notification`
const content = 'You received at least one notification. Click on this button to view them:'
testEmailData(null, notificationTemplate, notificationTemplateData('en'), [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
actionUrl,
name,
content,
settingsUrl,
])
})
})
describe('de', () => {
it('e-mail is build with all data', async () => {
const subject = `${CONFIG.APPLICATION_NAME} Benachrichtigung`
const content = `Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button, um sie anzusehen:`
testEmailData(null, notificationTemplate, notificationTemplateData('de'), [
...textsStandard,
{
templPropName: 'subject',
isContaining: false,
text: subject,
},
actionUrl,
name,
content,
settingsUrl,
])
})
})
})
})

View File

@ -25,8 +25,7 @@
<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 mindestens eine Benachrichtigung erhalten. Klick auf diesen Button,
um sie anzusehen:</p>
<p style="margin: 0;">Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button, um sie anzusehen:</p>
</td>
</tr>
<tr>
@ -71,7 +70,7 @@
<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; margin-top: 10px;">PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a href="{{{ ORGANIZATION_URL }}}/settings/notifications"
<p style="margin: 0; margin-top: 10px;">PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a href="{{{ settingsUrl }}}"
style="color: #17b53e;">Benachrichtigungseinstellungen</a>.</p>
</td>
</tr>

View File

@ -70,7 +70,7 @@
<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; margin-top: 10px;">PS: If you don't want to receive e-mails anymore, change your <a href="{{{ ORGANIZATION_URL }}}/settings/notifications"
<p style="margin: 0; margin-top: 10px;">PS: If you don't want to receive e-mails anymore, change your <a href="{{{ settingsUrl }}}"
style="color: #17b53e;">notification settings</a>.</p>
</td>
</tr>

View File

@ -24,8 +24,8 @@
<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 Passwort angefordert leider haben wir aber keinen
Account mit Deiner E-Mailadresse gefunden. Kann es sein, dass Du mit einer anderen Adresse bei uns
<p style="margin: 0;">Du hast bei uns ein neues Passwort 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>
@ -122,8 +122,8 @@
<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>
<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>

View File

@ -11,9 +11,9 @@ const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce } = response
if (inviteCode) {
await sendMail(signupTemplate({ email, nonce, inviteCode }))
await sendMail(signupTemplate({ email, variables: { nonce, inviteCode } }))
} else {
await sendMail(signupTemplate({ email, nonce }))
await sendMail(signupTemplate({ email, variables: { nonce } }))
}
delete response.nonce
return response
@ -23,14 +23,14 @@ const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo)
const { email } = args
const { email: userFound, nonce, name } = await resolve(root, args, context, resolveInfo)
const template = userFound ? resetPasswordTemplate : wrongAccountTemplate
await sendMail(template({ email, nonce, name }))
await sendMail(template({ email, variables: { nonce, name } }))
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 }))
await sendMail(emailVerificationTemplate({ email, variables: { nonce, name } }))
delete response.nonce
return response
}

View File

@ -41,11 +41,10 @@ const publishNotifications = async (context, promises) => {
notifications.forEach((notificationAdded, index) => {
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if (notificationAdded.to.sendNotificationEmails) {
// Wolle await
sendMail(
notificationTemplate({
email: notificationsEmailAddresses[index].email,
notification: notificationAdded,
variables: { notification: notificationAdded },
}),
)
}

View File

@ -182,12 +182,12 @@ describe('authorization', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
inviteCode: 'ABCDEF',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'AAAAAA',
code: 'ABCDEF',
})
})
@ -224,12 +224,12 @@ describe('authorization', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
inviteCode: 'ABCDEF',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = true
await Factory.build('inviteCode', {
code: 'AAAAAA',
code: 'ABCDEF',
})
})
@ -254,7 +254,7 @@ describe('authorization', () => {
CONFIG.INVITE_REGISTRATION = true
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'AAAAAA',
code: 'ABCDEF',
})
})
@ -262,7 +262,7 @@ describe('authorization', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
inviteCode: 'ABCDEF',
}
authenticatedUser = null
})

View File

@ -195,7 +195,7 @@ describe('slugifyMiddleware', () => {
variables = {
...variables,
name: 'I am a user',
nonce: '123456',
nonce: '12345',
password: 'yo',
email: '123@example.org',
termsAndConditionsAgreedVersion: '0.0.1',
@ -206,7 +206,7 @@ describe('slugifyMiddleware', () => {
beforeEach(async () => {
await Factory.build('emailAddress', {
email: '123@example.org',
nonce: '123456',
nonce: '12345',
verifiedAt: null,
})
})

View File

@ -1,100 +1,15 @@
import walkRecursive from '../helpers/walkRecursive'
// import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common'
import sanitizeHtml from 'sanitize-html'
// import { isEmpty, intersection } from 'lodash'
import linkifyHtml from 'linkifyjs/html'
function clean(dirty) {
if (!dirty) {
return dirty
}
dirty = linkifyHtml(dirty)
dirty = sanitizeHtml(dirty, {
allowedTags: [
'img',
'p',
'h3',
'h4',
'br',
'hr',
'b',
'i',
'u',
'em',
'strong',
'a',
'pre',
'ul',
'li',
'ol',
's',
'strike',
'span',
'blockquote',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
span: ['contenteditable', 'class', 'data-*'],
img: ['src'],
},
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
parser: {
lowerCaseTags: true,
},
transformTags: {
h1: 'h3',
h2: 'h3',
h3: 'h3',
h4: 'h4',
h5: 'strong',
i: 'em',
a: (tagName, attribs) => {
return {
tagName: 'a',
attribs: {
...attribs,
href: attribs.href || '',
target: '_blank',
rel: 'noopener noreferrer nofollow',
},
}
},
b: 'strong',
s: 'strike',
},
})
// remove empty html tags and duplicated linebreaks and returns
dirty = dirty
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$1')
// replace all p tags with line breaks (and spaces) only by single linebreaks
// limit linebreaks to max 2 (equivalent to html "br" linebreak)
.replace(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim, '</p><p>')
// remove additional linebreaks inside p tags
.replace(/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '')
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
.replace(/(\s*<br ?\/?>\s*)+<\/p+>/gim, '</p>')
return dirty
}
import { cleanHtml } from '../middleware/helpers/cleanHtml.js'
const fields = ['content', 'contentExcerpt', 'reasonDescription']
export default {
Mutation: async (resolve, root, args, context, info) => {
args = walkRecursive(args, fields, clean)
args = walkRecursive(args, fields, cleanHtml)
return resolve(root, args, context, info)
},
Query: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
return walkRecursive(result, fields, clean)
return walkRecursive(result, fields, cleanHtml)
},
}

View File

@ -158,7 +158,7 @@ describe('VerifyEmailAddress', () => {
`
beforeEach(() => {
variables = { ...variables, email: 'to-be-verified@example.org', nonce: '123456' }
variables = { ...variables, email: 'to-be-verified@example.org', nonce: '12345' }
})
describe('unauthenticated', () => {

View File

@ -1,8 +1,13 @@
import CONSTANTS_REGISTRATION from './../../../constants/registration'
export default function generateInviteCode() {
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
return Array.from({ length: 6 }, (n = Math.floor(Math.random() * 36)) => {
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
}).join('')
return Array.from(
{ length: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH },
(n = Math.floor(Math.random() * 36)) => {
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
},
).join('')
}

View File

@ -1,5 +1,11 @@
import CONSTANTS_REGISTRATION from './../../../constants/registration'
// TODO: why this is not used in resolver 'requestPasswordReset'?
export default function generateNonce() {
return Array.from({ length: 5 }, (n = Math.floor(Math.random() * 10)) => {
return String.fromCharCode(n + 48)
}).join('')
return Array.from(
{ length: CONSTANTS_REGISTRATION.NONCE_LENGTH },
(n = Math.floor(Math.random() * 10)) => {
return String.fromCharCode(n + 48)
},
).join('')
}

View File

@ -3,6 +3,7 @@ import { getDriver } from '../../db/neo4j'
import { gql } from '../../helpers/jest'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
import CONSTANTS_REGISTRATION from './../../constants/registration'
let user
let query
@ -107,7 +108,11 @@ describe('inviteCodes', () => {
errors: undefined,
data: {
GenerateInviteCode: {
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/),
code: expect.stringMatching(
new RegExp(
`^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`,
),
),
expiresAt: null,
createdAt: expect.any(String),
},
@ -129,7 +134,11 @@ describe('inviteCodes', () => {
errors: undefined,
data: {
GenerateInviteCode: {
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/),
code: expect.stringMatching(
new RegExp(
`^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`,
),
),
expiresAt: nextWeek.toISOString(),
createdAt: expect.any(String),
},

View File

@ -1,11 +1,13 @@
import { v4 as uuid } from 'uuid'
import bcrypt from 'bcryptjs'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset'
export default {
Mutation: {
requestPasswordReset: async (_parent, { email }, { driver }) => {
const nonce = uuid().substring(0, 6)
// TODO: why this is generated differntly from 'backend/src/schema/resolvers/helpers/generateNonce.js'?
const nonce = uuid().substring(0, CONSTANTS_REGISTRATION.NONCE_LENGTH)
return createPasswordReset({ driver, nonce, email })
},
resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => {

View File

@ -1,6 +1,7 @@
import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
@ -109,7 +110,7 @@ describe('passwordReset', () => {
const resets = await getAllPasswordResets()
const [reset] = resets
const { nonce } = reset.properties
expect(nonce).toHaveLength(6)
expect(nonce).toHaveLength(CONSTANTS_REGISTRATION.NONCE_LENGTH)
})
})
})
@ -118,7 +119,7 @@ describe('passwordReset', () => {
describe('resetPassword', () => {
const setup = async (options = {}) => {
const { email = 'user@example.org', issuedAt = new Date(), nonce = 'abcdef' } = options
const { email = 'user@example.org', issuedAt = new Date(), nonce = '12345' } = options
await createPasswordReset({ driver, email, issuedAt, nonce })
}
@ -148,7 +149,7 @@ describe('resetPassword', () => {
describe('invalid email', () => {
it('resolves to false', async () => {
await setup()
variables = { ...variables, email: 'non-existent@example.org', nonce: 'abcdef' }
variables = { ...variables, email: 'non-existent@example.org', nonce: '12345' }
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { resetPassword: false },
})
@ -162,7 +163,7 @@ describe('resetPassword', () => {
describe('but invalid nonce', () => {
beforeEach(() => {
variables = { ...variables, nonce: 'slkdjf' }
variables = { ...variables, nonce: 'slkdj' }
})
it('resolves to false', async () => {
@ -177,7 +178,7 @@ describe('resetPassword', () => {
beforeEach(() => {
variables = {
...variables,
nonce: 'abcdef',
nonce: '12345',
}
})

View File

@ -179,7 +179,7 @@ describe('SignupVerification', () => {
beforeEach(async () => {
variables = {
...variables,
nonce: '123456',
nonce: '12345',
name: 'John Doe',
password: '123',
email: 'john@example.org',
@ -207,7 +207,7 @@ describe('SignupVerification', () => {
describe('sending a valid nonce', () => {
beforeEach(() => {
variables = { ...variables, nonce: '123456' }
variables = { ...variables, nonce: '12345' }
})
it('rejects', async () => {
@ -222,7 +222,7 @@ describe('SignupVerification', () => {
beforeEach(async () => {
const args = {
email: 'john@example.org',
nonce: '123456',
nonce: '12345',
}
await neode.model('EmailAddress').create(args)
})

View File

@ -38,8 +38,8 @@ const newlyCreatedNodesWithLocales = [
nameRU: 'Вельцхайм',
nameNL: 'Welzheim',
namePL: 'Welzheim',
lng: 9.63444,
lat: 48.87472,
lng: 9.634741,
lat: 48.874924,
},
state: {
id: expect.stringContaining('region'),
@ -119,25 +119,27 @@ describe('Location Service', () => {
lang: 'en',
}
const result = await query({ query: queryLocations, variables })
expect(result.data.queryLocations).toEqual([
{ id: 'place.14094307404564380', place_name: 'Berlin, Germany' },
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Maryland, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Connecticut, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, New Jersey, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin Township, New Jersey, United States',
},
])
expect(result.data.queryLocations).toEqual(
expect.arrayContaining([
{ id: 'place.14094307404564380', place_name: 'Berlin, Germany' },
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Maryland, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, Connecticut, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin, New Jersey, United States',
},
{
id: expect.stringMatching(/^place\.[0-9]+$/),
place_name: 'Berlin Heights, Ohio, United States',
},
]),
)
})
it('query Location existing in different language', async () => {

View File

@ -963,6 +963,11 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@faker-js/faker@5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.1.0.tgz#cee1d77ada0d0dbbe77201d18b1ebabf432d9c0f"
integrity sha512-0VonSKh7fBCqvY+V2FLN2ZW4pR4ZtWJalWmwSaiaB7yK7y4qp8vDfuaq9QdLjf/cdZGx3M7Wc4Q+x4fZHxI21Q==
"@graphql-toolkit/common@0.10.4":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@graphql-toolkit/common/-/common-0.10.4.tgz#7785f2a3f14559d0778859c49f4442078c196695"
@ -1023,7 +1028,7 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.0.0.tgz#ba83436edfac1d1ffd0e94797d43419c20ad49b8"
integrity sha512-XxD4A5YMIH70ddjG7BJBUz7RWVQAwIP/36Eoyh0DsaWp92OAeXkrbtSEaYkynBPTsN9Uv2mZq9QWZYILl2Svrw==
"@hapi/joi@^15.1.0":
"@hapi/joi@^15.1.1":
version "15.1.1"
resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7"
integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==
@ -4587,10 +4592,6 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
faker@Marak/faker.js#master:
version "4.1.0"
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/3b2fa4aebccee52ae1bafc15d575061fb30c3cf1"
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@ -7527,14 +7528,18 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo4j-driver@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-1.7.6.tgz#eccb135a71eba9048c68717444593a6424cffc49"
integrity sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==
neo4j-driver-bolt-connection@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.3.4.tgz#de642bb6a62ffc6ae2e280dccf21395b4d1705a2"
integrity sha512-yxbvwGav+N7EYjcEAINqL6D3CZV+ee2qLInpAhx+iNurwbl3zqtBGiVP79SZ+7tU++y3Q1fW5ofikH06yc+LqQ==
dependencies:
"@babel/runtime" "^7.5.5"
neo4j-driver-core "^4.3.4"
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neo4j-driver-core@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.3.4.tgz#b445a4fbf94dce8441075099bd6ac3133c1cf5ee"
integrity sha512-3tn3j6IRUNlpXeehZ9Xv7dLTZPB4a7APaoJ+xhQyMmYQO3ujDM4RFHc0pZcG+GokmaltT5pUCIPTDYx6ODdhcA==
neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
version "4.0.2"
@ -7546,6 +7551,16 @@ neo4j-driver@^4.0.1, neo4j-driver@^4.0.2:
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neo4j-driver@^4.2.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.3.4.tgz#a54f0562f868ee94dff7509df74e3eb2c1f95a85"
integrity sha512-AGrsFFqnoZv4KhJdmKt4mOBV5mnxmV3+/t8KJTOM68jQuEWoy+RlmAaRRaCSU4eY586OFN/R8lg9MrJpZdSFjw==
dependencies:
"@babel/runtime" "^7.5.5"
neo4j-driver-bolt-connection "^4.3.4"
neo4j-driver-core "^4.3.4"
rxjs "^6.6.3"
neo4j-graphql-js@^2.11.5:
version "2.11.5"
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.11.5.tgz#4e887d727ec05b2c57ab81fad373fa3fcb734e39"
@ -7559,15 +7574,15 @@ neo4j-graphql-js@^2.11.5:
lodash "^4.17.15"
neo4j-driver "^4.0.1"
neode@^0.3.7:
version "0.3.7"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.3.7.tgz#766105307e138b1212957aceba538e89e3d784cb"
integrity sha512-XnRJyD6bZx4HyHBmnLHuVUKtSD3FhBPXYdh7/rqiFAwBDMOSySjMFjFCYmop+sF8IBZmliowDs8zkSHt27U1kw==
neode@^0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.7.tgz#033007b57a2ee167e9ee5537493086db08d005eb"
integrity sha512-YXlc187JRpeKCBcUIkY6nimXXG+Tvlopfe71/FPno2THrwmYt5mm0RPHZ+mXF2O1Xg6zvjKvOpCpDz2vHBfroQ==
dependencies:
"@hapi/joi" "^15.1.0"
"@hapi/joi" "^15.1.1"
dotenv "^4.0.0"
neo4j-driver "^1.7.6"
uuid "^3.3.2"
neo4j-driver "^4.2.2"
uuid "^3.4.0"
next-tick@^1.0.0:
version "1.0.0"
@ -8982,6 +8997,13 @@ rxjs@^6.4.0, rxjs@^6.5.2, rxjs@^6.5.4:
dependencies:
tslib "^1.9.0"
rxjs@^6.6.3:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -10347,7 +10369,7 @@ uuid@3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.1.0, uuid@^3.3.2:
uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==

View File

@ -5,9 +5,9 @@ Feature: Notification for a mention
Background:
Given the following "users" are in the database:
| slug | email | password | id | name | termsAndConditionsAgreedVersion |
| wolle-aus-hamburg | wolle@example.org | 1234 | wolle | Wolle aus Hamburg | 0.0.4 |
| matt-rider | matt@example.org | 4321 | matt | Matt Rider | 0.0.4 |
| slug | email | password | id | name | termsAndConditionsAgreedVersion |
| wolle-aus-hamburg | wolle@example.org | 1234 | wolle | Wolfgang aus Hamburg | 0.0.4 |
| matt-rider | matt@example.org | 4321 | matt | Matt Rider | 0.0.4 |
Scenario: Mention another user, re-login as this user and see notifications
Given I am logged in as "wolle-aus-hamburg"

View File

@ -1,9 +1,12 @@
import { When } from "cypress-cucumber-preprocessor/steps";
When('I add a social media link', () => {
cy.get('input#addSocialMedia')
.type('https://freeradical.zone/peter-pan')
.get('button')
cy.get('button')
.contains('Add link')
.click()
.get('#editSocialMedia')
.type('https://freeradical.zone/peter-pan')
.get('button')
.contains('Save')
.click()
})

View File

@ -2,9 +2,12 @@ import { Given } from "cypress-cucumber-preprocessor/steps";
Given('I have added a social media link', () => {
cy.visit('/settings/my-social-media')
.get('input#addSocialMedia')
.type('https://freeradical.zone/peter-pan')
.get('button')
.contains('Add link')
.click()
.get('#editSocialMedia')
.type('https://freeradical.zone/peter-pan')
.get('button')
.contains('Save')
.click()
})

View File

@ -1,7 +1,7 @@
{
"name": "ocelot-social",
"version": "1.0.6",
"description": "Fullstack and API tests with cypress and cucumber for ocelot.social",
"version": "1.0.7",
"description": "Free and open source software program code available to run social networks.",
"author": "ocelot.social Community",
"license": "MIT",
"private": false,
@ -33,18 +33,18 @@
"cypress": "^7.0.1",
"cypress-cucumber-preprocessor": "^2.2.1",
"cypress-file-upload": "^3.5.3",
"date-fns": "^2.23.0",
"date-fns": "^2.25.0",
"dotenv": "^8.2.0",
"expect": "^25.3.0",
"faker": "Marak/faker.js#master",
"@faker-js/faker": "5.1.0",
"graphql-request": "^2.0.0",
"import": "^0.0.6",
"jsonwebtoken": "^8.5.1",
"mock-socket": "^9.0.3",
"neo4j-driver": "^4.0.2",
"neo4j-driver": "^4.3.4",
"neode": "^0.4.7",
"npm-run-all": "^4.1.5",
"rosie": "^2.0.1",
"rosie": "^2.1.0",
"slug": "^5.1.0"
},
"resolutions": {

1
webapp/.nvmrc Normal file
View File

@ -0,0 +1 @@
v12.19.0

View File

@ -1,8 +1,8 @@
import faker from '@faker-js/faker'
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import HcCommentList from './CommentList.vue'
import helpers from '~/storybook/helpers'
import faker from 'faker'
helpers.init()

View File

@ -3,12 +3,16 @@
<template #default="{ openMenu, closeMenu }">
<slot name="button">
<menu-bar-button
class="legend-question-button"
icon="question-circle"
circle
ghost
class="legend-question-button"
@mouseover.native="openMenu()"
@mouseleave.native="closeMenu()"
:onClick="
() => {
isDropdownOpen ? closeMenu() : openMenu()
isDropdownOpen = !isDropdownOpen
}
"
/>
</slot>
</template>
@ -60,6 +64,7 @@ export default {
{ iconName: 'quote-right', name: `editor.legend.quote`, shortcut: '> + space' },
{ iconName: 'minus', name: `editor.legend.ruler`, shortcut: '---' },
],
isDropdownOpen: false,
}
},
}

View File

@ -11,6 +11,13 @@ export default class EventHandler extends Extension {
new Plugin({
props: {
transformPastedText(text) {
/* remove hashtag from d.tube url
* hashtags in url general are not a problem because the following link work like expected:
* http://www.nsosp.org/de/Quanten-Fluss-Theorie/index.php#OM:FrQFT:Home:Inhalt
*/
if (text.search(/d.tube/) > 0) {
text = text.replace(/\/#!\//gim, '/')
}
return text.trim()
},
transformPastedHTML(html) {

View File

@ -179,10 +179,24 @@ export default {
}
.html {
width: 100%;
// width: 100%;
// height: 100%;
// see this working solution here: https://stackoverflow.com/questions/35814653/automatic-height-when-embedding-a-youtube-video
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
iframe {
// width: 100%;
// height: auto;
// same solution example as above
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}

View File

@ -33,8 +33,8 @@ describe('PageFooter.vue', () => {
expect(wrapper.findAll('a')).toHaveLength(5)
})
it('renders three nuxt-links', () => {
expect(wrapper.findAll('.nuxt-link')).toHaveLength(3)
it('renders four nuxt-links', () => {
expect(wrapper.findAll('.nuxt-link')).toHaveLength(4)
})
it('renders version', () => {

View File

@ -40,7 +40,7 @@ describe('ChangePassword ', () => {
describe('given email and nonce', () => {
beforeEach(() => {
propsData.email = 'mail@example.org'
propsData.nonce = '123456'
propsData.nonce = '12345'
})
describe('submitting new password', () => {
@ -57,7 +57,7 @@ describe('ChangePassword ', () => {
it('delivers new password to backend', () => {
const expected = expect.objectContaining({
variables: { nonce: '123456', email: 'mail@example.org', password: 'supersecret' },
variables: { nonce: '12345', email: 'mail@example.org', password: 'supersecret' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})

View File

@ -22,6 +22,7 @@
<script>
import gql from 'graphql-tag'
import CONSTANTS_REGISTRATION from './../../constants/registration'
export const isValidInviteCodeQuery = gql`
query($code: ID!) {
@ -41,10 +42,12 @@ export default {
formSchema: {
inviteCode: {
type: 'string',
min: 6,
max: 6,
min: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH,
max: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH,
required: true,
message: this.$t('components.registration.invite-code.form.validations.length'),
message: this.$t('components.registration.invite-code.form.validations.length', {
inviteCodeLength: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH,
}),
},
},
dbRequestInProgress: false,

View File

@ -25,6 +25,8 @@
<script>
import gql from 'graphql-tag'
import { isEmail } from 'validator'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import EmailDisplayAndVerify from './EmailDisplayAndVerify'
export const verifyNonceQuery = gql`
@ -48,10 +50,12 @@ export default {
formSchema: {
nonce: {
type: 'string',
min: 5,
max: 5,
min: CONSTANTS_REGISTRATION.NONCE_LENGTH,
max: CONSTANTS_REGISTRATION.NONCE_LENGTH,
required: true,
message: this.$t('components.registration.email-nonce.form.validations.length'),
message: this.$t('components.registration.email-nonce.form.validations.length', {
nonceLength: CONSTANTS_REGISTRATION.NONCE_LENGTH,
}),
},
},
dbRequestInProgress: false,

View File

@ -97,7 +97,7 @@ storiesOf('RegistrationSlider', module)
email: 'wolle.huss@pjannto.com',
emailSend: false,
nonce: '47539',
name: 'Wolle',
name: 'Wolfgang',
password: 'Hello',
passwordConfirmation: 'Hello',
termsAndConditionsConfirmed: true,
@ -127,7 +127,7 @@ storiesOf('RegistrationSlider', module)
email: 'wolle.huss@pjannto.com',
emailSend: false,
nonce: '47539',
name: 'Wolle',
name: 'Wolfgang',
password: 'Hello',
passwordConfirmation: 'Hello',
termsAndConditionsConfirmed: true,
@ -171,7 +171,7 @@ storiesOf('RegistrationSlider', module)
email: 'wolle.huss@pjannto.com',
emailSend: true,
nonce: '47539',
name: 'Wolle',
name: 'Wolfgang',
password: 'Hello',
passwordConfirmation: 'Hello',
termsAndConditionsConfirmed: true,

View File

@ -0,0 +1,128 @@
import { mount } from '@vue/test-utils'
import MySomethingList from './MySomethingList.vue'
import Vue from 'vue'
const localVue = global.localVue
describe('MySomethingList.vue', () => {
let wrapper
let propsData
let data
let mocks
beforeEach(() => {
propsData = {
useFormData: { dummy: '' },
useItems: [{ id: 'id', dummy: 'dummy' }],
namePropertyKey: 'dummy',
callbacks: { edit: jest.fn(), submit: jest.fn(), delete: jest.fn() },
}
data = () => {
return {}
}
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest.fn(),
},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
}
})
describe('mount', () => {
let form, slots
const Wrapper = () => {
slots = {
'list-item': '<div class="list-item"></div>',
'edit-item': '<div class="edit-item"></div>',
}
return mount(MySomethingList, {
propsData,
data,
mocks,
localVue,
slots,
})
}
describe('given existing item', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('for each item it', () => {
it('displays the item as slot "list-item"', () => {
expect(wrapper.find('.list-item').exists()).toBe(true)
})
it('displays the edit button', () => {
expect(wrapper.find('.base-button[data-test="edit-button"]').exists()).toBe(true)
})
it('displays the delete button', () => {
expect(wrapper.find('.base-button[data-test="delete-button"]').exists()).toBe(true)
})
})
describe('editing item', () => {
beforeEach(async () => {
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
editButton.trigger('click')
await Vue.nextTick()
})
it('disables adding items while editing', () => {
const submitButton = wrapper.find('.base-button[data-test="add-save-button"]')
expect(submitButton.text()).not.toContain('settings.social-media.submit')
})
it('allows the user to cancel editing', async () => {
expect(wrapper.find('.edit-item').exists()).toBe(true)
const cancelButton = wrapper.find('button#cancel')
cancelButton.trigger('click')
await Vue.nextTick()
expect(wrapper.find('.edit-item').exists()).toBe(false)
})
})
describe('calls callback functions', () => {
it('calls edit', async () => {
const editButton = wrapper.find('.base-button[data-test="edit-button"]')
editButton.trigger('click')
await Vue.nextTick()
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
expect(propsData.callbacks.edit).toHaveBeenCalledTimes(1)
expect(propsData.callbacks.edit).toHaveBeenCalledWith(expect.any(Object), expectedItem)
})
it('calls submit', async () => {
form = wrapper.find('form')
form.trigger('submit')
await Vue.nextTick()
form.trigger('submit')
await Vue.nextTick()
const expectedItem = expect.objectContaining({ id: '' })
expect(propsData.callbacks.submit).toHaveBeenCalledTimes(1)
expect(propsData.callbacks.submit).toHaveBeenCalledWith(
expect.any(Object),
true,
expectedItem,
{ dummy: '' },
)
})
it('calls delete', async () => {
const deleteButton = wrapper.find('.base-button[data-test="delete-button"]')
deleteButton.trigger('click')
await Vue.nextTick()
const expectedItem = expect.objectContaining({ id: 'id', dummy: 'dummy' })
expect(propsData.callbacks.delete).toHaveBeenCalledTimes(1)
expect(propsData.callbacks.delete).toHaveBeenCalledWith(expect.any(Object), expectedItem)
})
})
})
})
})

View File

@ -0,0 +1,185 @@
<template>
<ds-form
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
@submit="handleSubmitItem"
>
<div v-if="isEditing">
<ds-space margin="base">
<ds-heading tag="h5">
{{
isCreation
? $t('settings.social-media.addNewTitle')
: $t('settings.social-media.editTitle', { name: editingItem[namePropertyKey] })
}}
</ds-heading>
</ds-space>
<ds-space v-if="items" margin-top="base">
<slot name="edit-item" />
</ds-space>
</div>
<div v-else>
<ds-space v-if="items" margin-top="base">
<ds-list>
<ds-list-item v-for="item in items" :key="item.id" class="list-item--high">
<template>
<slot name="list-item" :item="item" />
<span class="divider">|</span>
<base-button
icon="edit"
circle
ghost
@click="handleEditItem(item)"
:title="$t('actions.edit')"
data-test="edit-button"
/>
<base-button
icon="trash"
circle
ghost
@click="handleDeleteItem(item)"
:title="$t('actions.delete')"
data-test="delete-button"
/>
</template>
</ds-list-item>
</ds-list>
</ds-space>
</div>
<ds-space margin-top="base">
<ds-space margin-top="base">
<base-button
filled
:disabled="loading || !(!isEditing || (isEditing && !disabled))"
:loading="loading"
type="submit"
data-test="add-save-button"
>
{{ isEditing ? $t('actions.save') : $t('settings.social-media.submit') }}
</base-button>
<base-button v-if="isEditing" id="cancel" danger @click="handleCancel()">
{{ $t('actions.cancel') }}
</base-button>
</ds-space>
</ds-space>
</ds-form>
</template>
<script>
export default {
name: 'MySomethingList',
props: {
useFormData: {
type: Object,
default: () => ({}),
},
useFormSchema: {
type: Object,
default: () => ({}),
},
useItems: {
type: Array,
default: () => [],
},
defaultItem: {
type: Object,
default: () => ({}),
},
namePropertyKey: {
type: String,
required: true,
},
callbacks: {
type: Object,
default: () => ({
handleInput: () => {},
handleInputValid: () => {},
edit: () => {},
submit: () => {},
delete: () => {},
}),
},
},
data() {
return {
formData: this.useFormData,
formSchema: this.useFormSchema,
items: this.useItems,
disabled: true,
loading: false,
editingItem: null,
}
},
computed: {
isEditing() {
return this.editingItem !== null
},
isCreation() {
return this.editingItem !== null && this.editingItem.id === ''
},
},
watch: {
// can change by a parents callback and again given trough by v-bind from there
useItems(newItems) {
this.items = newItems
},
},
methods: {
handleInput(data) {
this.callbacks.handleInput(this, data)
this.disabled = true
},
handleInputValid(data) {
this.callbacks.handleInputValid(this, data)
},
handleEditItem(item) {
this.editingItem = item
this.callbacks.edit(this, item)
},
async handleSubmitItem() {
if (!this.isEditing) {
this.handleEditItem({ ...this.defaultItem, id: '' })
} else {
this.loading = true
if (await this.callbacks.submit(this, this.isCreation, this.editingItem, this.formData)) {
this.disabled = true
this.editingItem = null
}
this.loading = false
}
},
handleCancel() {
this.editingItem = null
this.disabled = true
},
async handleDeleteItem(item) {
await this.callbacks.delete(this, item)
},
},
}
</script>
<style lang="scss" scope>
.divider {
opacity: 0.4;
padding: 0 $space-small;
}
.icon-button {
cursor: pointer;
}
.list-item--high {
.ds-list-item-prefix {
align-self: center;
}
.ds-list-item-content {
display: flex;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,36 @@
import { shallowMount } from '@vue/test-utils'
import SocialMediaListItem from './SocialMediaListItem.vue'
describe('SocialMediaListItem.vue', () => {
let wrapper
let propsData
const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
const faviconUrl = 'https://freeradical.zone/favicon.ico'
beforeEach(() => {
propsData = {}
})
describe('shallowMount', () => {
const Wrapper = () => {
return shallowMount(SocialMediaListItem, { propsData })
}
describe('given existing social media links', () => {
beforeEach(() => {
propsData = { item: { id: 's1', url: socialMediaUrl, favicon: faviconUrl } }
wrapper = Wrapper()
})
describe('for each link item it', () => {
it('displays the favicon', () => {
expect(wrapper.find(`img[src="${faviconUrl}"]`).exists()).toBe(true)
})
it('displays the url', () => {
expect(wrapper.find(`a[href="${socialMediaUrl}"]`).exists()).toBe(true)
})
})
})
})
})

View File

@ -0,0 +1,18 @@
<template>
<a :href="item.url" target="_blank">
<img :src="item.favicon" alt="Link:" height="16" width="16" />
{{ item.url }}
</a>
</template>
<script>
export default {
name: 'SocialMediaListItem',
props: {
item: {
type: Object,
default: () => ({}),
},
},
}
</script>

View File

@ -1,3 +1,4 @@
import faker from '@faker-js/faker'
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import HcEmpty from '~/components/Empty/Empty'
@ -8,7 +9,6 @@ import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcHashtag from '~/components/Hashtag/Hashtag'
import helpers from '~/storybook/helpers'
import faker from 'faker'
import { post } from '~/components/PostTeaser/PostTeaser.story.js'
import { user } from '~/components/UserTeaser/UserTeaser.story.js'

View File

@ -83,7 +83,7 @@ const DATA_PRIVACY = defaultPageParamsPages.DATA_PRIVACY.overwrite({
},
})
const FAQ = defaultPageParamsPages.FAQ.overwrite({
// externalLink: 'https://ocelot.social', // if string is defined and not empty it's dominating
// externalLink: null, // if string is defined and not empty it's dominating
internalPage: {
// footerIdent: 'site.faq', // localized string identifier, if undefined default is used
@ -96,7 +96,7 @@ const FAQ = defaultPageParamsPages.FAQ.overwrite({
},
})
const SUPPORT = defaultPageParamsPages.SUPPORT.overwrite({
// externalLink: 'https://ocelot.social', // if string is defined and not empty it's dominating
// externalLink: null, // if string is defined and not empty it's dominating
internalPage: {
// footerIdent: 'site.support', // localized string identifier, if undefined default is used

View File

@ -0,0 +1,5 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
export default {
NONCE_LENGTH: 5,
INVITE_CODE_LENGTH: 6,
}

View File

@ -170,7 +170,7 @@
"nonce": "E-Mail-Code: 32143",
"validations": {
"error": "Ungültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!",
"length": "muss genau 5 Buchstaben lang sein",
"length": "muss genau {nonceLength} Buchstaben lang sein",
"success": "Gültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!"
}
},
@ -184,7 +184,7 @@
"next": "Weiter",
"validations": {
"error": "Ungültiger Einladungs-Code <b>{inviteCode}</b>!",
"length": "muss genau 6 Buchstaben lang sein",
"length": "muss genau {inviteCodeLength} Buchstaben lang sein",
"success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!"
}
}
@ -773,6 +773,8 @@
"name": "Sicherheit"
},
"social-media": {
"addNewTitle": "Neuen Link hinzufügen",
"editTitle": "Link \"{name}\" ändern",
"name": "Soziale Netzwerke",
"placeholder": "Deine Webadresse des Sozialen Netzwerkes",
"requireUnique": "Dieser Link existiert bereits",

View File

@ -170,7 +170,7 @@
"nonce": "E-mail code: 32143",
"validations": {
"error": "Invalid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!",
"length": "must be 5 characters long",
"length": "must be {nonceLength} characters long",
"success": "Valid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!"
}
},
@ -184,7 +184,7 @@
"next": "Continue",
"validations": {
"error": "Invalid invite code <b>{inviteCode}</b>!",
"length": "must be 6 characters long",
"length": "must be {inviteCodeLength} characters long",
"success": "Valid invite code <b>{inviteCode}</b>!"
}
}
@ -773,6 +773,8 @@
"name": "Security"
},
"social-media": {
"addNewTitle": "Add new link",
"editTitle": "Edit link \"{name}\"",
"name": "Social media",
"placeholder": "Your social media url",
"requireUnique": "You added this url already",

View File

@ -143,7 +143,7 @@
"next": "Continuar",
"nonce": "Introduzca el código",
"validations": {
"length": "debe tener exactamente 5 letras"
"length": "debe tener exactamente {nonceLength} letras"
}
}
},

View File

@ -143,7 +143,7 @@
"next": "Continuer",
"nonce": "Entrez votre code",
"validations": {
"length": "doit comporter 5 caractères"
"length": "doit comporter {nonceLength} caractères"
}
}
},

View File

@ -1,4 +1,60 @@
<!-- this file is replaced on rebranding by https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/locales/html/ -->
<!-- you can find and store templates at https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/templates/ -->
<p>Hier steht was zu den Spenden.</p>
<div class="info-page">
<h2>
Für das soziale Netzwerk Ocelot.Social für Entwickler
</h2>
<h3>
Wohin kann ich spenden?
</h3>
<p>
Hier steht was zu den Spenden.
</p>
</div>
<style type="text/css">
.info-page {
margin-bottom: 48px;
}
.info-page h2 {
margin: 24px 0;
}
.info-page h3 {
margin: 24px 0 16px 0;
}
.info-page h4 {
margin: 16px 0 8px 0;
}
.info-page p {
margin: 8px 0;
}
.info-page ul {
list-style-type: disc;
margin: 16px 0 16px 14px;
}
.info-page table {
background-color: #fff;
border: 1px solid #e0dede;
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0,.08),inset 0 0 0 1px rgba(255,255,255,.5);
margin: 16px 0;
max-width: 100%;
}
.info-page table thead {
background-color: #f0f0f0;
}
.info-page table td,
.info-page table th {
border: 1px solid #e0dede;
padding: 10px;
}
</style>

View File

@ -1,4 +1,60 @@
<!-- this file is replaced on rebranding by https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/locales/html/ -->
<!-- you can find and store templates at https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/templates/ -->
<p>Hier wird das Netzwerk beschrieben.</p>
<div class="info-page">
<h2>
Für das soziale Netzwerk Ocelot.Social für Entwickler
</h2>
<h3>
Das Entwicklernetzwerk
</h3>
<p>
Hier wird das Netzwerk beschrieben.
</p>
</div>
<style type="text/css">
.info-page {
margin-bottom: 48px;
}
.info-page h2 {
margin: 24px 0;
}
.info-page h3 {
margin: 24px 0 16px 0;
}
.info-page h4 {
margin: 16px 0 8px 0;
}
.info-page p {
margin: 8px 0;
}
.info-page ul {
list-style-type: disc;
margin: 16px 0 16px 14px;
}
.info-page table {
background-color: #fff;
border: 1px solid #e0dede;
border-collapse: collapse;
box-shadow: 0 1px 3px rgba(0,0,0,.08),inset 0 0 0 1px rgba(255,255,255,.5);
margin: 16px 0;
max-width: 100%;
}
.info-page table thead {
background-color: #f0f0f0;
}
.info-page table td,
.info-page table th {
border: 1px solid #e0dede;
padding: 10px;
}
</style>

View File

@ -104,7 +104,7 @@
"next": "Kontynuuj",
"nonce": "Wprowadź swój kod",
"validations": {
"length": "musi mieć długość 5 znaków."
"length": "musi mieć długość {nonceLength} znaków."
}
}
}

View File

@ -190,7 +190,7 @@
"next": "Continue",
"nonce": "Digite seu código",
"validations": {
"length": "deve ter 5 caracteres"
"length": "deve ter {nonceLength} caracteres"
}
}
},

View File

@ -143,7 +143,7 @@
"next": "Продолжить",
"nonce": "Введите код",
"validations": {
"length": "длина должна быть 5 символов"
"length": "длина должна быть {nonceLength} символов"
}
}
},

View File

@ -1,6 +1,6 @@
{
"name": "@ocelot-social/maintenance",
"version": "1.0.6",
"version": "1.0.7",
"description": "Maintenance page for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",

View File

@ -201,6 +201,7 @@ export default {
/** * A Boolean indicating if the cookie transmission requires a
* secure protocol (https). Defaults to false. */
secure: CONFIG.COOKIE_HTTPS_ONLY,
sameSite: 'lax', // for the meaning see https://www.thinktecture.com/de/identity/samesite/samesite-in-a-nutshell/
},
// includeNodeModules: true, // optional, default: false (this includes graphql-tag for node_modules folder)

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-webapp",
"version": "1.0.6",
"version": "1.0.7",
"description": "ocelot.social Frontend",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -108,6 +108,7 @@
"@babel/core": "~7.12.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "~7.9.0",
"@faker-js/faker": "5.1.0",
"@storybook/addon-a11y": "^6.3.6",
"@storybook/addon-actions": "^5.3.21",
"@storybook/addon-notes": "^5.3.18",
@ -136,7 +137,6 @@
"eslint-plugin-promise": "~4.3.1",
"eslint-plugin-standard": "~5.0.0",
"eslint-plugin-vue": "~6.2.2",
"faker": "^5.1.0",
"flush-promises": "^1.0.2",
"identity-obj-proxy": "^3.0.0",
"jest": "~26.6.3",

View File

@ -123,8 +123,8 @@ describe('Registration', () => {
expect(wrapper.find('.hc-empty').exists()).toBe(true)
})
it('"inviteCode=AAAAAA" query in URI', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' }
it('"inviteCode=ABCDEF" query in URI', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'ABCDEF' }
wrapper = await Wrapper()
expect(wrapper.find('.hc-empty').exists()).toBe(true)
})
@ -181,12 +181,12 @@ describe('Registration', () => {
expect(wrapper.find('.enter-invite').exists()).toBe(true)
})
it('"inviteCode=AAAAAA" query in URI have invite code in input', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' }
it('"inviteCode=ABCDEF" query in URI have invite code in input', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'ABCDEF' }
wrapper = await Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-invite')
expect(form.vm.formData.inviteCode).toEqual('AAAAAA')
expect(form.vm.formData.inviteCode).toEqual('ABCDEF')
})
})
})
@ -294,12 +294,12 @@ describe('Registration', () => {
expect(wrapper.find('.enter-invite').exists()).toBe(true)
})
it('"inviteCode=AAAAAA" query in URI have invite code in input', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' }
it('"inviteCode=ABCDEF" query in URI have invite code in input', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'ABCDEF' }
wrapper = await Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-invite')
expect(form.vm.formData.inviteCode).toEqual('AAAAAA')
expect(form.vm.formData.inviteCode).toEqual('ABCDEF')
})
})
})

View File

@ -33,7 +33,7 @@ describe('my-social-media.vue', () => {
})
describe('mount', () => {
let form, input, submitButton
let form, input
const Wrapper = () => {
const store = new Vuex.Store({
getters,
@ -42,11 +42,12 @@ describe('my-social-media.vue', () => {
}
describe('adding social media link', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper = Wrapper()
form = wrapper.find('form')
input = wrapper.find('input#addSocialMedia')
submitButton = wrapper.find('button')
form.trigger('submit')
await Vue.nextTick()
input = wrapper.find('input#editSocialMedia')
})
it('requires the link to be a valid url', async () => {
@ -79,7 +80,6 @@ describe('my-social-media.vue', () => {
const expected = expect.objectContaining({
variables: { url: newSocialMediaUrl },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
@ -88,10 +88,10 @@ describe('my-social-media.vue', () => {
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('clears the form', async () => {
it('switches back to list', async () => {
await flushPromises()
expect(input.value).toBe(undefined)
expect(submitButton.vm.$attrs.disabled).toBe(true)
const submitButton = wrapper.find('.base-button[data-test="add-save-button"]')
expect(submitButton.text()).not.toContain('settings.social-media.submit')
})
})
})
@ -100,10 +100,9 @@ describe('my-social-media.vue', () => {
beforeEach(() => {
getters = {
'auth/user': () => ({
socialMedia: [{ id: 's1', url: socialMediaUrl }],
socialMedia: [{ id: 's1', url: socialMediaUrl, favicon: faviconUrl }],
}),
}
wrapper = Wrapper()
form = wrapper.find('form')
})
@ -116,18 +115,12 @@ describe('my-social-media.vue', () => {
it('displays the url', () => {
expect(wrapper.find(`a[href="${socialMediaUrl}"]`).exists()).toBe(true)
})
it('displays the edit button', () => {
expect(wrapper.find('.base-button[data-test="edit-button"]').exists()).toBe(true)
})
it('displays the delete button', () => {
expect(wrapper.find('.base-button[data-test="delete-button"]').exists()).toBe(true)
})
})
it('does not accept a duplicate url', async () => {
wrapper.find('input#addSocialMedia').setValue(socialMediaUrl)
form.trigger('submit')
await Vue.nextTick()
wrapper.find('input#editSocialMedia').setValue(socialMediaUrl)
form.trigger('submit')
await Vue.nextTick()
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
@ -141,12 +134,6 @@ describe('my-social-media.vue', () => {
input = wrapper.find('input#editSocialMedia')
})
it('disables adding new links while editing', () => {
const addInput = wrapper.find('input#addSocialMedia')
expect(addInput.exists()).toBe(false)
})
it('sends the new url to the backend', async () => {
const expected = expect.objectContaining({
variables: { id: 's1', url: newSocialMediaUrl },
@ -156,13 +143,6 @@ describe('my-social-media.vue', () => {
await Vue.nextTick()
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('allows the user to cancel editing', async () => {
const cancelButton = wrapper.find('button#cancel')
cancelButton.trigger('click')
await Vue.nextTick()
expect(wrapper.find('input#editSocialMedia').exists()).toBe(false)
})
})
describe('deleting social media link', () => {
@ -176,7 +156,6 @@ describe('my-social-media.vue', () => {
const expected = expect.objectContaining({
variables: { id: 's1' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})

View File

@ -1,97 +1,73 @@
<template>
<ds-form
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
@submit="handleSubmitSocialMedia"
>
<base-card>
<h2 class="title">{{ $t('settings.social-media.name') }}</h2>
<ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small">
<ds-list>
<ds-list-item v-for="link in socialMediaLinks" :key="link.id" class="list-item--high">
<ds-input
v-if="editingLink.id === link.id"
id="editSocialMedia"
model="socialMediaUrl"
type="text"
:placeholder="$t('settings.social-media.placeholder')"
/>
<template v-else>
<a :href="link.url" target="_blank">
<img :src="link.favicon" alt="Link:" height="16" width="16" />
{{ link.url }}
</a>
<span class="divider">|</span>
<base-button
icon="edit"
circle
ghost
@click="handleEditSocialMedia(link)"
:title="$t('actions.edit')"
data-test="edit-button"
/>
<base-button
icon="trash"
circle
ghost
@click="handleDeleteSocialMedia(link)"
:title="$t('actions.delete')"
data-test="delete-button"
/>
</template>
</ds-list-item>
</ds-list>
</ds-space>
<ds-space margin-top="base">
<base-card>
<ds-heading tag="h2" class="title">{{ $t('settings.social-media.name') }}</ds-heading>
<my-something-list
:useFormData="useFormData"
:useFormSchema="useFormSchema"
:useItems="socialMediaLinks"
:defaultItem="{ url: '' }"
:namePropertyKey="'url'"
:callbacks="{
handleInput: () => {},
handleInputValid,
edit: callbackEditSocialMedia,
submit: handleSubmitSocialMedia,
delete: callbackDeleteSocialMedia,
}"
>
<template #list-item="{ item }">
<social-media-list-item :item="item" />
</template>
<template #edit-item>
<ds-input
v-if="!editingLink.id"
id="addSocialMedia"
id="editSocialMedia"
model="socialMediaUrl"
type="text"
:placeholder="$t('settings.social-media.placeholder')"
/>
<ds-space margin-top="base">
<base-button filled :disabled="disabled" type="submit">
{{ editingLink.id ? $t('actions.save') : $t('settings.social-media.submit') }}
</base-button>
<base-button v-if="editingLink.id" id="cancel" danger @click="handleCancel()">
{{ $t('actions.cancel') }}
</base-button>
</ds-space>
</ds-space>
</base-card>
</ds-form>
</template>
</my-something-list>
</base-card>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import unionBy from 'lodash/unionBy'
import gql from 'graphql-tag'
import { mapGetters, mapMutations } from 'vuex'
import MySomethingList from '~/components/_new/features/MySomethingList/MySomethingList.vue'
import SocialMediaListItem from '~/components/_new/features/SocialMedia/SocialMediaListItem.vue'
export default {
components: {
MySomethingList,
SocialMediaListItem,
},
data() {
return {
formData: {
useFormData: {
socialMediaUrl: '',
},
formSchema: {
useFormSchema: {
socialMediaUrl: {
type: 'url',
message: this.$t('common.validations.url'),
},
},
disabled: true,
editingLink: {},
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
currentSocialMediaLinks() {
const domainRegex = /^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g
const { socialMedia = [] } = this.currentUser
return socialMedia.map(({ id, url }) => {
const [domain] = url.match(domainRegex) || []
const favicon = domain ? `${domain}/favicon.ico` : null
return { id, url, favicon }
})
},
socialMediaLinks() {
const domainRegex = /^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g
const { socialMedia = [] } = this.currentUser
@ -106,28 +82,83 @@ export default {
...mapMutations({
setCurrentUser: 'auth/SET_USER',
}),
handleCancel() {
this.editingLink = {}
this.formData.socialMediaUrl = ''
this.disabled = true
},
handleEditSocialMedia(link) {
this.editingLink = link
this.formData.socialMediaUrl = link.url
},
handleInput(data) {
this.disabled = true
},
handleInputValid(data) {
handleInputValid(thisList, data) {
if (data.socialMediaUrl.length < 1) {
this.disabled = true
thisList.disabled = true
} else {
this.disabled = false
thisList.disabled = false
}
},
async handleDeleteSocialMedia(link) {
callbackEditSocialMedia(thisList, link) {
thisList.formData.socialMediaUrl = link.url
// try to set focus on link edit field
// thisList.$refs.socialMediaUrl.$el.focus()
// !!! Check for existenz
// this.$scopedSlots.default()[0].context.$refs
// thisList.$scopedSlots['edit-item']()[0].$el.focus()
// console.log(thisList.$scopedSlots['edit-item']()[0].context.$refs)
// console.log(thisList.$scopedSlots['edit-item']()[0].context.$refs)
// console.log(thisList.$refs)
},
async handleSubmitSocialMedia(thisList, isCreation, item, formData) {
item.url = formData.socialMediaUrl
const items = this.socialMediaLinks
const duplicateUrl = items.find((eleItem) => eleItem.url === item.url)
if (duplicateUrl && duplicateUrl.id !== item.id) {
return thisList.$toast.error(thisList.$t('settings.social-media.requireUnique'))
}
let mutation, variables, successMessage
if (isCreation) {
mutation = gql`
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
url
}
}
`
variables = { url: item.url }
successMessage = thisList.$t('settings.social-media.successAdd')
} else {
mutation = gql`
mutation($id: ID!, $url: String!) {
UpdateSocialMedia(id: $id, url: $url) {
id
url
}
}
`
variables = { id: item.id, url: item.url }
successMessage = thisList.$t('settings.data.success')
}
try {
await this.$apollo.mutate({
await thisList.$apollo.mutate({
mutation,
variables,
update: (_store, { data }) => {
const newSocialMedia = !isCreation ? data.UpdateSocialMedia : data.CreateSocialMedia
this.setCurrentUser({
...this.currentUser,
socialMedia: unionBy([newSocialMedia], this.currentUser.socialMedia, 'id'),
})
},
})
thisList.$toast.success(successMessage)
return true
} catch (err) {
thisList.$toast.error(err.message)
return false
}
},
async callbackDeleteSocialMedia(thisList, item) {
try {
await thisList.$apollo.mutate({
mutation: gql`
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
@ -137,11 +168,11 @@ export default {
}
`,
variables: {
id: link.id,
id: item.id,
},
update: (store, { data }) => {
const socialMedia = this.currentUser.socialMedia.filter(
(element) => element.id !== link.id,
(element) => element.id !== item.id,
)
this.setCurrentUser({
...this.currentUser,
@ -150,87 +181,11 @@ export default {
},
})
this.$toast.success(this.$t('settings.social-media.successDelete'))
thisList.$toast.success(thisList.$t('settings.social-media.successDelete'))
} catch (err) {
this.$toast.error(err.message)
}
},
async handleSubmitSocialMedia() {
const isEditing = !!this.editingLink.id
const url = this.formData.socialMediaUrl
const duplicateUrl = this.socialMediaLinks.find((link) => link.url === url)
if (duplicateUrl && duplicateUrl.id !== this.editingLink.id) {
return this.$toast.error(this.$t('settings.social-media.requireUnique'))
}
let mutation = gql`
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
url
}
}
`
const variables = { url }
let successMessage = this.$t('settings.social-media.successAdd')
if (isEditing) {
mutation = gql`
mutation($id: ID!, $url: String!) {
UpdateSocialMedia(id: $id, url: $url) {
id
url
}
}
`
variables.id = this.editingLink.id
successMessage = this.$t('settings.data.success')
}
try {
await this.$apollo.mutate({
mutation,
variables,
update: (store, { data }) => {
const newSocialMedia = isEditing ? data.UpdateSocialMedia : data.CreateSocialMedia
this.setCurrentUser({
...this.currentUser,
socialMedia: unionBy([newSocialMedia], this.currentUser.socialMedia, 'id'),
})
},
})
this.$toast.success(successMessage)
this.formData.socialMediaUrl = ''
this.disabled = true
this.editingLink = {}
} catch (err) {
this.$toast.error(err.message)
thisList.$toast.error(err.message)
}
},
},
}
</script>
<style lang="scss">
.divider {
opacity: 0.4;
padding: 0 $space-small;
}
.icon-button {
cursor: pointer;
}
.list-item--high {
.ds-list-item-prefix {
align-self: center;
}
.ds-list-item-content {
display: flex;
align-items: center;
}
}
</style>

View File

@ -21,18 +21,30 @@ export default ({ app, req, cookie, store }) => {
const changeHandler = async (mutation) => {
if (process.server) return
const newLocale = mutation.payload.locale
const currentLocale = await app.$cookies.get(key)
const isDifferent = newLocale !== currentLocale
const localeInStore = mutation.payload.locale
let cookieExists = true
let localeInCookies = await app.$cookies.get(key)
if (!localeInCookies) {
cookieExists = false
localeInCookies = navigator.language.split('-')[0] // get browser language
}
const isLocaleStoreSameAsCookies = localeInStore === localeInCookies
if (!isDifferent) {
// cookie has to be set, otherwise Cypress test does not work
if (cookieExists && isLocaleStoreSameAsCookies) {
return
}
app.$cookies.set(key, newLocale)
if (!app.$i18n.localeExists(newLocale)) {
import(`~/locales/${newLocale}.json`).then((res) => {
app.$i18n.add(newLocale, res.default)
const expires = new Date()
expires.setDate(expires.getDate() + app.$env.COOKIE_EXPIRE_TIME)
app.$cookies.set(key, localeInStore, {
expires,
// maxAge: app.$env.COOKIE_EXPIRE_TIME * 60 * 60 * 24, // days to seconds
sameSite: 'lax', // for the meaning see https://www.thinktecture.com/de/identity/samesite/samesite-in-a-nutshell/
})
if (!app.$i18n.localeExists(localeInStore)) {
import(`~/locales/${localeInStore}.json`).then((res) => {
app.$i18n.add(localeInStore, res.default)
})
}
@ -42,7 +54,7 @@ export default ({ app, req, cookie, store }) => {
if (user && user._id && token) {
// TODO: SAVE LOCALE
// store.dispatch('usersettings/patch', {
// uiLanguage: newLocale
// uiLanguage: localeInStore
// }, { root: true })
}
}

View File

@ -1,14 +1,13 @@
import Vue from 'vue'
import Vuex from 'vuex'
import faker from 'faker'
import vuexI18n from 'vuex-i18n/dist/vuex-i18n.umd.js'
import Styleguide from '@human-connection/styleguide'
import faker from '@faker-js/faker'
import Filters from '~/plugins/vue-filters'
import Directives from '~/plugins/vue-directives'
import IziToast from '~/plugins/izi-toast'
import layout from './layout.vue'
import locales from '~/locales/index.js'
import '~/plugins/v-tooltip'
const helpers = {

View File

@ -2379,6 +2379,11 @@
ts-node "^8"
tslib "^1"
"@faker-js/faker@5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.1.0.tgz#cee1d77ada0d0dbbe77201d18b1ebabf432d9c0f"
integrity sha512-0VonSKh7fBCqvY+V2FLN2ZW4pR4ZtWJalWmwSaiaB7yK7y4qp8vDfuaq9QdLjf/cdZGx3M7Wc4Q+x4fZHxI21Q==
"@hapi/address@2.x.x":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.0.0.tgz#9f05469c88cb2fd3dcd624776b54ee95c312126a"
@ -10235,11 +10240,6 @@ fake-tag@^1.0.0:
resolved "https://registry.yarnpkg.com/fake-tag/-/fake-tag-1.0.1.tgz#1d59da482240a02bd83500ca98976530ed154b0d"
integrity sha512-qmewZoBpa71mM+y6oxXYW/d1xOYQmeIvnEXAt1oCmdP0sqcogWYLepR87QL1jQVLSVMVYDq2cjY6ec/Wu8/4pg==
faker@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/faker/-/faker-5.1.0.tgz#e10fa1dec4502551aee0eb771617a7e7b94692e8"
integrity sha512-RrWKFSSA/aNLP0g3o2WW1Zez7/MnMr7xkiZmoCfAGZmdkDQZ6l2KtuXHN5XjdvpRjDl8+3vf+Rrtl06Z352+Mw==
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"

View File

@ -1248,6 +1248,11 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@faker-js/faker@5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.1.0.tgz#cee1d77ada0d0dbbe77201d18b1ebabf432d9c0f"
integrity sha512-0VonSKh7fBCqvY+V2FLN2ZW4pR4ZtWJalWmwSaiaB7yK7y4qp8vDfuaq9QdLjf/cdZGx3M7Wc4Q+x4fZHxI21Q==
"@hapi/address@2.x.x":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.0.0.tgz#9f05469c88cb2fd3dcd624776b54ee95c312126a"
@ -2608,10 +2613,10 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.23.0:
version "2.23.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9"
integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==
date-fns@^2.25.0:
version "2.25.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
date-now@^0.1.4:
version "0.1.4"
@ -3058,10 +3063,6 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
faker@Marak/faker.js#master:
version "5.1.0"
resolved "https://codeload.github.com/Marak/faker.js/tar.gz/91dc8a3372426bc691be56153b33e81a16459f49"
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@ -4457,37 +4458,27 @@ neo-async@^2.6.0:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==
neo4j-driver-bolt-connection@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.3.1.tgz#bbaf0d2287d9be61a480397067c57e82e431fac9"
integrity sha512-xE7tHgoqcHM1qlXKWWxcOag2F+5OgNVeKDW9W+DKLQ7g3YGvutxcUzp1WrAc8hpFGM2IWVSNEeUt5c5SIXlHCg==
neo4j-driver-bolt-connection@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-4.3.4.tgz#de642bb6a62ffc6ae2e280dccf21395b4d1705a2"
integrity sha512-yxbvwGav+N7EYjcEAINqL6D3CZV+ee2qLInpAhx+iNurwbl3zqtBGiVP79SZ+7tU++y3Q1fW5ofikH06yc+LqQ==
dependencies:
neo4j-driver-core "^4.3.1"
neo4j-driver-core "^4.3.4"
text-encoding-utf-8 "^1.0.2"
neo4j-driver-core@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.3.1.tgz#5d92ae3f9ee48e3af2b4a085503368b15a195a98"
integrity sha512-FV194GxpPvgNpettGUMit6hxbMOkBR42j1K6xwfLvm25XYaZJMJzE0ezqS0Vp0G6etUqhUQChh7WYLdTMIztpQ==
neo4j-driver-core@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver-core/-/neo4j-driver-core-4.3.4.tgz#b445a4fbf94dce8441075099bd6ac3133c1cf5ee"
integrity sha512-3tn3j6IRUNlpXeehZ9Xv7dLTZPB4a7APaoJ+xhQyMmYQO3ujDM4RFHc0pZcG+GokmaltT5pUCIPTDYx6ODdhcA==
neo4j-driver@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.0.2.tgz#78de3b91e91572bcbd9d2e02554322fe1ab399ea"
integrity sha512-xQN4BZZsweaNNac7FDYAV6f/JybghwY3lk4fwblS8V5KQ+DBMPe4Pthh672mp+wEYZGyzPalq5CfpcBrWaZ4Gw==
neo4j-driver@^4.2.2, neo4j-driver@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.3.4.tgz#a54f0562f868ee94dff7509df74e3eb2c1f95a85"
integrity sha512-AGrsFFqnoZv4KhJdmKt4mOBV5mnxmV3+/t8KJTOM68jQuEWoy+RlmAaRRaCSU4eY586OFN/R8lg9MrJpZdSFjw==
dependencies:
"@babel/runtime" "^7.5.5"
rxjs "^6.5.2"
text-encoding-utf-8 "^1.0.2"
uri-js "^4.2.2"
neo4j-driver@^4.2.2:
version "4.3.1"
resolved "https://registry.yarnpkg.com/neo4j-driver/-/neo4j-driver-4.3.1.tgz#897c812abf631b5a520162bb1d71d6a1b320d231"
integrity sha512-7MguiDpGWoj10wfQvi7HRLBif39Sz67w5zOyhaDG8OcHfJbU+onlnGfE81AOlOlYSj/JSv196K1RcFXuJMZv8Q==
dependencies:
"@babel/runtime" "^7.5.5"
neo4j-driver-bolt-connection "^4.3.1"
neo4j-driver-core "^4.3.1"
neo4j-driver-bolt-connection "^4.3.4"
neo4j-driver-core "^4.3.4"
rxjs "^6.6.3"
neode@^0.4.7:
@ -5284,19 +5275,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rosie@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/rosie/-/rosie-2.0.1.tgz#c250c4787ce450b72aa9eff26509f68589814fa2"
integrity sha1-wlDEeHzkULcqqe/yZQn2hYmBT6I=
rosie@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/rosie/-/rosie-2.1.0.tgz#0213a9d2b0401a2549cbce5f1cd914caffa22358"
integrity sha512-Dbzdc+prLXZuB/suRptDnBUY29SdGvND3bLg6cll8n7PNqzuyCxSlRfrkn8PqjS9n4QVsiM7RCvxCkKAkTQRjA==
rxjs@^6.3.3, rxjs@^6.5.2:
version "6.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
dependencies:
tslib "^1.9.0"
rxjs@^6.6.3:
rxjs@^6.3.3, rxjs@^6.6.3:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==