mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into brand-reformer-network-first-step
# Conflicts: # backend/src/config/logos.ts # webapp/constants/logos.js
This commit is contained in:
commit
dea77e8c31
@ -26,6 +26,8 @@ SMTP_DKIM_PRIVATKEY=
|
||||
# SMTP_IGNORE_TLS=true
|
||||
# SMTP_USERNAME=
|
||||
# SMTP_PASSWORD=
|
||||
# SMTP_MAX_CONNECTIONS=1
|
||||
# SMTP_MAX_MESSAGES= 10
|
||||
|
||||
JWT_SECRET="b/&&7b78BF&fv/Vd"
|
||||
JWT_EXPIRES="2y"
|
||||
|
||||
@ -22,6 +22,8 @@ FROM base AS build
|
||||
COPY . .
|
||||
ONBUILD COPY ./branding/constants/ src/config/tmp
|
||||
ONBUILD RUN tools/replace-constants.sh
|
||||
# copy categories to brand them (use yarn prod:db:data:categories)
|
||||
ONBUILD COPY branding/constants/ src/constants/
|
||||
ONBUILD COPY ./branding/email/ src/middleware/helpers/email/
|
||||
ONBUILD COPY ./branding/middlewares/ src/middleware/branding/
|
||||
ONBUILD COPY ./branding/data/ src/db/data
|
||||
|
||||
@ -87,9 +87,9 @@ A fresh database needs to be initialized and migrated.
|
||||
# in folder backend while database is running
|
||||
yarn db:migrate init
|
||||
# for docker environments:
|
||||
docker exec backend yarn db:migrate init
|
||||
docker exec ocelot-social-backend-1 yarn db:migrate init
|
||||
# for docker production:
|
||||
docker exec backend yarn prod:migrate init
|
||||
docker exec ocelot-social-backend-1 yarn prod:migrate init
|
||||
```
|
||||
|
||||
```sh
|
||||
@ -97,9 +97,9 @@ docker exec backend yarn prod:migrate init
|
||||
yarn db:migrate up
|
||||
|
||||
# for docker development:
|
||||
docker exec backend yarn db:migrate up
|
||||
docker exec ocelot-social-backend-1 yarn db:migrate up
|
||||
# for docker production
|
||||
docker exec backend yarn prod:migrate up
|
||||
docker exec ocelot-social-backend-1 yarn prod:migrate up
|
||||
```
|
||||
|
||||
### Optional Data
|
||||
@ -131,7 +131,7 @@ To do so, run:
|
||||
yarn db:data:branding
|
||||
|
||||
# for docker
|
||||
docker exec backend yarn db:data:branding
|
||||
docker exec ocelot-social-backend-1 yarn db:data:branding
|
||||
```
|
||||
|
||||
### Seed Data
|
||||
@ -143,7 +143,7 @@ For a predefined set of test data you can seed the database with:
|
||||
yarn db:seed
|
||||
|
||||
# for docker
|
||||
docker exec backend yarn db:seed
|
||||
docker exec ocelot-social-backend-1 yarn db:seed
|
||||
```
|
||||
|
||||
### Reset Data
|
||||
@ -157,9 +157,9 @@ yarn db:reset
|
||||
yarn db:reset:withmigrations
|
||||
|
||||
# for docker
|
||||
docker exec backend yarn db:reset
|
||||
docker exec ocelot-social-backend-1 yarn db:reset
|
||||
# or deleting the migrations as well
|
||||
docker exec backend yarn db:reset:withmigrations
|
||||
docker exec ocelot-social-backend-1 yarn db:reset:withmigrations
|
||||
# you could also wipe out your neo4j database and delete all volumes with:
|
||||
docker compose down -v
|
||||
```
|
||||
@ -180,7 +180,7 @@ $ yarn run db:migrate:create your_data_migration
|
||||
|
||||
# for docker
|
||||
# in main folder while docker compose is running
|
||||
$ docker compose exec backend yarn run db:migrate:create your_data_migration
|
||||
$ docker compose exec ocelot-social-backend-1 yarn run db:migrate:create your_data_migration
|
||||
# Edit the file in ./src/db/migrations/
|
||||
```
|
||||
|
||||
@ -208,5 +208,12 @@ $ yarn run test
|
||||
|
||||
# for docker
|
||||
# in main folder while docker compose is running
|
||||
$ docker exec backend yarn run test
|
||||
$ docker exec ocelot-social-backend-1 yarn run test
|
||||
```
|
||||
|
||||
If the snapshots of the emails must be updated, you have to run the tests in docker! Otherwise the CI will fail.
|
||||
|
||||
```sh
|
||||
# in main folder while docker compose is running
|
||||
$ docker exec ocelot-social-backend-1 yarn run test -u src/emails/
|
||||
```
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
"db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts",
|
||||
"db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create",
|
||||
"prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js",
|
||||
"prod:db:data:branding": "node build/src/db/data-branding.js"
|
||||
"prod:db:data:branding": "node build/src/db/data-branding.js",
|
||||
"prod:db:data:categories": "node build/src/db/categories.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.15.4",
|
||||
|
||||
@ -117,6 +117,10 @@ const options = {
|
||||
ORGANIZATION_URL: emails.ORGANIZATION_LINK,
|
||||
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
|
||||
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
|
||||
INVITE_CODES_PERSONAL_PER_USER:
|
||||
(env.INVITE_CODES_PERSONAL_PER_USER && parseInt(env.INVITE_CODES_PERSONAL_PER_USER)) || 7,
|
||||
INVITE_CODES_GROUP_PER_USER:
|
||||
(env.INVITE_CODES_GROUP_PER_USER && parseInt(env.INVITE_CODES_GROUP_PER_USER)) || 7,
|
||||
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
|
||||
}
|
||||
|
||||
|
||||
32
backend/src/config/logosBranded.ts
Normal file
32
backend/src/config/logosBranded.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// this file is duplicated in `backend/src/config/logos.ts` and `webapp/constants/logos.js` and replaced on rebranding
|
||||
// this are the paths in the webapp
|
||||
import { merge } from 'lodash'
|
||||
|
||||
import logos from '@config/logos'
|
||||
|
||||
const defaultLogos = {
|
||||
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
|
||||
LOGO_HEADER_MOBILE_PATH: '/img/custom/logo-horizontal.svg',
|
||||
LOGO_HEADER_WIDTH: '130px',
|
||||
LOGO_HEADER_MOBILE_WIDTH: '100px',
|
||||
LOGO_HEADER_CLICK: {
|
||||
// externalLink: {
|
||||
// url: 'https://ocelot.social',
|
||||
// target: '_blank',
|
||||
// },
|
||||
externalLink: null,
|
||||
internalPath: {
|
||||
to: {
|
||||
name: 'index',
|
||||
},
|
||||
scrollTo: '.main-navigation',
|
||||
},
|
||||
},
|
||||
LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg',
|
||||
}
|
||||
|
||||
export default merge(defaultLogos, logos)
|
||||
@ -5,98 +5,116 @@ export const CATEGORIES_MAX = 3
|
||||
export const categories = [
|
||||
{
|
||||
icon: 'networking',
|
||||
id: 'cat0',
|
||||
slug: 'networking',
|
||||
name: 'networking',
|
||||
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
|
||||
},
|
||||
{
|
||||
icon: 'home',
|
||||
id: 'cat1',
|
||||
slug: 'home',
|
||||
name: 'home',
|
||||
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
|
||||
},
|
||||
{
|
||||
icon: 'energy',
|
||||
id: 'cat2',
|
||||
slug: 'energy',
|
||||
name: 'energy',
|
||||
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
|
||||
},
|
||||
{
|
||||
icon: 'psyche',
|
||||
id: 'cat3',
|
||||
slug: 'psyche',
|
||||
name: 'psyche',
|
||||
description: 'Seele, Gefühle, Glück',
|
||||
},
|
||||
{
|
||||
icon: 'movement',
|
||||
id: 'cat4',
|
||||
slug: 'body-and-excercise',
|
||||
name: 'body-and-excercise',
|
||||
description: 'Sport, Yoga, Massage, Tanzen, Entspannung',
|
||||
},
|
||||
{
|
||||
icon: 'balance-scale',
|
||||
id: 'cat5',
|
||||
slug: 'law',
|
||||
name: 'law',
|
||||
description: 'Menschenrechte, Gesetze, Verordnungen',
|
||||
},
|
||||
{
|
||||
icon: 'finance',
|
||||
id: 'cat6',
|
||||
slug: 'finance',
|
||||
name: 'finance',
|
||||
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'child',
|
||||
id: 'cat7',
|
||||
slug: 'children',
|
||||
name: 'children',
|
||||
description: 'Familie, Pädagogik, Schule, Prägung',
|
||||
},
|
||||
{
|
||||
icon: 'mobility',
|
||||
id: 'cat8',
|
||||
slug: 'mobility',
|
||||
name: 'mobility',
|
||||
description: 'Reise, Verkehr, Elektromobilität',
|
||||
},
|
||||
{
|
||||
icon: 'shopping-cart',
|
||||
id: 'cat9',
|
||||
slug: 'economy',
|
||||
name: 'economy',
|
||||
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
|
||||
},
|
||||
{
|
||||
icon: 'peace',
|
||||
id: 'cat10',
|
||||
slug: 'peace',
|
||||
name: 'peace',
|
||||
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
|
||||
},
|
||||
{
|
||||
icon: 'politics',
|
||||
id: 'cat11',
|
||||
slug: 'politics',
|
||||
name: 'politics',
|
||||
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
|
||||
},
|
||||
{
|
||||
icon: 'nature',
|
||||
id: 'cat12',
|
||||
slug: 'nature',
|
||||
name: 'nature',
|
||||
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
|
||||
},
|
||||
{
|
||||
icon: 'science',
|
||||
id: 'cat13',
|
||||
slug: 'science',
|
||||
name: 'science',
|
||||
description: 'Bildung, Hochschule, Publikationen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'health',
|
||||
id: 'cat14',
|
||||
slug: 'health',
|
||||
name: 'health',
|
||||
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
|
||||
},
|
||||
{
|
||||
icon: 'media',
|
||||
id: 'cat15',
|
||||
slug: 'it-and-media',
|
||||
name: 'it-and-media',
|
||||
description:
|
||||
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
|
||||
},
|
||||
{
|
||||
icon: 'spirituality',
|
||||
id: 'cat16',
|
||||
slug: 'spirituality',
|
||||
name: 'spirituality',
|
||||
description: 'Religion, Werte, Ethik',
|
||||
},
|
||||
{
|
||||
icon: 'culture',
|
||||
id: 'cat17',
|
||||
slug: 'culture',
|
||||
name: 'culture',
|
||||
description: 'Kunst, Theater, Musik, Fotografie, Film',
|
||||
},
|
||||
{
|
||||
icon: 'miscellaneous',
|
||||
id: 'cat18',
|
||||
slug: 'miscellaneous',
|
||||
name: 'miscellaneous',
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Driver } from 'neo4j-driver'
|
||||
|
||||
export const query =
|
||||
(driver: Driver) =>
|
||||
async ({ query, variables = {} }: { driver; query: string; variables: object }) => {
|
||||
async ({ query, variables = {} }: { query: string; variables?: object }) => {
|
||||
const session = driver.session()
|
||||
|
||||
const result = session.readTransaction(async (transaction) => {
|
||||
@ -19,9 +19,9 @@ export const query =
|
||||
}
|
||||
}
|
||||
|
||||
export const mutate =
|
||||
export const write =
|
||||
(driver: Driver) =>
|
||||
async ({ query, variables = {} }: { driver; query: string; variables: object }) => {
|
||||
async ({ query, variables = {} }: { query: string; variables?: object }) => {
|
||||
const session = driver.session()
|
||||
|
||||
const result = session.writeTransaction(async (transaction) => {
|
||||
@ -44,6 +44,6 @@ export default () => {
|
||||
driver,
|
||||
neode,
|
||||
query: query(driver),
|
||||
mutate: mutate(driver),
|
||||
write: write(driver),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,38 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { categories } from '@constants/categories'
|
||||
import databaseContext from '@context/database'
|
||||
|
||||
import { getDriver } from './neo4j'
|
||||
const { query, write, driver } = databaseContext()
|
||||
|
||||
const createCategories = async () => {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
categories.forEach(({ icon, name }, index) => {
|
||||
const id = `cat${index + 1}`
|
||||
txc.run(
|
||||
`MERGE (c:Category {
|
||||
icon: "${icon}",
|
||||
slug: "${name}",
|
||||
name: "${name}",
|
||||
id: "${id}",
|
||||
createdAt: toString(datetime())
|
||||
})`,
|
||||
)
|
||||
})
|
||||
const result = await query({
|
||||
query: 'MATCH (category:Category) RETURN category { .* }',
|
||||
})
|
||||
try {
|
||||
await createCategoriesTxResultPromise
|
||||
console.log('Successfully created categories!') // eslint-disable-line no-console
|
||||
// eslint-disable-next-line no-catch-all/no-catch-all
|
||||
} catch (error) {
|
||||
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
|
||||
} finally {
|
||||
session.close()
|
||||
driver.close()
|
||||
}
|
||||
|
||||
const existingCategories = result.records.map((r) => r.get('category'))
|
||||
const existingCategoryIds = existingCategories.map((c) => c.id)
|
||||
|
||||
const newCategories = categories.filter((c) => !existingCategoryIds.includes(c.id))
|
||||
|
||||
await write({
|
||||
query: `UNWIND $newCategories AS map
|
||||
CREATE (category:Category)
|
||||
SET category = map
|
||||
SET category.createdAt = toString(datetime())`,
|
||||
variables: {
|
||||
newCategories,
|
||||
},
|
||||
})
|
||||
|
||||
const categoryIds = categories.map((c) => c.id)
|
||||
await write({
|
||||
query: `MATCH (category:Category)
|
||||
WHERE NOT category.id IN $categoryIds
|
||||
DETACH DELETE category`,
|
||||
variables: {
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully created categories!')
|
||||
await driver.close()
|
||||
}
|
||||
|
||||
;(async function () {
|
||||
|
||||
@ -10,7 +10,7 @@ import { Factory } from 'rosie'
|
||||
import slugify from 'slug'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import generateInviteCode from '@graphql/resolvers/helpers/generateInviteCode'
|
||||
import { generateInviteCode } from '@graphql/resolvers/inviteCodes'
|
||||
|
||||
import { getDriver, getNeode } from './neo4j'
|
||||
|
||||
@ -268,17 +268,27 @@ const inviteCodeDefaults = {
|
||||
|
||||
Factory.define('inviteCode')
|
||||
.attrs(inviteCodeDefaults)
|
||||
.option('groupId', null)
|
||||
.option('group', ['groupId'], (groupId) => {
|
||||
if (groupId) {
|
||||
return neode.find('Group', groupId)
|
||||
}
|
||||
})
|
||||
.option('generatedById', null)
|
||||
.option('generatedBy', ['generatedById'], (generatedById) => {
|
||||
if (generatedById) return neode.find('User', generatedById)
|
||||
return Factory.build('user')
|
||||
})
|
||||
.after(async (buildObject, options) => {
|
||||
const [inviteCode, generatedBy] = await Promise.all([
|
||||
const [inviteCode, generatedBy, group] = await Promise.all([
|
||||
neode.create('InviteCode', buildObject),
|
||||
options.generatedBy,
|
||||
options.group,
|
||||
])
|
||||
await Promise.all([inviteCode.relateTo(generatedBy, 'generated')])
|
||||
await inviteCode.relateTo(generatedBy, 'generated')
|
||||
if (group) {
|
||||
await inviteCode.relateTo(group, 'invitesTo')
|
||||
}
|
||||
return inviteCode
|
||||
})
|
||||
|
||||
|
||||
@ -14,4 +14,10 @@ export default {
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
invitesTo: {
|
||||
type: 'relationship',
|
||||
relationship: 'INVITES_TO',
|
||||
target: 'Group',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ footer {
|
||||
<h2>Hello chatReceiver,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>you have received a new chat message from <a class="user" href="http://webapp:3000/user/chatSender/chatsender">chatSender</a>.
|
||||
<p>you have received a new chat message from <a class="user" href="http://webapp:3000/profile/chatSender/chatsender">chatSender</a>.
|
||||
</p><a class="button" href="http://webapp:3000/chat">Show Chat</a>
|
||||
<div class="text-block">
|
||||
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -109,7 +109,7 @@ footer {
|
||||
"text": "HELLO CHATRECEIVER,
|
||||
|
||||
you have received a new chat message from chatSender
|
||||
[http://webapp:3000/user/chatSender/chatsender].
|
||||
[http://webapp:3000/profile/chatSender/chatsender].
|
||||
|
||||
Show Chat [http://webapp:3000/chat]
|
||||
|
||||
@ -218,7 +218,7 @@ footer {
|
||||
<h2>Hallo chatReceiver,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>du hast eine neue Chat-Nachricht von <a class="user" href="http://webapp:3000/user/chatSender/chatsender">chatSender</a> erhalten.
|
||||
<p>du hast eine neue Chat-Nachricht von <a class="user" href="http://webapp:3000/profile/chatSender/chatsender">chatSender</a> erhalten.
|
||||
</p><a class="button" href="http://webapp:3000/chat">Chat anzeigen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -236,7 +236,7 @@ footer {
|
||||
"text": "HALLO CHATRECEIVER,
|
||||
|
||||
du hast eine neue Chat-Nachricht von chatSender
|
||||
[http://webapp:3000/user/chatSender/chatsender] erhalten.
|
||||
[http://webapp:3000/profile/chatSender/chatsender] erhalten.
|
||||
|
||||
Chat anzeigen [http://webapp:3000/chat]
|
||||
|
||||
|
||||
@ -221,9 +221,9 @@ footer {
|
||||
<h2>Hallo User,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456">E-Mail Adresse bestätigen</a>
|
||||
<p>Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. </p>
|
||||
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst du deine neue E-Mail Adresse bestätigen:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456">E-Mail Adresse bestätigen</a>
|
||||
<p>Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. </p>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -239,16 +239,16 @@ footer {
|
||||
"text": "HALLO USER,
|
||||
|
||||
Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button
|
||||
kannst Du Deine neue E-Mail Adresse bestätigen:
|
||||
kannst du deine neue E-Mail Adresse bestätigen:
|
||||
|
||||
E-Mail Adresse bestätigen
|
||||
[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456]
|
||||
|
||||
Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese
|
||||
Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese
|
||||
Nachricht einfach ignorieren.
|
||||
|
||||
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ footer {
|
||||
<h2>Hello Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>your role in the group “The Group” has been changed. Click on the button to view this group:</p><a class="button" href="http://webapp:3000/group/g1/the-group">View group</a>
|
||||
<p>your role in the group “The Group” has been changed. Click on the button to view this group:</p><a class="button" href="http://webapp:3000/groups/g1/the-group">View group</a>
|
||||
<div class="text-block">
|
||||
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– The ocelot.social Team</p><br>
|
||||
@ -110,7 +110,7 @@ footer {
|
||||
your role in the group “The Group” has been changed. Click on the button to view
|
||||
this group:
|
||||
|
||||
View group [http://webapp:3000/group/g1/the-group]
|
||||
View group [http://webapp:3000/groups/g1/the-group]
|
||||
|
||||
See you soon on ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -217,7 +217,7 @@ footer {
|
||||
<h2>Hello Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> commented on a post that you are observing with the title “New Post”. Click on the button to view this comment:
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> commented on a post that you are observing with the title “New Post”. Click on the button to view this comment:
|
||||
</p><a class="button" href="http://webapp:3000/post/p1/new-post#commentId-c1">View comment</a>
|
||||
<div class="text-block">
|
||||
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -234,9 +234,9 @@ footer {
|
||||
"subject": "ocelot.social – Notification: New comment on post",
|
||||
"text": "HELLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] commented on a post that
|
||||
you are observing with the title “New Post”. Click on the button to view this
|
||||
comment:
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] commented on a post
|
||||
that you are observing with the title “New Post”. Click on the button to view
|
||||
this comment:
|
||||
|
||||
View comment [http://webapp:3000/post/p1/new-post#commentId-c1]
|
||||
|
||||
@ -473,7 +473,7 @@ footer {
|
||||
<h2>Hello Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> mentioned you in a comment to the post with the title “New Post”. Click on the button to view this comment:
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> mentioned you in a comment to the post with the title “New Post”. Click on the button to view this comment:
|
||||
</p><a class="button" href="http://webapp:3000/post/p1/new-post#commentId-c1">View comment</a>
|
||||
<div class="text-block">
|
||||
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -490,7 +490,7 @@ footer {
|
||||
"subject": "ocelot.social – Notification: Mentioned in comment",
|
||||
"text": "HELLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] mentioned you in a
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] mentioned you in a
|
||||
comment to the post with the title “New Post”. Click on the button to view this
|
||||
comment:
|
||||
|
||||
@ -977,8 +977,8 @@ footer {
|
||||
<h2>Hello Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> joined the group “The Group”. Click on the button to view this group:
|
||||
</p><a class="button" href="http://webapp:3000/group/g1/the-group">View group</a>
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> joined the group “The Group”. Click on the button to view this group:
|
||||
</p><a class="button" href="http://webapp:3000/groups/g1/the-group">View group</a>
|
||||
<div class="text-block">
|
||||
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– The ocelot.social Team</p><br>
|
||||
@ -994,10 +994,10 @@ footer {
|
||||
"subject": "ocelot.social – Notification: User joined group",
|
||||
"text": "HELLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] joined the group “The
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] joined the group “The
|
||||
Group”. Click on the button to view this group:
|
||||
|
||||
View group [http://webapp:3000/group/g1/the-group]
|
||||
View group [http://webapp:3000/groups/g1/the-group]
|
||||
|
||||
See you soon on ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -1104,8 +1104,8 @@ footer {
|
||||
<h2>Hello Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> left the group “The Group”. Click on the button to view this group:
|
||||
</p><a class="button" href="http://webapp:3000/group/g1/the-group">View group</a>
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> left the group “The Group”. Click on the button to view this group:
|
||||
</p><a class="button" href="http://webapp:3000/groups/g1/the-group">View group</a>
|
||||
<div class="text-block">
|
||||
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– The ocelot.social Team</p><br>
|
||||
@ -1121,10 +1121,10 @@ footer {
|
||||
"subject": "ocelot.social – Notification: User left group",
|
||||
"text": "HELLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] left the group “The
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] left the group “The
|
||||
Group”. Click on the button to view this group:
|
||||
|
||||
View group [http://webapp:3000/group/g1/the-group]
|
||||
View group [http://webapp:3000/groups/g1/the-group]
|
||||
|
||||
See you soon on ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -1231,7 +1231,7 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>deine Rolle in der Gruppe „The Group“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:</p><a class="button" href="http://webapp:3000/group/g1/the-group">Gruppe ansehen</a>
|
||||
<p>deine Rolle in der Gruppe „The Group“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:</p><a class="button" href="http://webapp:3000/groups/g1/the-group">Gruppe ansehen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p><br>
|
||||
@ -1250,7 +1250,7 @@ footer {
|
||||
deine Rolle in der Gruppe „The Group“ wurde geändert. Klicke auf den Knopf, um
|
||||
diese Gruppe zu sehen:
|
||||
|
||||
Gruppe ansehen [http://webapp:3000/group/g1/the-group]
|
||||
Gruppe ansehen [http://webapp:3000/groups/g1/the-group]
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -1357,7 +1357,7 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> hat einen Beitrag den du beobachtest mit dem Titel „New Post“ kommentiert. Klicke auf den Knopf, um diesen Kommentar zu sehen:
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> hat einen Beitrag den du beobachtest mit dem Titel „New Post“ kommentiert. Klicke auf den Knopf, um diesen Kommentar zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/post/p1/new-post#commentId-c1">Kommentar ansehen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -1374,8 +1374,8 @@ footer {
|
||||
"subject": "ocelot.social – Benachrichtigung: Neuer Kommentar zu Beitrag",
|
||||
"text": "HALLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] hat einen Beitrag den du
|
||||
beobachtest mit dem Titel „New Post“ kommentiert. Klicke auf den Knopf, um
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] hat einen Beitrag den
|
||||
du beobachtest mit dem Titel „New Post“ kommentiert. Klicke auf den Knopf, um
|
||||
diesen Kommentar zu sehen:
|
||||
|
||||
Kommentar ansehen [http://webapp:3000/post/p1/new-post#commentId-c1]
|
||||
@ -1613,7 +1613,7 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> hat dich in einem Kommentar zu dem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Kommentar zu sehen:
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> hat dich in einem Kommentar zu dem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Kommentar zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/post/p1/new-post#commentId-c1">Kommentar ansehen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -1630,7 +1630,7 @@ footer {
|
||||
"subject": "ocelot.social – Benachrichtigung: Erwähnung in Kommentar",
|
||||
"text": "HALLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] hat dich in einem
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] hat dich in einem
|
||||
Kommentar zu dem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf,
|
||||
um den Kommentar zu sehen:
|
||||
|
||||
@ -1741,7 +1741,7 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> hat Dich in einem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> hat dich in einem Beitrag mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/post/p1/new-post">Beitrag ansehen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
@ -1758,7 +1758,7 @@ footer {
|
||||
"subject": "ocelot.social – Benachrichtigung: Erwähnung in Beitrag",
|
||||
"text": "HALLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] hat Dich in einem Beitrag
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] hat dich in einem Beitrag
|
||||
mit dem Titel „New Post“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:
|
||||
|
||||
Beitrag ansehen [http://webapp:3000/post/p1/new-post]
|
||||
@ -2117,8 +2117,8 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> ist der Gruppe „The Group“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/group/g1/the-group">Gruppe ansehen</a>
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> ist der Gruppe „The Group“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/groups/g1/the-group">Gruppe ansehen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p><br>
|
||||
@ -2134,10 +2134,10 @@ footer {
|
||||
"subject": "ocelot.social – Benachrichtigung: Nutzer tritt Gruppe bei",
|
||||
"text": "HALLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] ist der Gruppe „The
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] ist der Gruppe „The
|
||||
Group“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen:
|
||||
|
||||
Gruppe ansehen [http://webapp:3000/group/g1/the-group]
|
||||
Gruppe ansehen [http://webapp:3000/groups/g1/the-group]
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -2244,8 +2244,8 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p><a class="user" href="http://webapp:3000/user/u2/peter-lustig">Peter Lustig</a> hat die Gruppe „The Group“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/group/g1/the-group">Gruppe ansehen</a>
|
||||
<p><a class="user" href="http://webapp:3000/profile/u2/peter-lustig">Peter Lustig</a> hat die Gruppe „The Group“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen:
|
||||
</p><a class="button" href="http://webapp:3000/groups/g1/the-group">Gruppe ansehen</a>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p><br>
|
||||
@ -2261,10 +2261,10 @@ footer {
|
||||
"subject": "ocelot.social – Benachrichtigung: Nutzer verlässt Gruppe",
|
||||
"text": "HALLO JENNY ROSTOCK,
|
||||
|
||||
Peter Lustig [http://webapp:3000/user/u2/peter-lustig] hat die Gruppe „The
|
||||
Peter Lustig [http://webapp:3000/profile/u2/peter-lustig] hat die Gruppe „The
|
||||
Group“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen:
|
||||
|
||||
Gruppe ansehen [http://webapp:3000/group/g1/the-group]
|
||||
Gruppe ansehen [http://webapp:3000/groups/g1/the-group]
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
|
||||
@ -230,12 +230,12 @@ footer {
|
||||
<h2>Willkommen bei ocelot.social!</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code">Bestätige Deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
|
||||
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
<p>Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code">Bestätige deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.</p>
|
||||
<p>Falls du dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
</p>
|
||||
<p>PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<p>PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -252,21 +252,21 @@ footer {
|
||||
|
||||
Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt
|
||||
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können
|
||||
… Bitte bestätige Deine E-Mail Adresse:
|
||||
… Bitte bestätige deine E-Mail Adresse:
|
||||
|
||||
Bestätige Deine E-Mail Adresse
|
||||
Bestätige deine E-Mail Adresse
|
||||
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code]
|
||||
|
||||
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert
|
||||
Das funktioniert allerdings nur, wenn du dich über unsere Website registriert
|
||||
hast.
|
||||
|
||||
Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
|
||||
PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach
|
||||
PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach
|
||||
ignorieren. ;)
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
@ -509,12 +509,12 @@ footer {
|
||||
<h2>Willkommen bei ocelot.social!</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail">Bestätige Deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
|
||||
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
<p>Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail">Bestätige deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.</p>
|
||||
<p>Falls du dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
</p>
|
||||
<p>PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<p>PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -531,21 +531,21 @@ footer {
|
||||
|
||||
Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt
|
||||
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können
|
||||
… Bitte bestätige Deine E-Mail Adresse:
|
||||
… Bitte bestätige deine E-Mail Adresse:
|
||||
|
||||
Bestätige Deine E-Mail Adresse
|
||||
Bestätige deine E-Mail Adresse
|
||||
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail]
|
||||
|
||||
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert
|
||||
Das funktioniert allerdings nur, wenn du dich über unsere Website registriert
|
||||
hast.
|
||||
|
||||
Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
|
||||
PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach
|
||||
PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach
|
||||
ignorieren. ;)
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -220,9 +220,9 @@ footer {
|
||||
<h2>Hallo Jenny Rostock,</h2>
|
||||
<div class="wrapper">
|
||||
<div class="content"></div>
|
||||
<p>Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456">Bestätige Deine E-Mail Adresse</a>
|
||||
<p>Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456">Bestätige deine E-Mail Adresse</a>
|
||||
<p>Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.</p>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -240,14 +240,14 @@ footer {
|
||||
Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
|
||||
kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:
|
||||
|
||||
Bestätige Deine E-Mail Adresse
|
||||
Bestätige deine E-Mail Adresse
|
||||
[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456]
|
||||
|
||||
Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach
|
||||
ignorieren.
|
||||
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
|
||||
@ -16,20 +16,20 @@
|
||||
"wrongEmail": "Falsche Mailaddresse?"
|
||||
},
|
||||
"registration": {
|
||||
"introduction": "Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:",
|
||||
"codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ",
|
||||
"codeHintException": "Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.",
|
||||
"notYouStart": "Falls Du Dich nicht selbst bei ",
|
||||
"introduction": "Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:",
|
||||
"codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ",
|
||||
"codeHintException": "Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.",
|
||||
"notYouStart": "Falls du dich nicht selbst bei ",
|
||||
"notYouEnd": " angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.",
|
||||
"ps": "PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)"
|
||||
"ps": "PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)"
|
||||
},
|
||||
"emailVerification": {
|
||||
"codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ",
|
||||
"introduction": "Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:",
|
||||
"doNotChange": "Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. "
|
||||
"codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ",
|
||||
"introduction": "Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst du deine neue E-Mail Adresse bestätigen:",
|
||||
"doNotChange": "Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. "
|
||||
},
|
||||
"buttons": {
|
||||
"confirmEmail": "Bestätige Deine E-Mail Adresse",
|
||||
"confirmEmail": "Bestätige deine E-Mail Adresse",
|
||||
"resetPassword": "Passwort zurücksetzen",
|
||||
"tryAgain": "Versuch' es mit einer anderen E-Mail",
|
||||
"verifyEmail": "E-Mail Adresse bestätigen",
|
||||
@ -47,12 +47,12 @@
|
||||
"welcome": "Willkommen bei"
|
||||
},
|
||||
"resetPassword": {
|
||||
"codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: ",
|
||||
"codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ",
|
||||
"ignore": "Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.",
|
||||
"introduction": "Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:"
|
||||
},
|
||||
"wrongEmail": {
|
||||
"codeHint": "Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: ",
|
||||
"codeHint": "Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: ",
|
||||
"ignoreEnd": " hast oder dein Password gar nicht ändern willst, kannst du diese E-Mail einfach ignorieren!",
|
||||
"ignoreStart": "Wenn du noch keinen Account bei ",
|
||||
"introduction": "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?"
|
||||
@ -63,7 +63,7 @@
|
||||
"commentedOnPost": " hat einen Beitrag den du beobachtest mit dem Titel „{postTitle}“ kommentiert. Klicke auf den Knopf, um diesen Kommentar zu sehen:",
|
||||
"followedUserPosted": ", ein Nutzer dem du folgst, hat einen neuen Beitrag mit dem Titel „{postTitle}“ geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:",
|
||||
"mentionedInComment": " hat dich in einem Kommentar zu dem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Kommentar zu sehen:",
|
||||
"mentionedInPost": " hat Dich in einem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:",
|
||||
"mentionedInPost": " hat dich in einem Beitrag mit dem Titel „{postTitle}“ erwähnt. Klicke auf den Knopf, um den Beitrag zu sehen:",
|
||||
"postInGroup": "jemand hat einen neuen Beitrag mit dem Titel „{postTitle}“ in einer deiner Gruppen geschrieben. Klicke auf den Knopf, um diesen Beitrag zu sehen:",
|
||||
"removedUserFromGroup": "du wurdest aus der Gruppe „{groupName}“ entfernt.",
|
||||
"userJoinedGroup": " ist der Gruppe „{groupName}“ beigetreten. Klicke auf den Knopf, um diese Gruppe zu sehen:",
|
||||
|
||||
@ -11,7 +11,7 @@ import { createTransport } from 'nodemailer'
|
||||
// import type Email as EmailType from '@types/email-templates'
|
||||
|
||||
import CONFIG, { nodemailerTransportOptions } from '@config/index'
|
||||
import logosWebapp from '@config/logos'
|
||||
import logosWebapp from '@config/logosBranded'
|
||||
import metadata from '@config/metadata'
|
||||
import { UserDbProperties } from '@db/types/User'
|
||||
|
||||
@ -115,7 +115,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
commenterUrl:
|
||||
notification?.from?.__typename === 'Comment'
|
||||
? new URL(
|
||||
`/user/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
|
||||
`/profile/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
@ -131,7 +131,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
groupUrl:
|
||||
notification?.from?.__typename === 'Group'
|
||||
? new URL(
|
||||
`/group/${notification?.from?.id}/${notification?.from?.slug}`,
|
||||
`/groups/${notification?.from?.id}/${notification?.from?.slug}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
@ -142,7 +142,7 @@ export const sendNotificationMail = async (notification: any): Promise<OriginalM
|
||||
groupRelatedUserUrl:
|
||||
notification?.from?.__typename === 'Group'
|
||||
? new URL(
|
||||
`/user/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`,
|
||||
`/profile/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`,
|
||||
CONFIG.CLIENT_URI,
|
||||
)
|
||||
: undefined,
|
||||
@ -176,7 +176,7 @@ export const sendChatMessageMail = async (
|
||||
locale: recipientUser.locale,
|
||||
name: recipientUser.name,
|
||||
chattingUser: senderUser.name,
|
||||
chattingUserUrl: new URL(`/user/${senderUser.id}/${senderUser.slug}`, CONFIG.CLIENT_URI),
|
||||
chattingUserUrl: new URL(`/profile/${senderUser.id}/${senderUser.slug}`, CONFIG.CLIENT_URI),
|
||||
chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
|
||||
},
|
||||
})
|
||||
|
||||
40
backend/src/graphql/queries/Group.ts
Normal file
40
backend/src/graphql/queries/Group.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const Group = gql`
|
||||
query Group($isMember: Boolean, $id: ID, $slug: String) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
descriptionExcerpt
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
myRole
|
||||
inviteCodes {
|
||||
code
|
||||
redeemedByCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
12
backend/src/graphql/queries/GroupMembers.ts
Normal file
12
backend/src/graphql/queries/GroupMembers.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const GroupMembers = gql`
|
||||
query GroupMembers($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
15
backend/src/graphql/queries/currentUser.ts
Normal file
15
backend/src/graphql/queries/currentUser.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const currentUser = gql`
|
||||
query currentUser {
|
||||
currentUser {
|
||||
following {
|
||||
name
|
||||
}
|
||||
inviteCodes {
|
||||
code
|
||||
redeemedByCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
36
backend/src/graphql/queries/generateGroupInviteCode.ts
Normal file
36
backend/src/graphql/queries/generateGroupInviteCode.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const generateGroupInviteCode = gql`
|
||||
mutation generateGroupInviteCode($groupId: ID!, $expiresAt: String, $comment: String) {
|
||||
generateGroupInviteCode(groupId: $groupId, expiresAt: $expiresAt, comment: $comment) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
36
backend/src/graphql/queries/generatePersonalInviteCode.ts
Normal file
36
backend/src/graphql/queries/generatePersonalInviteCode.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const generatePersonalInviteCode = gql`
|
||||
mutation generatePersonalInviteCode($expiresAt: String, $comment: String) {
|
||||
generatePersonalInviteCode(expiresAt: $expiresAt, comment: $comment) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,14 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const groupMembersQuery = () => {
|
||||
return gql`
|
||||
query ($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const groupQuery = () => {
|
||||
return gql`
|
||||
query ($isMember: Boolean, $id: ID, $slug: String) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
descriptionExcerpt
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
36
backend/src/graphql/queries/invalidateInviteCode.ts
Normal file
36
backend/src/graphql/queries/invalidateInviteCode.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const invalidateInviteCode = gql`
|
||||
mutation invalidateInviteCode($code: String!) {
|
||||
invalidateInviteCode(code: $code) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
7
backend/src/graphql/queries/redeemInviteCode.ts
Normal file
7
backend/src/graphql/queries/redeemInviteCode.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const redeemInviteCode = gql`
|
||||
mutation redeemInviteCode($code: String!) {
|
||||
redeemInviteCode(code: $code)
|
||||
}
|
||||
`
|
||||
49
backend/src/graphql/queries/validateInviteCode.ts
Normal file
49
backend/src/graphql/queries/validateInviteCode.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const unauthenticatedValidateInviteCode = gql`
|
||||
query validateInviteCode($code: String!) {
|
||||
validateInviteCode(code: $code) {
|
||||
code
|
||||
invitedTo {
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
generatedBy {
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const authenticatedValidateInviteCode = gql`
|
||||
query validateInviteCode($code: String!) {
|
||||
validateInviteCode(code: $code) {
|
||||
code
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,41 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
const driver = getDriver()
|
||||
const instance = getNeode()
|
||||
let regularUser, administrator, moderator, badge, verification
|
||||
|
||||
let authenticatedUser, regularUser, administrator, moderator, badge, verification, query, mutate
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let query, mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
describe('Badges', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
regularUser = await Factory.build(
|
||||
'user',
|
||||
@ -83,7 +85,6 @@ describe('Badges', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -122,7 +123,7 @@ describe('Badges', () => {
|
||||
})
|
||||
|
||||
describe('authenticated as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = moderator.toJson()
|
||||
})
|
||||
|
||||
@ -322,7 +323,7 @@ describe('Badges', () => {
|
||||
})
|
||||
|
||||
describe('authenticated as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = moderator.toJson()
|
||||
})
|
||||
|
||||
|
||||
@ -10,8 +10,8 @@ import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
|
||||
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
|
||||
import { groupMembersQuery } from '@graphql/queries/groupMembersQuery'
|
||||
import { groupQuery } from '@graphql/queries/groupQuery'
|
||||
import { Group as groupQuery } from '@graphql/queries/Group'
|
||||
import { GroupMembers as groupMembersQuery } from '@graphql/queries/GroupMembers'
|
||||
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
|
||||
import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation'
|
||||
import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation'
|
||||
@ -423,7 +423,7 @@ describe('in mode', () => {
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupQuery(), variables: {} })
|
||||
const { errors } = await query({ query: groupQuery, variables: {} })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -541,7 +541,7 @@ describe('in mode', () => {
|
||||
describe('in general finds only listed groups – no hidden groups where user is none or pending member', () => {
|
||||
describe('without any filters', () => {
|
||||
it('finds all listed groups – including the set descriptionExcerpts and locations', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: {} })
|
||||
const result = await query({ query: groupQuery, variables: {} })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
@ -586,9 +586,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('has set categories', async () => {
|
||||
await expect(
|
||||
query({ query: groupQuery(), variables: {} }),
|
||||
).resolves.toMatchObject({
|
||||
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@ -622,7 +620,7 @@ describe('in mode', () => {
|
||||
describe('with given id', () => {
|
||||
describe("id = 'my-group'", () => {
|
||||
it('finds only the listed group with this id', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: { id: 'my-group' } })
|
||||
const result = await query({ query: groupQuery, variables: { id: 'my-group' } })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: [
|
||||
@ -642,7 +640,7 @@ describe('in mode', () => {
|
||||
describe("id = 'third-hidden-group'", () => {
|
||||
it("finds only the hidden group where I'm 'usual' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { id: 'third-hidden-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -664,7 +662,7 @@ describe('in mode', () => {
|
||||
describe("id = 'second-hidden-group'", () => {
|
||||
it("finds no hidden group where I'm 'pending' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { id: 'second-hidden-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -674,7 +672,7 @@ describe('in mode', () => {
|
||||
describe("id = 'hidden-group'", () => {
|
||||
it("finds no hidden group where I'm not(!) a member at all", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { id: 'hidden-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -686,7 +684,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'the-best-group'", () => {
|
||||
it('finds only the listed group with this slug', async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'the-best-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -708,7 +706,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'third-investigative-journalism-group'", () => {
|
||||
it("finds only the hidden group where I'm 'usual' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'third-investigative-journalism-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -730,7 +728,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'second-investigative-journalism-group'", () => {
|
||||
it("finds no hidden group where I'm 'pending' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'second-investigative-journalism-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -740,7 +738,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'investigative-journalism-group'", () => {
|
||||
it("finds no hidden group where I'm not(!) a member at all", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'investigative-journalism-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -750,7 +748,7 @@ describe('in mode', () => {
|
||||
|
||||
describe('isMember = true', () => {
|
||||
it('finds only listed groups where user is member', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: { isMember: true } })
|
||||
const result = await query({ query: groupQuery, variables: { isMember: true } })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
@ -774,7 +772,7 @@ describe('in mode', () => {
|
||||
|
||||
describe('isMember = false', () => {
|
||||
it('finds only listed groups where user is not(!) member', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: { isMember: false } })
|
||||
const result = await query({ query: groupQuery, variables: { isMember: false } })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
@ -1039,7 +1037,7 @@ describe('in mode', () => {
|
||||
variables = {
|
||||
id: 'not-existing-group',
|
||||
}
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1212,7 +1210,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1245,7 +1243,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1278,7 +1276,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1321,7 +1319,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1354,7 +1352,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1386,7 +1384,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1397,7 +1395,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1419,7 +1417,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1456,7 +1454,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1493,7 +1491,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1529,7 +1527,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1540,7 +1538,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -2418,7 +2416,7 @@ describe('in mode', () => {
|
||||
describe('here "closed-group" for example', () => {
|
||||
const memberInGroup = async (userId, groupId) => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables: {
|
||||
id: groupId,
|
||||
},
|
||||
|
||||
@ -436,6 +436,24 @@ export default {
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
inviteCodes: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Group!')
|
||||
}
|
||||
return (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})-[:GENERATED]->(inviteCodes:InviteCode)-[:INVITES_TO]->(g:Group {id: $parent.id})
|
||||
RETURN inviteCodes {.*}
|
||||
ORDER BY inviteCodes.createdAt ASC
|
||||
`,
|
||||
variables: {
|
||||
user: context.user,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
).records.map((r) => r.get('inviteCodes'))
|
||||
},
|
||||
...Resolver('Group', {
|
||||
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
|
||||
hasMany: {
|
||||
@ -451,6 +469,18 @@ export default {
|
||||
'MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )',
|
||||
},
|
||||
}),
|
||||
name: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!context.user) {
|
||||
return parent.groupType === 'hidden' ? '' : parent.name
|
||||
}
|
||||
return parent.name
|
||||
},
|
||||
about: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!context.user) {
|
||||
return parent.groupType === 'hidden' ? '' : parent.about
|
||||
}
|
||||
return parent.about
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import registrationConstants from '@constants/registrationBranded'
|
||||
|
||||
export default function generateInviteCode() {
|
||||
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
|
||||
return Array.from(
|
||||
{ length: registrationConstants.INVITE_CODE_LENGTH },
|
||||
(n: number = 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('')
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,136 +1,294 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import generateInviteCode from './helpers/generateInviteCode'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { validateInviteCode } from './transactions/inviteCodes'
|
||||
import CONFIG from '@config/index'
|
||||
import registrationConstants from '@constants/registrationBranded'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { Context } from '@src/server'
|
||||
|
||||
const uniqueInviteCode = async (session, code) => {
|
||||
return session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, {
|
||||
code,
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
export const generateInviteCode = () => {
|
||||
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
|
||||
return Array.from(
|
||||
{ length: registrationConstants.INVITE_CODE_LENGTH },
|
||||
(n: number = 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('')
|
||||
}
|
||||
|
||||
const uniqueInviteCode = async (context: Context, code: string) => {
|
||||
return (
|
||||
(
|
||||
await context.database.query({
|
||||
query: `MATCH (inviteCode:InviteCode { code: toUpper($code) })
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR inviteCode.expiresAt >= datetime()
|
||||
RETURN toString(count(inviteCode)) AS count`,
|
||||
variables: { code },
|
||||
})
|
||||
).records[0].get('count') === '0'
|
||||
)
|
||||
}
|
||||
|
||||
export const validateInviteCode = async (context: Context, inviteCode) => {
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
OPTIONAL MATCH (inviteCode:InviteCode { code: toUpper($inviteCode) })
|
||||
RETURN
|
||||
CASE
|
||||
WHEN inviteCode IS NULL THEN false
|
||||
WHEN inviteCode.expiresAt IS NULL THEN true
|
||||
WHEN datetime(inviteCode.expiresAt) >= datetime() THEN true
|
||||
ELSE false END AS result
|
||||
`,
|
||||
variables: { inviteCode },
|
||||
})
|
||||
return parseInt(String(result.records[0].get('count'))) === 0
|
||||
})
|
||||
).records
|
||||
return result[0].get('result') === true
|
||||
}
|
||||
|
||||
export const redeemInviteCode = async (context: Context, code, newUser = false) => {
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User)
|
||||
OPTIONAL MATCH (inviteCode)-[:INVITES_TO]->(group:Group)
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR datetime(inviteCode.expiresAt) >= datetime()
|
||||
RETURN inviteCode {.*}, group {.*}, host {.*}`,
|
||||
variables: { code },
|
||||
})
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inviteCode = result[0].get('inviteCode')
|
||||
const group = result[0].get('group')
|
||||
const host = result[0].get('host')
|
||||
|
||||
if (!inviteCode || !host) {
|
||||
return false
|
||||
}
|
||||
|
||||
// self
|
||||
if (host.id === context.user.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Personal Invite Link
|
||||
if (!group) {
|
||||
// We redeemed this link while having an account, hence we do nothing, but return true
|
||||
if (!newUser) {
|
||||
return true
|
||||
}
|
||||
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id}), (inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User)
|
||||
MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
|
||||
MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
|
||||
MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
|
||||
`,
|
||||
variables: { user: context.user, code },
|
||||
})
|
||||
// Group Invite Link
|
||||
} else {
|
||||
const role = ['closed', 'hidden'].includes(group.groupType as string) ? 'pending' : 'usual'
|
||||
|
||||
const optionalInvited = newUser
|
||||
? 'MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)'
|
||||
: ''
|
||||
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id}), (group:Group)<-[:INVITES_TO]-(inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User)
|
||||
MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
${optionalInvited}
|
||||
MERGE (user)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role = $role
|
||||
`,
|
||||
variables: { user: context.user, code, role },
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
getInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
|
||||
WHERE ic.expiresAt IS NULL
|
||||
OR datetime(ic.expiresAt) >= datetime()
|
||||
RETURN properties(ic) AS inviteCodes`,
|
||||
{
|
||||
userId,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCodes'))
|
||||
})
|
||||
try {
|
||||
const inviteCode = await readTxResultPromise
|
||||
if (inviteCode && inviteCode.length > 0) return inviteCode[0]
|
||||
let code = generateInviteCode()
|
||||
while (!(await uniqueInviteCode(session, code))) {
|
||||
code = generateInviteCode()
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})
|
||||
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
|
||||
ON CREATE SET
|
||||
ic.createdAt = toString(datetime()),
|
||||
ic.expiresAt = $expiresAt
|
||||
RETURN ic AS inviteCode`,
|
||||
{
|
||||
userId,
|
||||
code,
|
||||
expiresAt: null,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCode').properties)
|
||||
validateInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode { code: toUpper($args.code) })
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR datetime(inviteCode.expiresAt) >= datetime()
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { args },
|
||||
})
|
||||
const txResult = await writeTxResultPromise
|
||||
return txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
MyInviteCodes: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
|
||||
RETURN properties(ic) AS inviteCodes`,
|
||||
{
|
||||
userId,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCodes'))
|
||||
})
|
||||
try {
|
||||
const txResult = await readTxResultPromise
|
||||
return txResult
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
isValidInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const { code } = args
|
||||
const session = context.driver.session()
|
||||
if (!code) return false
|
||||
return validateInviteCode(session, code)
|
||||
|
||||
return result[0].get('inviteCode')
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
GenerateInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
generatePersonalInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const userInviteCodeAmount = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode)<-[:GENERATED]-(user:User {id: $user.id})
|
||||
WHERE NOT (inviteCode)-[:INVITES_TO]->(:Group)
|
||||
AND (inviteCode.expiresAt IS NULL OR inviteCode.expiresAt >= datetime())
|
||||
RETURN toString(count(inviteCode)) as count
|
||||
`,
|
||||
variables: { user: context.user },
|
||||
})
|
||||
).records[0].get('count')
|
||||
|
||||
if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_PERSONAL_PER_USER) {
|
||||
throw new Error('You have reached the maximum of Invite Codes you can generate')
|
||||
}
|
||||
|
||||
let code = generateInviteCode()
|
||||
while (!(await uniqueInviteCode(session, code))) {
|
||||
while (!(await uniqueInviteCode(context, code))) {
|
||||
code = generateInviteCode()
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})
|
||||
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
|
||||
ON CREATE SET
|
||||
ic.createdAt = toString(datetime()),
|
||||
ic.expiresAt = $expiresAt
|
||||
RETURN ic AS inviteCode`,
|
||||
{
|
||||
userId,
|
||||
code,
|
||||
expiresAt: args.expiresAt,
|
||||
},
|
||||
|
||||
return (
|
||||
await context.database.write({
|
||||
// We delete a potential old invite code if there is a collision on an expired code
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})
|
||||
OPTIONAL MATCH (oldInviteCode:InviteCode { code: toUpper($code) })
|
||||
DETACH DELETE oldInviteCode
|
||||
MERGE (user)-[:GENERATED]->(inviteCode:InviteCode { code: toUpper($code)})
|
||||
ON CREATE SET
|
||||
inviteCode.createdAt = toString(datetime()),
|
||||
inviteCode.expiresAt = $args.expiresAt,
|
||||
inviteCode.comment = $args.comment
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { user: context.user, code, args },
|
||||
})
|
||||
).records[0].get('inviteCode')
|
||||
},
|
||||
generateGroupInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const userInviteCodeAmount = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (:Group {id: $args.groupId})<-[:INVITES_TO]-(inviteCode:InviteCode)<-[:GENERATED]-(user:User {id: $user.id})
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR inviteCode.expiresAt >= datetime()
|
||||
RETURN toString(count(inviteCode)) as count
|
||||
`,
|
||||
variables: { user: context.user, args },
|
||||
})
|
||||
).records[0].get('count')
|
||||
|
||||
if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_GROUP_PER_USER) {
|
||||
throw new Error(
|
||||
'You have reached the maximum of Invite Codes you can generate for this group',
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCode').properties)
|
||||
})
|
||||
try {
|
||||
const txResult = await writeTxResultPromise
|
||||
return txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
let code = generateInviteCode()
|
||||
while (!(await uniqueInviteCode(context, code))) {
|
||||
code = generateInviteCode()
|
||||
}
|
||||
|
||||
const inviteCode = (
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH
|
||||
(user:User {id: $user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId})
|
||||
WHERE NOT membership.role = 'pending'
|
||||
OPTIONAL MATCH (oldInviteCode:InviteCode { code: toUpper($code) })
|
||||
DETACH DELETE oldInviteCode
|
||||
MERGE (user)-[:GENERATED]->(inviteCode:InviteCode { code: toUpper($code) })-[:INVITES_TO]->(group)
|
||||
ON CREATE SET
|
||||
inviteCode.createdAt = toString(datetime()),
|
||||
inviteCode.expiresAt = $args.expiresAt,
|
||||
inviteCode.comment = $args.comment
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { user: context.user, code, args },
|
||||
})
|
||||
).records
|
||||
|
||||
if (inviteCode.length !== 1) {
|
||||
// Not a member
|
||||
throw new Error('Not Authorized!')
|
||||
}
|
||||
|
||||
return inviteCode[0].get('inviteCode')
|
||||
},
|
||||
invalidateInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const result = (
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})-[:GENERATED]-(inviteCode:InviteCode {code: toUpper($args.code)})
|
||||
SET inviteCode.expiresAt = toString(datetime())
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { args, user: context.user },
|
||||
})
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
// Link not generated by this user or does not exist
|
||||
throw new Error('Not Authorized!')
|
||||
}
|
||||
|
||||
return result[0].get('inviteCode')
|
||||
},
|
||||
redeemInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
return redeemInviteCode(context, args.code)
|
||||
},
|
||||
},
|
||||
InviteCode: {
|
||||
invitedTo: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.code) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode {code: $parent.code})-[:INVITES_TO]->(group:Group)
|
||||
RETURN group {.*}
|
||||
`,
|
||||
variables: { parent },
|
||||
})
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
return null
|
||||
}
|
||||
return result[0].get('group')
|
||||
},
|
||||
isValid: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.code) {
|
||||
return false
|
||||
}
|
||||
return validateInviteCode(context, parent.code)
|
||||
},
|
||||
...Resolver('InviteCode', {
|
||||
idAttribute: 'code',
|
||||
undefinedToNull: ['expiresAt'],
|
||||
undefinedToNull: ['expiresAt', 'comment'],
|
||||
count: {
|
||||
redeemedByCount: '<-[:REDEEMED]-(related:User)',
|
||||
},
|
||||
hasOne: {
|
||||
generatedBy: '<-[:GENERATED]-(related:User)',
|
||||
},
|
||||
|
||||
@ -24,6 +24,9 @@ export default {
|
||||
],
|
||||
}),
|
||||
distanceToMe: async (parent, _params, context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Location!')
|
||||
}
|
||||
const session = context.driver.session()
|
||||
|
||||
const query = session.readTransaction(async (transaction) => {
|
||||
|
||||
@ -1,55 +1,51 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import Image from '@db/models/Image'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { createPostMutation } from '@graphql/queries/createPostMutation'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
CONFIG.CATEGORIES_ACTIVE = true
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
let query
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let user
|
||||
|
||||
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||
let variables
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let query, mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
mutate = createTestClientResult.mutate
|
||||
query = createTestClientResult.query
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||
let variables
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {}
|
||||
user = await Factory.build(
|
||||
@ -64,22 +60,22 @@ beforeEach(async () => {
|
||||
},
|
||||
)
|
||||
await Promise.all([
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat27',
|
||||
name: 'Animal Protection',
|
||||
icon: 'paw',
|
||||
@ -88,7 +84,6 @@ beforeEach(async () => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -233,7 +228,6 @@ describe('Post', () => {
|
||||
Post(filter: $filter) {
|
||||
id
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
@ -249,7 +243,7 @@ describe('Post', () => {
|
||||
Post: [
|
||||
{
|
||||
id: 'post-by-followed-user',
|
||||
author: { id: 'followed-by-me', name: 'Followed User' },
|
||||
author: { name: 'Followed User' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -976,11 +970,11 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
it('updates the image', async () => {
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -990,9 +984,9 @@ describe('UpdatePost', () => {
|
||||
variables = { ...variables, image: null }
|
||||
})
|
||||
it('deletes the image', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(6)
|
||||
await expect(database.neode.all('Image')).resolves.toHaveLength(6)
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(5)
|
||||
await expect(database.neode.all('Image')).resolves.toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1002,11 +996,11 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
it('keeps the image unchanged', async () => {
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
})
|
||||
})
|
||||
@ -1253,18 +1247,18 @@ describe('pin posts', () => {
|
||||
|
||||
it('removes previous `pinned` attribute', async () => {
|
||||
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
|
||||
pinnedPost = await neode.cypher(cypher, {})
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(cypher, {})
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes previous PINNED relationship', async () => {
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(
|
||||
pinnedPost = await database.neode.cypher(
|
||||
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
|
||||
{},
|
||||
)
|
||||
@ -1593,7 +1587,7 @@ describe('emotions', () => {
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
author = await neode.create('User', { id: 'u257' })
|
||||
author = await database.neode.create('User', { id: 'u257' })
|
||||
postToEmote = await Factory.build(
|
||||
'post',
|
||||
{
|
||||
@ -1628,7 +1622,7 @@ describe('emotions', () => {
|
||||
`
|
||||
let postsEmotionsQueryVariables
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
postsEmotionsQueryVariables = { id: 'p1376' }
|
||||
})
|
||||
|
||||
|
||||
@ -1,49 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import EmailAddress from '@db/models/EmailAddress'
|
||||
import User from '@db/models/User'
|
||||
import { getDriver, getNeode } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let variables
|
||||
const driver = getDriver()
|
||||
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {}
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -98,7 +97,7 @@ describe('Signup', () => {
|
||||
describe('creates a EmailAddress node', () => {
|
||||
it('with `createdAt` attribute', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
const emailAddress = await neode.first<typeof EmailAddress>(
|
||||
const emailAddress = await database.neode.first<typeof EmailAddress>(
|
||||
'EmailAddress',
|
||||
{ email: 'someuser@example.org' },
|
||||
undefined,
|
||||
@ -112,7 +111,7 @@ describe('Signup', () => {
|
||||
|
||||
it('with a cryptographic `nonce`', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
const emailAddress = await neode.first<typeof EmailAddress>(
|
||||
const emailAddress = await database.neode.first<typeof EmailAddress>(
|
||||
'EmailAddress',
|
||||
{ email: 'someuser@example.org' },
|
||||
undefined,
|
||||
@ -153,12 +152,12 @@ describe('Signup', () => {
|
||||
|
||||
it('creates no additional `EmailAddress` node', async () => {
|
||||
// admin account and the already existing user
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
data: { Signup: { email: 'someuser@example.org' } },
|
||||
errors: undefined,
|
||||
})
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -194,7 +193,7 @@ describe('SignupVerification', () => {
|
||||
}
|
||||
`
|
||||
describe('given valid password and email', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
nonce: '12345',
|
||||
@ -207,7 +206,7 @@ describe('SignupVerification', () => {
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
@ -215,8 +214,8 @@ describe('SignupVerification', () => {
|
||||
beforeEach(async () => {
|
||||
const { email, nonce } = variables
|
||||
const [emailAddress, user] = await Promise.all([
|
||||
neode.model('EmailAddress').create({ email, nonce }),
|
||||
neode
|
||||
database.neode.model('EmailAddress').create({ email, nonce }),
|
||||
database.neode
|
||||
.model('User')
|
||||
.create({ name: 'Somebody', password: '1234', email: 'john@example.org' }),
|
||||
])
|
||||
@ -242,7 +241,7 @@ describe('SignupVerification', () => {
|
||||
email: 'john@example.org',
|
||||
nonce: '12345',
|
||||
}
|
||||
await neode.model('EmailAddress').create(args)
|
||||
await database.neode.model('EmailAddress').create(args)
|
||||
})
|
||||
|
||||
describe('sending a valid nonce', () => {
|
||||
@ -258,7 +257,7 @@ describe('SignupVerification', () => {
|
||||
|
||||
it('sets `verifiedAt` attribute of EmailAddress', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
const email = await neode.first(
|
||||
const email = await database.neode.first(
|
||||
'EmailAddress',
|
||||
{ email: 'john@example.org' },
|
||||
undefined,
|
||||
@ -276,14 +275,18 @@ describe('SignupVerification', () => {
|
||||
RETURN email
|
||||
`
|
||||
await mutate({ mutation, variables })
|
||||
const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' })
|
||||
const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('sets `about` attribute of User', async () => {
|
||||
variables = { ...variables, about: 'Find this description in the user profile' }
|
||||
await mutate({ mutation, variables })
|
||||
const user = await neode.first<typeof User>('User', { name: 'John Doe' }, undefined)
|
||||
const user = await database.neode.first<typeof User>(
|
||||
'User',
|
||||
{ name: 'John Doe' },
|
||||
undefined,
|
||||
)
|
||||
await expect(user.toJson()).resolves.toMatchObject({
|
||||
about: 'Find this description in the user profile',
|
||||
})
|
||||
@ -306,7 +309,7 @@ describe('SignupVerification', () => {
|
||||
RETURN email
|
||||
`
|
||||
await mutate({ mutation, variables })
|
||||
const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' })
|
||||
const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
|
||||
@ -7,10 +7,12 @@ import { UserInputError } from 'apollo-server'
|
||||
import { hash } from 'bcryptjs'
|
||||
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
import existingEmailAddress from './helpers/existingEmailAddress'
|
||||
import generateNonce from './helpers/generateNonce'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import { redeemInviteCode } from './inviteCodes'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -33,7 +35,7 @@ export default {
|
||||
throw new UserInputError(e.message)
|
||||
}
|
||||
},
|
||||
SignupVerification: async (_parent, args, context) => {
|
||||
SignupVerification: async (_parent, args, context: Context) => {
|
||||
const { termsAndConditionsAgreedVersion } = args
|
||||
const regEx = /^[0-9]+\.[0-9]+\.[0-9]+$/g
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
@ -52,69 +54,60 @@ export default {
|
||||
const { driver } = context
|
||||
const session = driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const createUserTransactionResponse = await transaction.run(signupCypher(inviteCode), {
|
||||
args,
|
||||
nonce,
|
||||
email,
|
||||
inviteCode,
|
||||
})
|
||||
const createUserTransactionResponse = await transaction.run(
|
||||
`
|
||||
MATCH (email:EmailAddress {nonce: $nonce, email: $email})
|
||||
WHERE NOT (email)-[:BELONGS_TO]->()
|
||||
CREATE (user:User)
|
||||
MERGE (user)-[:PRIMARY_EMAIL]->(email)
|
||||
MERGE (user)<-[:BELONGS_TO]-(email)
|
||||
SET user += $args
|
||||
SET user.id = randomUUID()
|
||||
SET user.role = 'user'
|
||||
SET user.createdAt = toString(datetime())
|
||||
SET user.updatedAt = toString(datetime())
|
||||
SET user.allowEmbedIframes = false
|
||||
SET user.showShoutsPublicly = false
|
||||
SET email.verifiedAt = toString(datetime())
|
||||
WITH user
|
||||
OPTIONAL MATCH (post:Post)-[:IN]->(group:Group)
|
||||
WHERE NOT group.groupType = 'public'
|
||||
WITH user, collect(post) AS invisiblePosts
|
||||
FOREACH (invisiblePost IN invisiblePosts |
|
||||
MERGE (user)-[:CANNOT_SEE]->(invisiblePost)
|
||||
)
|
||||
RETURN user {.*}
|
||||
`,
|
||||
{
|
||||
args,
|
||||
nonce,
|
||||
email,
|
||||
inviteCode,
|
||||
},
|
||||
)
|
||||
const [user] = createUserTransactionResponse.records.map((record) => record.get('user'))
|
||||
if (!user) throw new UserInputError('Invalid email or nonce')
|
||||
|
||||
return user
|
||||
})
|
||||
try {
|
||||
const user = await writeTxResultPromise
|
||||
|
||||
// To allow redeeming and return an User object we require a User in the context
|
||||
context.user = user
|
||||
|
||||
if (inviteCode) {
|
||||
await redeemInviteCode(context, inviteCode, true)
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (e) {
|
||||
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||
throw new UserInputError('User with this slug already exists!')
|
||||
throw new UserInputError(e.message)
|
||||
} finally {
|
||||
session.close()
|
||||
await session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const signupCypher = (inviteCode) => {
|
||||
let optionalMatch = ''
|
||||
let optionalMerge = ''
|
||||
if (inviteCode) {
|
||||
optionalMatch = `
|
||||
OPTIONAL MATCH
|
||||
(inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User)
|
||||
`
|
||||
optionalMerge = `
|
||||
MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
|
||||
MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
|
||||
MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
|
||||
`
|
||||
}
|
||||
const cypher = `
|
||||
MATCH (email:EmailAddress {nonce: $nonce, email: $email})
|
||||
WHERE NOT (email)-[:BELONGS_TO]->()
|
||||
${optionalMatch}
|
||||
CREATE (user:User)
|
||||
MERGE (user)-[:PRIMARY_EMAIL]->(email)
|
||||
MERGE (user)<-[:BELONGS_TO]-(email)
|
||||
${optionalMerge}
|
||||
SET user += $args
|
||||
SET user.id = randomUUID()
|
||||
SET user.role = 'user'
|
||||
SET user.createdAt = toString(datetime())
|
||||
SET user.updatedAt = toString(datetime())
|
||||
SET user.allowEmbedIframes = false
|
||||
SET user.showShoutsPublicly = false
|
||||
SET email.verifiedAt = toString(datetime())
|
||||
WITH user
|
||||
OPTIONAL MATCH (post:Post)-[:IN]->(group:Group)
|
||||
WHERE NOT group.groupType = 'public'
|
||||
WITH user, collect(post) AS invisiblePosts
|
||||
FOREACH (invisiblePost IN invisiblePosts |
|
||||
MERGE (user)-[:CANNOT_SEE]->(invisiblePost)
|
||||
)
|
||||
RETURN user {.*}
|
||||
`
|
||||
return cypher
|
||||
}
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
export async function validateInviteCode(session, inviteCode) {
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (ic:InviteCode { code: toUpper($inviteCode) })
|
||||
RETURN
|
||||
CASE
|
||||
WHEN ic.expiresAt IS NULL THEN true
|
||||
WHEN datetime(ic.expiresAt) >= datetime() THEN true
|
||||
ELSE false END AS result`,
|
||||
{
|
||||
inviteCode,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('result'))
|
||||
})
|
||||
try {
|
||||
const txResult = await readTxResultPromise
|
||||
return !!txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
|
||||
import Resolver from './helpers/Resolver'
|
||||
@ -467,6 +468,19 @@ export default {
|
||||
},
|
||||
},
|
||||
User: {
|
||||
inviteCodes: async (_parent, _args, context: Context, _resolveInfo) => {
|
||||
return (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})-[:GENERATED]->(inviteCodes:InviteCode)
|
||||
WHERE NOT (inviteCodes)-[:INVITES_TO]->(:Group)
|
||||
RETURN inviteCodes {.*}
|
||||
ORDER BY inviteCodes.createdAt ASC
|
||||
`,
|
||||
variables: { user: context.user },
|
||||
})
|
||||
).records.map((record) => record.get('inviteCodes'))
|
||||
},
|
||||
emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => {
|
||||
return [
|
||||
{
|
||||
@ -668,7 +682,6 @@ export default {
|
||||
shouted: '-[:SHOUTED]->(related:Post)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
badgeTrophies: '<-[:REWARDED]-(related:Badge)',
|
||||
inviteCodes: '-[:GENERATED]->(related:InviteCode)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@ -43,6 +43,9 @@ type Group {
|
||||
posts: [Post] @relation(name: "IN", direction: "IN")
|
||||
|
||||
isMutedByMe: Boolean! @cypher(statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )")
|
||||
|
||||
"inviteCodes to this group the current user has generated"
|
||||
inviteCodes: [InviteCode]! @neo4j_ignore
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -3,16 +3,23 @@ type InviteCode {
|
||||
createdAt: String!
|
||||
generatedBy: User @relation(name: "GENERATED", direction: "IN")
|
||||
redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN")
|
||||
redeemedByCount: Int! @cypher(statement: "MATCH (this)<-[:REDEEMED]-(related:User)")
|
||||
expiresAt: String
|
||||
}
|
||||
comment: String
|
||||
|
||||
invitedTo: Group @neo4j_ignore
|
||||
# invitedFrom: User! @neo4j_ignore # -> see generatedBy
|
||||
|
||||
type Mutation {
|
||||
GenerateInviteCode(expiresAt: String = null): InviteCode
|
||||
isValid: Boolean! @neo4j_ignore
|
||||
}
|
||||
|
||||
type Query {
|
||||
MyInviteCodes: [InviteCode]
|
||||
isValidInviteCode(code: ID!): Boolean
|
||||
getInviteCode: InviteCode
|
||||
validateInviteCode(code: String!): InviteCode
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
generatePersonalInviteCode(expiresAt: String = null, comment: String = null): InviteCode!
|
||||
generateGroupInviteCode(groupId: ID!, expiresAt: String = null, comment: String = null): InviteCode!
|
||||
invalidateInviteCode(code: String!): InviteCode
|
||||
redeemInviteCode(code: String!): Boolean!
|
||||
}
|
||||
|
||||
@ -72,9 +72,6 @@ type User {
|
||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT")
|
||||
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||
|
||||
# Is the currently logged in user following that user?
|
||||
followedByCurrentUser: Boolean! @cypher(
|
||||
statement: """
|
||||
@ -125,6 +122,7 @@ type User {
|
||||
|
||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
# Badges
|
||||
badgeVerification: Badge! @neo4j_ignore
|
||||
badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||
badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||
@ -132,6 +130,11 @@ type User {
|
||||
badgeTrophiesUnused: [Badge]! @neo4j_ignore
|
||||
badgeTrophiesUnusedCount: Int! @neo4j_ignore
|
||||
|
||||
"personal inviteCodes the user has generated"
|
||||
inviteCodes: [InviteCode]! @neo4j_ignore
|
||||
# inviteCodes: [InviteCode]! @relation(name: "GENERATED", direction: "OUT")
|
||||
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||
|
||||
emotions: [EMOTED]
|
||||
|
||||
activeCategories: [String] @cypher(
|
||||
|
||||
@ -15,6 +15,7 @@ import languages from './languages/languages'
|
||||
import login from './login/loginMiddleware'
|
||||
import notifications from './notifications/notificationsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import permissions from './permissionsMiddleware'
|
||||
import sentry from './sentryMiddleware'
|
||||
import sluggify from './sluggifyMiddleware'
|
||||
|
||||
@ -1,42 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getDriver, getNeode } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
let variables
|
||||
let owner, anotherRegularUser, administrator, moderator
|
||||
|
||||
let query, mutate, variables
|
||||
let authenticatedUser, owner, anotherRegularUser, administrator, moderator
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let query, mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => ({
|
||||
driver,
|
||||
instance,
|
||||
user: authenticatedUser,
|
||||
}),
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -109,7 +112,7 @@ describe('authorization', () => {
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { User: [null] },
|
||||
data: { User: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -242,7 +245,7 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('as anyone', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
@ -267,7 +270,7 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('as anyone with valid invite code', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
email: 'some@email.org',
|
||||
inviteCode: 'ABCDEF',
|
||||
@ -287,7 +290,7 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('as anyone without valid invite', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
email: 'some@email.org',
|
||||
inviteCode: 'no valid invite code',
|
||||
|
||||
@ -9,7 +9,9 @@ import { rule, shield, deny, allow, or, and } from 'graphql-shield'
|
||||
import CONFIG from '@config/index'
|
||||
import SocialMedia from '@db/models/SocialMedia'
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { validateInviteCode } from '@graphql/resolvers/transactions/inviteCodes'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { validateInviteCode } from '@graphql/resolvers/inviteCodes'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
const debug = !!CONFIG.DEBUG
|
||||
const allowExternalErrors = true
|
||||
@ -370,11 +372,28 @@ const noEmailFilter = rule({
|
||||
|
||||
const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
|
||||
|
||||
const inviteRegistration = rule()(async (_parent, args, { _user, driver }) => {
|
||||
const inviteRegistration = rule()(async (_parent, args, context: Context) => {
|
||||
if (!CONFIG.INVITE_REGISTRATION) return false
|
||||
const { inviteCode } = args
|
||||
const session = driver.session()
|
||||
return validateInviteCode(session, inviteCode)
|
||||
return validateInviteCode(context, inviteCode)
|
||||
})
|
||||
|
||||
const isAllowedToGenerateGroupInviteCode = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, context: Context) => {
|
||||
if (!context.user) return false
|
||||
|
||||
return !!(
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (user:User{id: user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId})
|
||||
WHERE (group.type IN ['closed','hidden'] AND membership.role IN ['admin', 'owner'])
|
||||
OR (NOT group.type IN ['closed','hidden'] AND NOT membership.role = 'pending')
|
||||
RETURN count(group) as count
|
||||
`,
|
||||
variables: { user: context.user, args },
|
||||
})
|
||||
).records[0].get('count')
|
||||
})
|
||||
|
||||
// Permissions
|
||||
@ -399,7 +418,7 @@ export default shield(
|
||||
Post: allow,
|
||||
profilePagePosts: allow,
|
||||
Comment: allow,
|
||||
User: or(noEmailFilter, isAdmin),
|
||||
User: and(isAuthenticated, or(noEmailFilter, isAdmin)),
|
||||
Badge: allow,
|
||||
PostsEmotionsCountByEmotion: allow,
|
||||
PostsEmotionsByCurrentUser: isAuthenticated,
|
||||
@ -408,15 +427,15 @@ export default shield(
|
||||
notifications: isAuthenticated,
|
||||
Donations: isAuthenticated,
|
||||
userData: isAuthenticated,
|
||||
MyInviteCodes: isAuthenticated,
|
||||
isValidInviteCode: allow,
|
||||
VerifyNonce: allow,
|
||||
queryLocations: isAuthenticated,
|
||||
availableRoles: isAdmin,
|
||||
getInviteCode: isAuthenticated, // and inviteRegistration
|
||||
Room: isAuthenticated,
|
||||
Message: isAuthenticated,
|
||||
UnreadRooms: isAuthenticated,
|
||||
|
||||
// Invite Code
|
||||
validateInviteCode: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
@ -465,7 +484,13 @@ export default shield(
|
||||
pinPost: isAdmin,
|
||||
unpinPost: isAdmin,
|
||||
UpdateDonations: isAdmin,
|
||||
GenerateInviteCode: isAuthenticated,
|
||||
|
||||
// InviteCode
|
||||
generatePersonalInviteCode: isAuthenticated,
|
||||
generateGroupInviteCode: isAllowedToGenerateGroupInviteCode,
|
||||
invalidateInviteCode: isAuthenticated,
|
||||
redeemInviteCode: isAuthenticated,
|
||||
|
||||
switchUserRole: isAdmin,
|
||||
markTeaserAsViewed: allow,
|
||||
saveCategorySettings: isAuthenticated,
|
||||
@ -480,8 +505,27 @@ export default shield(
|
||||
resetTrophyBadgesSelected: isAuthenticated,
|
||||
},
|
||||
User: {
|
||||
'*': isAuthenticated,
|
||||
name: allow,
|
||||
avatar: allow,
|
||||
email: or(isMyOwn, isAdmin),
|
||||
emailNotificationSettings: isMyOwn,
|
||||
inviteCodes: isMyOwn,
|
||||
},
|
||||
Group: {
|
||||
'*': isAuthenticated, // TODO - only those who are allowed to see the group
|
||||
avatar: allow,
|
||||
name: allow,
|
||||
about: allow,
|
||||
groupType: allow,
|
||||
},
|
||||
InviteCode: {
|
||||
'*': allow,
|
||||
redeemedBy: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
redeemedByCount: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
createdAt: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
expiresAt: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
comment: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
},
|
||||
Location: {
|
||||
distanceToMe: isAuthenticated,
|
||||
|
||||
@ -2,47 +2,46 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
|
||||
import { createPostMutation } from '@graphql/queries/createPostMutation'
|
||||
import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation'
|
||||
import { updateGroupMutation } from '@graphql/queries/updateGroupMutation'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
let authenticatedUser
|
||||
let variables
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
const descriptionAdditional100 =
|
||||
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
const database = databaseContext()
|
||||
|
||||
const { mutate } = createTestClient(server)
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@ -19,6 +19,7 @@ import pubsubContext from '@context/pubsub'
|
||||
import CONFIG from './config'
|
||||
import schema from './graphql/schema'
|
||||
import decode from './jwt/decode'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import middleware from './middleware'
|
||||
|
||||
const serverDatabase = databaseContext()
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
:loading-rooms="loadingRooms"
|
||||
show-files="false"
|
||||
show-audio="false"
|
||||
:height="'calc(100dvh - 190px)'"
|
||||
:styles="JSON.stringify(computedChatStyle)"
|
||||
:show-footer="true"
|
||||
:responsive-breakpoint="responsiveBreakpoint"
|
||||
|
||||
@ -136,12 +136,12 @@
|
||||
<ds-flex-item class="mobile-hamburger-menu">
|
||||
<client-only>
|
||||
<!-- chat menu -->
|
||||
<div style="display: inline-flex">
|
||||
<div>
|
||||
<chat-notification-menu />
|
||||
</div>
|
||||
<!-- notification menu -->
|
||||
<div style="display: inline-flex; padding-right: clamp(10px, 2.5vw, 20px)">
|
||||
<notification-menu />
|
||||
<div>
|
||||
<notification-menu no-menu />
|
||||
</div>
|
||||
</client-only>
|
||||
<!-- hamburger menu -->
|
||||
@ -284,7 +284,7 @@ import { mapGetters } from 'vuex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import { SHOW_GROUP_BUTTON_IN_HEADER } from '~/constants/groups.js'
|
||||
import { SHOW_CONTENT_FILTER_HEADER_MENU } from '~/constants/filter.js'
|
||||
import LOGOS from '~/constants/logos.js'
|
||||
import LOGOS from '~/constants/logosBranded.js'
|
||||
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
||||
import ChatNotificationMenu from '~/components/ChatNotificationMenu/ChatNotificationMenu'
|
||||
import CustomButton from '~/components/CustomButton/CustomButton'
|
||||
@ -409,6 +409,25 @@ export default {
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
& > div {
|
||||
display: inline-flex;
|
||||
|
||||
padding-right: 15px;
|
||||
&:first-child {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
.svg {
|
||||
height: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hamburger-button .svg {
|
||||
height: 1.5em;
|
||||
}
|
||||
}
|
||||
.mobile-menu {
|
||||
margin: 0 20px;
|
||||
|
||||
@ -1,29 +1,30 @@
|
||||
<template>
|
||||
<component :is="tag" class="ds-logo" :class="[inverse && 'ds-logo-inverse']">
|
||||
<!-- Desktop logo -->
|
||||
<img
|
||||
v-if="!inverse"
|
||||
class="ds-logo-svg"
|
||||
class="ds-logo-svg ds-logo-desktop"
|
||||
:alt="metadata.APPLICATION_NAME + ' ' + logo.alt"
|
||||
:src="logo.path"
|
||||
:style="logoWidthStyle"
|
||||
/>
|
||||
|
||||
<!-- Mobile logo (falls back to desktop if not provided) -->
|
||||
<img
|
||||
v-else
|
||||
class="ds-logo-svg"
|
||||
:alt="metadata.APPLICATION_NAME + ' ' + logo.alt"
|
||||
:src="logo.path"
|
||||
:style="logoWidthStyle"
|
||||
class="ds-logo-svg ds-logo-mobile"
|
||||
:alt="metadata.APPLICATION_NAME + ' ' + logo.alt + ' Mobile'"
|
||||
:src="logo.mobilePath || logo.path"
|
||||
:style="mobileLogoWidthStyle"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logos from '~/constants/logos.js'
|
||||
import logos from '~/constants/logosBranded.js'
|
||||
import metadata from '~/constants/metadata.js'
|
||||
|
||||
/**
|
||||
* This component displays the brand's logo.
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
*/
|
||||
export default {
|
||||
name: 'Logo',
|
||||
@ -42,6 +43,13 @@ export default {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* Mobile logo width
|
||||
*/
|
||||
mobileLogoWidth: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* Inverse the logo
|
||||
*/
|
||||
@ -61,8 +69,10 @@ export default {
|
||||
const logosObject = {
|
||||
header: {
|
||||
path: logos.LOGO_HEADER_PATH,
|
||||
mobilePath: logos.LOGO_HEADER_MOBILE_PATH || null,
|
||||
alt: 'Header',
|
||||
widthDefault: logos.LOGO_HEADER_WIDTH,
|
||||
mobileWidthDefault: logos.LOGO_HEADER_MOBILE_WIDTH || logos.LOGO_HEADER_WIDTH,
|
||||
},
|
||||
welcome: { path: logos.LOGO_WELCOME_PATH, alt: 'Welcome', widthDefault: '200px' },
|
||||
signup: { path: logos.LOGO_SIGNUP_PATH, alt: 'Sign Up', widthDefault: '200px' },
|
||||
@ -85,12 +95,12 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
logoWidthStyle() {
|
||||
let width = ''
|
||||
if (this.logoWidth === null) {
|
||||
width = this.logo.widthDefault
|
||||
} else {
|
||||
width = this.logoWidth
|
||||
}
|
||||
const width = this.logoWidth === null ? this.logo.widthDefault : this.logoWidth
|
||||
return `width: ${width};`
|
||||
},
|
||||
mobileLogoWidthStyle() {
|
||||
const width =
|
||||
this.mobileLogoWidth === null ? this.logo.mobileWidthDefault : this.mobileLogoWidth
|
||||
return `width: ${width};`
|
||||
},
|
||||
},
|
||||
@ -115,6 +125,25 @@ export default {
|
||||
fill: #000000;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Show desktop logo by default and hide mobile logo */
|
||||
.ds-logo-desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ds-logo-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.ds-logo-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ds-logo-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<docs src="./demo.md"></docs>
|
||||
|
||||
@ -14,6 +14,18 @@
|
||||
}"
|
||||
/>
|
||||
</nuxt-link>
|
||||
<nuxt-link v-else-if="noMenu" class="notifications-menu" :to="{ name: 'notifications' }">
|
||||
<base-button
|
||||
ghost
|
||||
circle
|
||||
v-tooltip="{
|
||||
content: $t('header.notifications.tooltip'),
|
||||
placement: 'bottom-start',
|
||||
}"
|
||||
>
|
||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||
</base-button>
|
||||
</nuxt-link>
|
||||
<dropdown v-else class="notifications-menu" offset="8" :placement="placement">
|
||||
<template #default="{ toggleMenu }">
|
||||
<base-button
|
||||
@ -29,9 +41,6 @@
|
||||
</base-button>
|
||||
</template>
|
||||
<template #popover="{ closeMenu }">
|
||||
<div class="notifications-menu-popover">
|
||||
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
|
||||
</div>
|
||||
<ds-flex class="notifications-link-container">
|
||||
<ds-flex-item class="notifications-link-container-item" :width="{ base: '100%' }" centered>
|
||||
<nuxt-link :to="{ name: 'notifications' }">
|
||||
@ -51,6 +60,9 @@
|
||||
</base-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<div class="notifications-menu-popover">
|
||||
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
</template>
|
||||
@ -82,6 +94,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
placement: { type: String },
|
||||
noMenu: { type: Boolean, default: false },
|
||||
},
|
||||
methods: {
|
||||
async markAsRead(notificationSourceId) {
|
||||
|
||||
@ -32,3 +32,16 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@media only screen and (max-width: 500px) {
|
||||
.ds-container {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
|
||||
.base-card {
|
||||
padding: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { storiesOf } from '@storybook/vue'
|
||||
import helpers from '~/storybook/helpers'
|
||||
import logos from '~/constants/logos.js'
|
||||
import logos from '~/constants/logosBranded.js'
|
||||
import BaseCard from './BaseCard.vue'
|
||||
|
||||
storiesOf('Generic/BaseCard', module)
|
||||
|
||||
31
webapp/constants/logosBranded.js
Normal file
31
webapp/constants/logosBranded.js
Normal file
@ -0,0 +1,31 @@
|
||||
// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding
|
||||
// this are the paths in the webapp
|
||||
import { merge } from 'lodash'
|
||||
import logos from '~/constants/logos'
|
||||
|
||||
const defaultLogos = {
|
||||
LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
|
||||
LOGO_HEADER_MOBILE_PATH: '/img/custom/logo-horizontal.svg',
|
||||
LOGO_HEADER_WIDTH: '130px',
|
||||
LOGO_HEADER_MOBILE_WIDTH: '100px',
|
||||
LOGO_HEADER_CLICK: {
|
||||
// externalLink: {
|
||||
// url: 'https://ocelot.social',
|
||||
// target: '_blank',
|
||||
// },
|
||||
externalLink: null,
|
||||
internalPath: {
|
||||
to: {
|
||||
name: 'index',
|
||||
},
|
||||
scrollTo: '.main-navigation',
|
||||
},
|
||||
},
|
||||
LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg',
|
||||
LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg',
|
||||
}
|
||||
|
||||
export default merge(defaultLogos, logos)
|
||||
@ -23,7 +23,7 @@
|
||||
</ds-container>
|
||||
</div>
|
||||
<ds-container>
|
||||
<div style="padding: 5rem 2rem">
|
||||
<div class="content">
|
||||
<nuxt />
|
||||
</div>
|
||||
</ds-container>
|
||||
@ -61,7 +61,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.main-navigation-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -69,4 +69,14 @@ export default {
|
||||
.main-navigation-right .desktop-view {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.layout-blank .content {
|
||||
padding: 5rem 2rem;
|
||||
}
|
||||
@media only screen and (max-width: 500px) {
|
||||
.layout-blank .content {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -65,7 +65,7 @@
|
||||
"tagCountUnique": "Nutzer"
|
||||
},
|
||||
"invites": {
|
||||
"description": "Einladungen sind eine wunderbare Möglichkeit, Deine Freunde in Deinem Netzwerk zu haben …",
|
||||
"description": "Einladungen sind eine wunderbare Möglichkeit, deine Freunde in deinem Netzwerk zu haben …",
|
||||
"name": "Nutzer einladen",
|
||||
"title": "Leute einladen"
|
||||
},
|
||||
@ -109,7 +109,7 @@
|
||||
"isOnline": "online",
|
||||
"isTyping": "tippt...",
|
||||
"lastSeen": "zuletzt gesehen ",
|
||||
"messageDeleted": "Diese Nachricht wuerde gelöscht",
|
||||
"messageDeleted": "Diese Nachricht wurde gelöscht",
|
||||
"messagesEmpty": "Keine Nachrichten",
|
||||
"newMessages": "Neue Nachrichten",
|
||||
"page": {
|
||||
@ -222,7 +222,7 @@
|
||||
"buttonTitle": "Weiter",
|
||||
"form": {
|
||||
"click-next": "Click auf Weiter.",
|
||||
"description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
|
||||
"description": "Öffne dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
|
||||
"next": "Weiter",
|
||||
"nonce": "E-Mail-Code: 32143",
|
||||
"validations": {
|
||||
@ -252,12 +252,12 @@
|
||||
"signup": {
|
||||
"form": {
|
||||
"data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.",
|
||||
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
|
||||
"description": "Um loszulegen, kannst du dich hier kostenfrei registrieren:",
|
||||
"errors": {
|
||||
"email-exists": "Es gibt schon ein Nutzerkonto mit dieser E-Mail-Adresse!"
|
||||
},
|
||||
"submit": "Konto erstellen",
|
||||
"success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an <b>{email}</b> geschickt",
|
||||
"success": "Eine E-Mail mit einem Link zum Abschließen deiner Registrierung wurde an <b>{email}</b> geschickt",
|
||||
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\" target=\"_blank\"><ds-text bold color=\"primary\">Nutzungsbedingungen</ds-text></a> zu."
|
||||
},
|
||||
"title": "Mach mit bei {APPLICATION_NAME}!",
|
||||
@ -269,7 +269,7 @@
|
||||
"amount-clicks": "{amount} Klicks",
|
||||
"amount-comments": "{amount} Kommentare",
|
||||
"amount-shouts": "{amount} Empfehlungen",
|
||||
"amount-views": "{amount} Aurufe",
|
||||
"amount-views": "{amount} Aufrufe",
|
||||
"categories": {
|
||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Themen ausgewählt"
|
||||
},
|
||||
@ -335,9 +335,9 @@
|
||||
},
|
||||
"filterMyGroups": "Meine Gruppen",
|
||||
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
|
||||
"languageSelectLabel": "Sprache Deines Beitrags",
|
||||
"languageSelectLabel": "Sprache deines Beitrags",
|
||||
"languageSelectText": "Sprache wählen",
|
||||
"newEvent": "Erstelle einen neue Veranstaltung",
|
||||
"newEvent": "Erstelle eine neue Veranstaltung",
|
||||
"newPost": "Erstelle einen neuen Beitrag",
|
||||
"success": "Gespeichert!",
|
||||
"teaserImage": {
|
||||
@ -355,13 +355,13 @@
|
||||
"delete": {
|
||||
"cancel": "Abbrechen",
|
||||
"comment": {
|
||||
"message": "Bist Du sicher, dass Du den Kommentar „<b>{name}</b>“ löschen möchtest?",
|
||||
"message": "Bist du sicher, dass du den Kommentar „<b>{name}</b>“ löschen möchtest?",
|
||||
"success": "Kommentar erfolgreich gelöscht!",
|
||||
"title": "Lösche Kommentar",
|
||||
"type": "Kommentar"
|
||||
},
|
||||
"contribution": {
|
||||
"message": "Bist Du sicher, dass Du den Beitrag „<b>{name}</b>“ löschen möchtest?",
|
||||
"message": "Bist du sicher, dass du den Beitrag „<b>{name}</b>“ löschen möchtest?",
|
||||
"success": "Beitrag erfolgreich gelöscht!",
|
||||
"title": "Lösche Beitrag",
|
||||
"type": "Beitrag"
|
||||
@ -371,19 +371,19 @@
|
||||
"disable": {
|
||||
"cancel": "Abbrechen",
|
||||
"comment": {
|
||||
"message": "Bist Du sicher, dass Du den Kommentar „<b>{name}</b>“ deaktivieren möchtest?",
|
||||
"message": "Bist du sicher, dass du den Kommentar „<b>{name}</b>“ deaktivieren möchtest?",
|
||||
"title": "Kommentar sperren",
|
||||
"type": "Kommentar"
|
||||
},
|
||||
"contribution": {
|
||||
"message": "Bist Du sicher, dass Du den Beitrag von „<b>{name}</b>“ deaktivieren möchtest?",
|
||||
"message": "Bist du sicher, dass du den Beitrag von „<b>{name}</b>“ deaktivieren möchtest?",
|
||||
"title": "Beitrag sperren",
|
||||
"type": "Beitrag"
|
||||
},
|
||||
"submit": "Deaktivieren",
|
||||
"success": "Erfolgreich deaktiviert",
|
||||
"user": {
|
||||
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ sperren möchtest?",
|
||||
"message": "Bist du sicher, dass du den Nutzer „<b>{name}</b>“ sperren möchtest?",
|
||||
"title": "Nutzer sperren",
|
||||
"type": "Nutzer"
|
||||
}
|
||||
@ -395,8 +395,8 @@
|
||||
"editor": {
|
||||
"embed": {
|
||||
"always_allow": "Einzubettende Inhalte von Drittanbietern immer erlauben (diese Einstellung ist jederzeit änderbar)",
|
||||
"data_privacy_info": "Deine Daten wurden noch nicht an Drittanbieter weitergegeben. Wenn Du diesen Inhalt jetzt abspielst, registriert der folgende Anbieter wahrscheinlich Deine Nutzerdaten:",
|
||||
"data_privacy_warning": "Achte auf Deine Daten!",
|
||||
"data_privacy_info": "Deine Daten wurden noch nicht an Drittanbieter weitergegeben. Wenn du diesen Inhalt jetzt abspielst, registriert der folgende Anbieter wahrscheinlich deine Nutzerdaten:",
|
||||
"data_privacy_warning": "Achte auf deine Daten!",
|
||||
"play_now": "Jetzt ansehen"
|
||||
},
|
||||
"hashtag": {
|
||||
@ -656,7 +656,7 @@
|
||||
},
|
||||
"maintenance": {
|
||||
"explanation": "Derzeit führen wir einige geplante Wartungsarbeiten durch, bitte versuche es später erneut.",
|
||||
"questions": "Bei Fragen oder Problemen erreichst Du uns per E-Mail an",
|
||||
"questions": "Bei Fragen oder Problemen erreichst du uns per E-Mail an",
|
||||
"title": "{APPLICATION_NAME} befindet sich in der Wartung"
|
||||
},
|
||||
"map": {
|
||||
@ -697,32 +697,32 @@
|
||||
"cancel": "Abbruch",
|
||||
"Comment": {
|
||||
"disable": {
|
||||
"message": "Möchtest Du den Kommentar „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"message": "Möchtest du den Kommentar „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"title": "Sperre den Kommentar abschließend"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Möchtest Du den Kommentar „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"message": "Möchtest du den Kommentar „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"title": "Entsperre den Kommentar abschließend"
|
||||
}
|
||||
},
|
||||
"Post": {
|
||||
"disable": {
|
||||
"message": "Möchtest Du den Beitrag „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"message": "Möchtest du den Beitrag „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"title": "Sperre den Beitrag abschließend"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Möchtest Du den Beitrag „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"message": "Möchtest du den Beitrag „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"title": "Entsperre den Beitrag abschließend"
|
||||
}
|
||||
},
|
||||
"submit": "Bestätige Entscheidung",
|
||||
"User": {
|
||||
"disable": {
|
||||
"message": "Möchtest Du den Nutzer „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"message": "Möchtest du den Nutzer „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||
"title": "Sperre den Nutzer abschließend"
|
||||
},
|
||||
"enable": {
|
||||
"message": "Möchtest Du den Nutzer „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"message": "Möchtest du den Nutzer „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||
"title": "Entsperre den Nutzer abschließend"
|
||||
}
|
||||
}
|
||||
@ -757,7 +757,7 @@
|
||||
"notifications": {
|
||||
"comment": "Kommentar",
|
||||
"content": "Inhalt oder Beschreibung",
|
||||
"empty": "Bedaure, Du hast momentan keinerlei Benachrichtigungen.",
|
||||
"empty": "Bedaure, du hast momentan keinerlei Benachrichtigungen.",
|
||||
"filterLabel": {
|
||||
"all": "Alle",
|
||||
"read": "Gelesen",
|
||||
@ -768,14 +768,14 @@
|
||||
"pageLink": "Alle Benachrichtigungen",
|
||||
"post": "Beitrag oder Gruppe",
|
||||
"reason": {
|
||||
"changed_group_member_role": "Hat Deine Rolle in der Gruppe geändert …",
|
||||
"changed_group_member_role": "Hat deine Rolle in der Gruppe geändert …",
|
||||
"commented_on_post": "Hat einen Beitrag den du beobachtest kommentiert …",
|
||||
"followed_user_posted": "Hat einen neuen Betrag geschrieben …",
|
||||
"mentioned_in_comment": "Hat Dich in einem Kommentar erwähnt …",
|
||||
"mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …",
|
||||
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
||||
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
|
||||
"post_in_group": "Hat einen Beitrag in der Gruppe geschrieben …",
|
||||
"removed_user_from_group": "Hat Dich aus der Gruppe entfernt …",
|
||||
"user_joined_group": "Ist Deiner Gruppe beigetreten …",
|
||||
"removed_user_from_group": "Hat dich aus der Gruppe entfernt …",
|
||||
"user_joined_group": "Ist deiner Gruppe beigetreten …",
|
||||
"user_left_group": "Hat deine Gruppe verlassen …"
|
||||
},
|
||||
"title": "Benachrichtigungen",
|
||||
@ -879,22 +879,22 @@
|
||||
"release": {
|
||||
"cancel": "Abbrechen",
|
||||
"comment": {
|
||||
"error": "Den Kommentar hast Du schon gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Kommentar „<b>{name}</b>“ freigeben möchtest?",
|
||||
"error": "Den Kommentar hast du schon gemeldet!",
|
||||
"message": "Bist du sicher, dass du den Kommentar „<b>{name}</b>“ freigeben möchtest?",
|
||||
"title": "Kommentar freigeben",
|
||||
"type": "Kommentar"
|
||||
},
|
||||
"contribution": {
|
||||
"error": "Den Beitrag hast Du schon gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Beitrag „<b>{name}</b>“ freigeben möchtest?",
|
||||
"error": "Den Beitrag hast du schon gemeldet!",
|
||||
"message": "Bist du sicher, dass du den Beitrag „<b>{name}</b>“ freigeben möchtest?",
|
||||
"title": "Beitrag freigeben",
|
||||
"type": "Beitrag"
|
||||
},
|
||||
"submit": "freigeben",
|
||||
"success": "Erfolgreich freigegeben!",
|
||||
"user": {
|
||||
"error": "Den Nutzer hast Du schon gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ freigeben möchtest?",
|
||||
"error": "Den Nutzer hast du schon gemeldet!",
|
||||
"message": "Bist du sicher, dass du den Nutzer „<b>{name}</b>“ freigeben möchtest?",
|
||||
"title": "Nutzer freigeben",
|
||||
"type": "Nutzer"
|
||||
}
|
||||
@ -903,13 +903,13 @@
|
||||
"cancel": "Abbrechen",
|
||||
"comment": {
|
||||
"error": "Du hast den Kommentar bereits gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Kommentar von „<b>{name}</b>“ melden möchtest?",
|
||||
"message": "Bist du sicher, dass du den Kommentar von „<b>{name}</b>“ melden möchtest?",
|
||||
"title": "Kommentar melden",
|
||||
"type": "Kommentar"
|
||||
},
|
||||
"contribution": {
|
||||
"error": "Du hast den Beitrag bereits gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Beitrag „<b>{name}</b>“ melden möchtest?",
|
||||
"message": "Bist du sicher, dass du den Beitrag „<b>{name}</b>“ melden möchtest?",
|
||||
"title": "Beitrag melden",
|
||||
"type": "Beitrag"
|
||||
},
|
||||
@ -930,7 +930,7 @@
|
||||
"placeholder": "Thema …"
|
||||
},
|
||||
"description": {
|
||||
"label": "Bitte erkläre: Warum möchtest Du dies melden?",
|
||||
"label": "Bitte erkläre: Warum möchtest du dies melden?",
|
||||
"placeholder": "Zusätzliche Information …"
|
||||
}
|
||||
},
|
||||
@ -938,7 +938,7 @@
|
||||
"success": "Vielen Dank für diese Meldung!",
|
||||
"user": {
|
||||
"error": "Du hast den Nutzer bereits gemeldet!",
|
||||
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ melden möchtest?",
|
||||
"message": "Bist du sicher, dass du den Nutzer „<b>{name}</b>“ melden möchtest?",
|
||||
"title": "Nutzer melden",
|
||||
"type": "Nutzer"
|
||||
}
|
||||
@ -977,15 +977,15 @@
|
||||
"slug": "Alias",
|
||||
"unblock": "Entsperren"
|
||||
},
|
||||
"empty": "Bislang hast Du niemanden blockiert.",
|
||||
"empty": "Bislang hast du niemanden blockiert.",
|
||||
"explanation": {
|
||||
"closing": "Das sollte fürs Erste genügen, damit blockierte Nutzer Dich nicht mehr länger belästigen können.",
|
||||
"closing": "Das sollte fürs Erste genügen, damit blockierte Nutzer dich nicht mehr länger belästigen können.",
|
||||
"commenting-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.",
|
||||
"commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere ",
|
||||
"intro": "Wenn ein anderer Nutzer durch Dich blockiert wurde, dann passiert Folgendes:",
|
||||
"notifications": "Von Dir blockierte Nutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.",
|
||||
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen Deine Beiträge nicht mehr zu sehen.",
|
||||
"your-perspective": "In Deinen Benachrichtigungen tauchen keine Beiträge der blockierten Person mehr auf."
|
||||
"intro": "Wenn ein anderer Nutzer durch dich blockiert wurde, dann passiert Folgendes:",
|
||||
"notifications": "Von Dir blockierte Nutzer werden keine Benachrichtigungen mehr erhalten, falls sie in deinen Beiträgen erwähnt werden.",
|
||||
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen deine Beiträge nicht mehr zu sehen.",
|
||||
"your-perspective": "In deinen Benachrichtigungen tauchen keine Beiträge der blockierten Person mehr auf."
|
||||
},
|
||||
"how-to": "Du kannst andere Nutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
|
||||
"name": "Blockierte Nutzer",
|
||||
@ -993,7 +993,7 @@
|
||||
"unblocked": "{name} ist wieder entsperrt"
|
||||
},
|
||||
"data": {
|
||||
"labelBio": "Über Dich",
|
||||
"labelBio": "Über dich",
|
||||
"labelCity": "Deine Stadt oder Region",
|
||||
"labelCityHint": "(zeigt ungefähre Position auf der Landkarte)",
|
||||
"labelName": "Dein Name",
|
||||
@ -1026,17 +1026,17 @@
|
||||
"labelNewEmail": "Neue E-Mail-Adresse",
|
||||
"labelNonce": "Bestätigungscode eingeben",
|
||||
"name": "Deine E-Mail",
|
||||
"submitted": "Eine E-Mail zur Bestätigung Deiner Adresse wurde an <b>{email}</b> gesendet.",
|
||||
"submitted": "Eine E-Mail zur Bestätigung deiner Adresse wurde an <b>{email}</b> gesendet.",
|
||||
"success": "Eine neue E-Mail-Adresse wurde registriert.",
|
||||
"validation": {
|
||||
"same-email": "Das ist Deine aktuelle E-Mail-Adresse"
|
||||
"same-email": "Das ist deine aktuelle E-Mail-Adresse"
|
||||
},
|
||||
"verification-error": {
|
||||
"explanation": "Das kann verschiedene Ursachen haben:",
|
||||
"message": "Deine E-Mail-Adresse konnte nicht verifiziert werden.",
|
||||
"reason": {
|
||||
"invalid-nonce": "Ist der Bestätigungscode falsch?",
|
||||
"no-email-request": "Bist Du Dir sicher, dass Du eine Änderung Deiner E-Mail-Adresse angefragt hattest?"
|
||||
"no-email-request": "Bist du Dir sicher, dass du eine Änderung deiner E-Mail-Adresse angefragt hattest?"
|
||||
},
|
||||
"support": "Wenn das Problem weiterhin besteht, kontaktiere uns gerne per E-Mail an"
|
||||
}
|
||||
@ -1114,12 +1114,12 @@
|
||||
"change-password": {
|
||||
"button": "Passwort ändern",
|
||||
"label-new-password": "Dein neues Passwort",
|
||||
"label-new-password-confirm": "Bestätige Dein neues Passwort",
|
||||
"label-new-password-confirm": "Bestätige dein neues Passwort",
|
||||
"label-old-password": "Dein altes Passwort",
|
||||
"message-new-password-confirm-required": "Bestätige Dein neues Passwort",
|
||||
"message-new-password-confirm-required": "Bestätige dein neues Passwort",
|
||||
"message-new-password-missmatch": "Gib dasselbe Passwort nochmals ein",
|
||||
"message-new-password-required": "Gib ein neues Passwort ein",
|
||||
"message-old-password-required": "Gib Dein altes Passwort ein",
|
||||
"message-old-password-required": "Gib dein altes Passwort ein",
|
||||
"passwordSecurity": "Passwortsicherheit",
|
||||
"passwordStrength0": "Sehr unsicheres Passwort",
|
||||
"passwordStrength1": "Unsicheres Passwort",
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<ds-heading tag="h1">{{ $t('chat.page.headline') }}</ds-heading>
|
||||
<add-chat-room-by-user-search
|
||||
v-if="showUserSearch"
|
||||
@add-chat-room="addChatRoom"
|
||||
@close-user-search="showUserSearch = false"
|
||||
/>
|
||||
<ds-space margin-bottom="small" />
|
||||
<chat
|
||||
:roomId="getShowChat.showChat ? getShowChat.roomID : null"
|
||||
ref="chat"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user