Merge branch 'master' into chat-message-notification-e2e-tests

This commit is contained in:
mahula 2025-05-09 00:36:36 +02:00 committed by GitHub
commit 7c33de9cc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 2346 additions and 2774 deletions

View File

@ -4,8 +4,17 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [3.5.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.2...3.5.3)
- fix(backend): correct email from [`#8501`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8501)
- refactor(backend): types for global config [`#8485`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8485)
- fix warning in workflow for lower case as [`#8494`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8494)
#### [3.5.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.1...3.5.2)
> 6 May 2025
- v3.5.2 [`#8498`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8498)
- fix emails2 [`#8497`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8497)
#### [3.5.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.5.0...3.5.1)
@ -1429,15 +1438,31 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- updated CHANGELOG.md [`9d9075f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d9075f2117b2eb4b607e7d59ab18c7e655c6ea7)
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...0.6.4)
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.4...0.6.4)
> 8 February 2021
- regenerated `CHANGELOG.md` [`ee688ec`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ee688ece24cf592b3989e83340701ca8772e876e)
- fetch full history [`5ecee4d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5ecee4d73a92d2e5c5ae971d79848ed27f65a72c)
- don't fail if tag exists (release) [`39c82fc`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/39c82fcb37d5c8e7e78a79288e1ef6280f8d0892)
- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
- - first steps towards docker image deployment & github autotagging [`5503216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5503216ad4a0230ac533042e4a69806590fc2a5a)
- - deploy structure image [`a60400b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a60400b4fe6f59bbb80e1073db4def3ba205e1a7)
#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...0.6.3)
#### [v0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...v0.6.4)
> 9 February 2021
- chore(release): 0.6.4 [`8b7570d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8b7570dc35d0ea431f673a711ac051f1e1320acb)
- change user roles is working, test fails [`8c3310a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8c3310abaf87c0e5597fec4f93fb37d27122c9e7)
- change user role: tests are working [`f10da4b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f10da4b09388fe1e2b85abd53f6ffc67c785d4c1)
#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.3...0.6.3)
> 8 February 2021
- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
- - fixed changelog [`cf70b12`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/cf70b12ed74011924ea788ab932fc9d7ac0e6bd9)
- - yarn install to allow yarn auto-changelog [`fc496aa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc496aa04cb7e804da4335da0cb5cda26f874ea2)
#### [v0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...v0.6.3)
> 8 February 2021

View File

@ -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

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "3.5.2",
"version": "3.5.3",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -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",

View File

@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable n/no-process-env */
import { config } from 'dotenv'
// eslint-disable-next-line import/no-namespace
import * as SMTPTransport from 'nodemailer/lib/smtp-pool'
import emails from './emails'
import metadata from './metadata'
@ -13,16 +15,17 @@ config()
// Use Cypress env or process.env
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare let Cypress: any | undefined
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env
const env = (typeof Cypress !== 'undefined' ? Cypress.env() : process.env) as typeof process.env
const environment = {
NODE_ENV: env.NODE_ENV || process.env.NODE_ENV,
NODE_ENV: env.NODE_ENV ?? process.env.NODE_ENV,
DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
TEST: env.NODE_ENV === 'test',
PRODUCTION: env.NODE_ENV === 'production',
// used for staging enviroments if 'PRODUCTION=true' and 'PRODUCTION_DB_CLEAN_ALLOW=true'
PRODUCTION_DB_CLEAN_ALLOW: env.PRODUCTION_DB_CLEAN_ALLOW === 'true' || false, // default = false
DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV as string)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV!)
? (env.DISABLED_MIDDLEWARES?.split(',') ?? [])
: [],
SEND_MAIL: env.NODE_ENV !== 'test',
@ -35,32 +38,51 @@ const required = {
}
const server = {
CLIENT_URI: env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: env.GRAPHQL_URI || 'http://localhost:4000',
JWT_EXPIRES: env.JWT_EXPIRES || '2y',
CLIENT_URI: env.CLIENT_URI ?? 'http://localhost:3000',
GRAPHQL_URI: env.GRAPHQL_URI ?? 'http://localhost:4000',
JWT_EXPIRES: env.JWT_EXPIRES ?? '2y',
}
const hasDKIMData = env.SMTP_DKIM_DOMAINNAME && env.SMTP_DKIM_KEYSELECTOR && env.SMTP_DKIM_PRIVATKEY
const SMTP_HOST = env.SMTP_HOST
const SMTP_PORT = (env.SMTP_PORT && parseInt(env.SMTP_PORT)) || undefined
const SMTP_IGNORE_TLS = env.SMTP_IGNORE_TLS !== 'false' // default = true
const SMTP_SECURE = env.SMTP_SECURE === 'true'
const SMTP_USERNAME = env.SMTP_USERNAME
const SMTP_PASSWORD = env.SMTP_PASSWORD
const SMTP_DKIM_DOMAINNAME = env.SMTP_DKIM_DOMAINNAME
const SMTP_DKIM_KEYSELECTOR = env.SMTP_DKIM_KEYSELECTOR
// PEM format = https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html
const SMTP_DKIM_PRIVATKEY = env.SMTP_DKIM_PRIVATKEY?.replace(/\\n/g, '\n') // replace all "\n" in .env string by real line break
const SMTP_MAX_CONNECTIONS = (env.SMTP_MAX_CONNECTIONS && parseInt(env.SMTP_MAX_CONNECTIONS)) || 5
const SMTP_MAX_MESSAGES = (env.SMTP_MAX_MESSAGES && parseInt(env.SMTP_MAX_MESSAGES)) || 100
const smtp = {
SMTP_HOST: env.SMTP_HOST,
SMTP_PORT: env.SMTP_PORT,
SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS !== 'false', // default = true
SMTP_SECURE: env.SMTP_SECURE === 'true',
SMTP_USERNAME: env.SMTP_USERNAME,
SMTP_PASSWORD: env.SMTP_PASSWORD,
SMTP_DKIM_DOMAINNAME: hasDKIMData && env.SMTP_DKIM_DOMAINNAME,
SMTP_DKIM_KEYSELECTOR: hasDKIMData && env.SMTP_DKIM_KEYSELECTOR,
// PEM format: https://docs.progress.com/bundle/datadirect-hybrid-data-pipeline-installation-46/page/PEM-file-format.html
SMTP_DKIM_PRIVATKEY: hasDKIMData && env.SMTP_DKIM_PRIVATKEY.replace(/\\n/g, '\n'), // replace all "\n" in .env string by real line break
SMTP_MAX_CONNECTIONS: env.SMTP_MAX_CONNECTIONS || 5,
SMTP_MAX_MESSAGES: env.SMTP_MAX_MESSAGES || 100,
const nodemailerTransportOptions: SMTPTransport.Options = {
host: SMTP_HOST,
port: SMTP_PORT,
ignoreTLS: SMTP_IGNORE_TLS,
secure: SMTP_SECURE, // true for 465, false for other ports
pool: true,
maxConnections: SMTP_MAX_CONNECTIONS,
maxMessages: SMTP_MAX_MESSAGES,
}
if (SMTP_USERNAME && SMTP_PASSWORD) {
nodemailerTransportOptions.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD,
}
}
if (SMTP_DKIM_DOMAINNAME && SMTP_DKIM_KEYSELECTOR && SMTP_DKIM_PRIVATKEY) {
nodemailerTransportOptions.dkim = {
domainName: SMTP_DKIM_DOMAINNAME,
keySelector: SMTP_DKIM_KEYSELECTOR,
privateKey: SMTP_DKIM_PRIVATKEY,
}
}
const neo4j = {
NEO4J_URI: env.NEO4J_URI || 'bolt://localhost:7687',
NEO4J_USERNAME: env.NEO4J_USERNAME || 'neo4j',
NEO4J_PASSWORD: env.NEO4J_PASSWORD || 'neo4j',
NEO4J_URI: env.NEO4J_URI ?? 'bolt://localhost:7687',
NEO4J_USERNAME: env.NEO4J_USERNAME ?? 'neo4j',
NEO4J_PASSWORD: env.NEO4J_PASSWORD ?? 'neo4j',
}
const sentry = {
@ -70,7 +92,7 @@ const sentry = {
const redis = {
REDIS_DOMAIN: env.REDIS_DOMAIN,
REDIS_PORT: env.REDIS_PORT,
REDIS_PORT: (env.REDIS_PORT && parseInt(env.REDIS_PORT)) || undefined,
REDIS_PASSWORD: env.REDIS_PASSWORD,
}
@ -95,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,
}
@ -110,10 +136,11 @@ export default {
...environment,
...server,
...required,
...smtp,
...neo4j,
...sentry,
...redis,
...s3,
...options,
}
export { nodemailerTransportOptions }

View File

@ -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: '',
},
]

View File

@ -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),
}
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { RedisPubSub } from 'graphql-redis-subscriptions'
import { PubSub } from 'graphql-subscriptions'
import Redis from 'ioredis'
@ -6,14 +5,15 @@ import Redis from 'ioredis'
import CONFIG from '@config/index'
export default () => {
if (!CONFIG.REDIS_DOMAIN || CONFIG.REDIS_PORT || CONFIG.REDIS_PASSWORD) {
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
if (!(REDIS_DOMAIN && REDIS_PORT && REDIS_PASSWORD)) {
return new PubSub()
}
const options = {
host: CONFIG.REDIS_DOMAIN,
port: CONFIG.REDIS_PORT,
password: CONFIG.REDIS_PASSWORD,
host: REDIS_DOMAIN,
port: REDIS_PORT,
password: REDIS_PASSWORD,
retryStrategy: (times) => {
return Math.min(times * 50, 2000)
},

View File

@ -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 () {

View File

@ -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
})

View File

@ -1,117 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable security/detect-non-literal-fs-filename */
import https from 'https'
import { existsSync, createReadStream } from 'node:fs'
import path from 'node:path'
import { S3 } from 'aws-sdk'
import mime from 'mime-types'
import s3Configs from '@config/index'
import { getDriver } from '@db/neo4j'
export const description = `
Upload all image files to a S3 compatible object storage in order to reduce
load on our backend.
`
export async function up(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
const agent = new https.Agent({
maxSockets: 5,
})
const {
AWS_ENDPOINT: endpoint,
AWS_REGION: region,
AWS_BUCKET: Bucket,
S3_CONFIGURED,
} = s3Configs
if (!S3_CONFIGURED) {
// eslint-disable-next-line no-console
console.log('No S3 given, cannot upload image files')
return
}
const s3 = new S3({ region, endpoint, httpOptions: { agent } })
try {
// Implement your migration here.
const { records } = await transaction.run('MATCH (image:Image) RETURN image.url as url')
let urls = records.map((r) => r.get('url'))
urls = urls.filter((url) => url.startsWith('/uploads'))
const locations = await Promise.all(
urls
.map((url) => {
return async () => {
const { pathname } = new URL(url, 'http://example.org')
const fileLocation = path.join(__dirname, `../../../public/${pathname}`)
const s3Location = `original${pathname}`
// eslint-disable-next-line n/no-sync
if (existsSync(fileLocation)) {
const mimeType = mime.lookup(fileLocation)
const params = {
Bucket,
Key: s3Location,
ACL: 'public-read',
ContentType: mimeType || 'image/jpeg',
Body: createReadStream(fileLocation),
}
const data = await s3.upload(params).promise()
const { Location: spacesUrl } = data
const updatedRecord = await transaction.run(
'MATCH (image:Image {url: $url}) SET image.url = $spacesUrl RETURN image.url as url',
{ url, spacesUrl },
)
const [updatedUrl] = updatedRecord.records.map((record) => record.get('url'))
return updatedUrl
}
}
})
.map((p) => p()),
)
// eslint-disable-next-line no-console
console.log('this is locations', locations)
await transaction.commit()
next()
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
throw new Error(error)
} finally {
await session.close()
}
}
export async function down(next) {
const driver = getDriver()
const session = driver.session()
const transaction = session.beginTransaction()
try {
// Implement your migration here.
await transaction.run(``)
await transaction.commit()
next()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
await transaction.rollback()
// eslint-disable-next-line no-console
console.log('rolled back')
} finally {
await session.close()
}
}

View File

@ -14,4 +14,10 @@ export default {
target: 'User',
direction: 'in',
},
invitesTo: {
type: 'relationship',
relationship: 'INVITES_TO',
target: 'Group',
direction: 'out',
},
}

View File

@ -1,6 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable import/no-named-as-default-member */
import neo4j, { Driver } from 'neo4j-driver'
import Neode from 'neode'

View File

@ -3,7 +3,7 @@
exports[`sendChatMessageMail English chat_message template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -130,7 +130,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendChatMessageMail German chat_message template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>

View File

@ -3,7 +3,7 @@
exports[`sendEmailVerification English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -133,7 +133,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendEmailVerification German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -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&amp;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&amp;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]!

View File

@ -3,7 +3,7 @@
exports[`sendNotificationMail English changed_group_member_role template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -129,7 +129,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English commented_on_post template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -257,7 +257,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English followed_user_posted template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -385,7 +385,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English mentioned_in_comment template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -513,7 +513,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English mentioned_in_post template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -640,7 +640,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English post_in_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -766,7 +766,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English removed_user_from_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -889,7 +889,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English user_joined_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -1016,7 +1016,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail English user_left_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -1143,7 +1143,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German changed_group_member_role template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -1269,7 +1269,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German commented_on_post template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -1397,7 +1397,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German followed_user_posted template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -1525,7 +1525,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German mentioned_in_comment template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -1653,7 +1653,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German mentioned_in_post template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -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]
@ -1780,7 +1780,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German post_in_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -1906,7 +1906,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German removed_user_from_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -2029,7 +2029,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German user_joined_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -2156,7 +2156,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendNotificationMail German user_left_group template 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>

View File

@ -3,7 +3,7 @@
exports[`sendRegistrationMail with invite code English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -142,7 +142,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendRegistrationMail with invite code German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -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&amp;nonce=123456&amp;inviteCode=welcome&amp;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&amp;nonce=123456&amp;inviteCode=welcome&amp;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]!
@ -282,7 +282,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendRegistrationMail without invite code English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -421,7 +421,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendRegistrationMail without invite code German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -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&amp;nonce=123456&amp;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&amp;nonce=123456&amp;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]!

View File

@ -3,7 +3,7 @@
exports[`sendResetPasswordMail English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -132,7 +132,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendResetPasswordMail German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
@ -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&amp;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&amp;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]!

View File

@ -3,7 +3,7 @@
exports[`sendWrongEmail English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
@ -130,7 +130,7 @@ ocelot.social Community [https://ocelot.social]",
exports[`sendWrongEmail German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social",
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>

View File

@ -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:",

View File

@ -7,17 +7,14 @@ import path from 'node:path'
import Email from 'email-templates'
import { createTransport } from 'nodemailer'
// import type Email as EmailType from '@types/email-templates'
import CONFIG from '@config/index'
import CONFIG, { nodemailerTransportOptions } from '@config/index'
import logosWebapp from '@config/logos'
import metadata from '@config/metadata'
import { UserDbProperties } from '@db/types/User'
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
const hasDKIMData =
CONFIG.SMTP_DKIM_DOMAINNAME && CONFIG.SMTP_DKIM_KEYSELECTOR && CONFIG.SMTP_DKIM_PRIVATKEY
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
@ -31,28 +28,13 @@ const defaultParams = {
renderSettingsUrl: true,
}
export const transport = createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports
pool: true,
maxConnections: CONFIG.SMTP_MAX_CONNECTIONS,
maxMessages: CONFIG.SMTP_MAX_MESSAGES,
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,
pass: CONFIG.SMTP_PASSWORD,
},
dkim: hasDKIMData && {
domainName: CONFIG.SMTP_DKIM_DOMAINNAME,
keySelector: CONFIG.SMTP_DKIM_KEYSELECTOR,
privateKey: CONFIG.SMTP_DKIM_PRIVATKEY,
},
})
const from = `${CONFIG.APPLICATION_NAME} <${CONFIG.EMAIL_DEFAULT_SENDER}>`
const transport = createTransport(nodemailerTransportOptions)
const email = new Email({
message: {
from: `${CONFIG.APPLICATION_NAME}`,
from,
},
transport,
i18n: {

View File

@ -0,0 +1,36 @@
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
}
}
`

View 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
}
}
`

View File

@ -0,0 +1,15 @@
import gql from 'graphql-tag'
export const currentUser = gql`
query currentUser {
currentUser {
following {
name
}
inviteCodes {
code
redeemedByCount
}
}
}
`

View 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
}
}
`

View 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
}
}
`

View File

@ -1,14 +0,0 @@
import gql from 'graphql-tag'
export const groupMembersQuery = () => {
return gql`
query ($id: ID!) {
GroupMembers(id: $id) {
id
name
slug
myRoleInGroup
}
}
`
}

View File

@ -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
}
}
`
}

View 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
}
}
`

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const redeemInviteCode = gql`
mutation redeemInviteCode($code: String!) {
redeemInviteCode(code: $code)
}
`

View 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
}
}
`

View File

@ -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()
})

View File

@ -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,
},

View File

@ -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
},
},
}

View File

@ -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('')
}

View File

@ -138,6 +138,9 @@ const s3Upload = async ({ createReadStream, uniqueFilename, mimetype }) => {
const s3 = new S3({ region, endpoint })
const s3Location = `original/${uniqueFilename}`
if (!Bucket) {
throw new Error('AWS_BUCKET is undefined')
}
const params = {
Bucket,
Key: s3Location,
@ -160,6 +163,9 @@ const s3Delete = async (url) => {
const s3 = new S3({ region, endpoint })
let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error
pathname = pathname.substring(1) // remove first character '/'
if (!Bucket) {
throw new Error('AWS_BUCKET is undefined')
}
const params = {
Bucket,
Key: pathname,

File diff suppressed because it is too large Load Diff

View File

@ -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)',
},

View File

@ -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) => {

View File

@ -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' }
})

View File

@ -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)
})

View File

@ -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,14 +54,45 @@ 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')
// To allow redeeming and return an User object we require a User in the context
context.user = user
// join Group via invite Code
await redeemInviteCode(context, inviteCode, true)
return user
})
try {
@ -70,51 +103,8 @@ export default {
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
}

View File

@ -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()
}
}

View File

@ -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,23 @@ export default {
},
},
User: {
inviteCodes: async (_parent, _args, context: Context, _resolveInfo) => {
const {
user: { id: userId },
} = context
return (
await context.database.query({
query: `
MATCH (user:User {id: $userId})-[:GENERATED]->(inviteCodes:InviteCode)
WHERE NOT (inviteCodes)-[:INVITES_TO]->(:Group)
RETURN inviteCodes {.*}
ORDER BY inviteCodes.createdAt ASC
`,
variables: { userId },
})
).records
},
emailNotificationSettings: async (parent, _params, _context, _resolveInfo) => {
return [
{

View File

@ -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
}

View File

@ -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!
}

View File

@ -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(

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import CONFIG from './config'
import createServer from './server'

View File

@ -1,80 +0,0 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { createTransport } from 'nodemailer'
import { htmlToText } from 'nodemailer-html-to-text'
import CONFIG from '@config/index'
import { cleanHtml } from '@middleware/helpers/cleanHtml'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
const hasDKIMData =
CONFIG.SMTP_DKIM_DOMAINNAME && CONFIG.SMTP_DKIM_KEYSELECTOR && CONFIG.SMTP_DKIM_PRIVATKEY
const transporter = createTransport({
host: CONFIG.SMTP_HOST,
port: CONFIG.SMTP_PORT,
ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports
pool: true,
maxConnections: CONFIG.SMTP_MAX_CONNECTIONS,
maxMessages: CONFIG.SMTP_MAX_MESSAGES,
auth: hasAuthData && {
user: CONFIG.SMTP_USERNAME,
pass: CONFIG.SMTP_PASSWORD,
},
dkim: hasDKIMData && {
domainName: CONFIG.SMTP_DKIM_DOMAINNAME,
keySelector: CONFIG.SMTP_DKIM_KEYSELECTOR,
privateKey: CONFIG.SMTP_DKIM_PRIVATKEY,
},
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-function
let sendMailCallback: any = async () => {}
if (!hasEmailConfig) {
if (!CONFIG.TEST) {
// eslint-disable-next-line no-console
console.log('Warning: Middlewares will not try to send mails.')
// TODO: disable e-mail logging on database seeding?
// TODO: implement general logging like 'log4js', see Gradido project: https://github.com/gradido/gradido/blob/master/backend/log4js-config.json
sendMailCallback = async (templateArgs) => {
// eslint-disable-next-line no-console
console.log('--- Log Unsend E-Mail ---')
// eslint-disable-next-line no-console
console.log('To: ' + templateArgs.to)
// eslint-disable-next-line no-console
console.log('From: ' + templateArgs.from)
// eslint-disable-next-line no-console
console.log('Subject: ' + templateArgs.subject)
// eslint-disable-next-line no-console
console.log('Content:')
// eslint-disable-next-line no-console
console.log(
cleanHtml(templateArgs.html, 'dummyKey', {
allowedTags: ['a'],
allowedAttributes: { a: ['href'] },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any).replace(/&amp;/g, '&'),
)
}
}
} else {
sendMailCallback = async (templateArgs) => {
transporter.use(
'compile',
htmlToText({
ignoreImage: true,
wordwrap: false,
}),
)
await transporter.sendMail(templateArgs)
}
}
export const sendMail = sendMailCallback

View File

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

View File

@ -1,140 +0,0 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable import/no-namespace */
import mustache from 'mustache'
import CONFIG from '@config/index'
import logosWebapp from '@config/logos'
import metadata from '@config/metadata'
import * as templates from './templates'
import * as templatesDE from './templates/de'
import * as templatesEN from './templates/en'
const from = CONFIG.EMAIL_DEFAULT_SENDER
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const defaultParams = {
welcomeImageUrl,
APPLICATION_NAME: CONFIG.APPLICATION_NAME,
ORGANIZATION_NAME: metadata.ORGANIZATION_NAME,
ORGANIZATION_URL: CONFIG.ORGANIZATION_URL,
supportUrl: CONFIG.SUPPORT_URL,
}
const englishHint = 'English version below!'
export const signupTemplate = ({ email, variables: { nonce, inviteCode = null } }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
// dev format example: http://localhost:3000/registration?method=invite-mail&email=huss%40pjannto.com&nonce=64853
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', email)
actionUrl.searchParams.set('nonce', nonce)
if (inviteCode) {
actionUrl.searchParams.set('inviteCode', inviteCode)
actionUrl.searchParams.set('method', 'invite-code')
} else {
actionUrl.searchParams.set('method', 'invite-mail')
}
const renderParams = { ...defaultParams, englishHint, actionUrl, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.signup }),
}
}
export const emailVerificationTemplate = ({ email, variables: { nonce, name } }) => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', email)
actionUrl.searchParams.set('nonce', nonce)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.emailVerification }),
}
}
export const resetPasswordTemplate = ({ email, variables: { nonce, name } }) => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
const renderParams = { ...defaultParams, englishHint, actionUrl, name, nonce, subject }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.passwordReset }),
}
}
export const chatMessageTemplate = ({ email, variables: { senderUser, recipientUser } }) => {
const subject = `Neue Chat-Nachricht | New chat message - ${senderUser.name}`
const actionUrl = new URL('/chat', CONFIG.CLIENT_URI)
const renderParams = {
...defaultParams,
subject,
englishHint,
actionUrl,
senderUser,
recipientUser,
}
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.chatMessage }),
}
}
export const wrongAccountTemplate = ({ email, _variables = {} }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, englishHint, actionUrl }
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content: templates.wrongAccount }),
}
}
export const notificationTemplate = ({ email, variables: { notification } }) => {
const actionUrl = new URL('/notifications', CONFIG.CLIENT_URI)
const settingsUrl = new URL('/settings/notifications', CONFIG.CLIENT_URI)
const renderParams = { ...defaultParams, name: notification.to.name, settingsUrl, actionUrl }
let content
switch (notification.to.locale) {
case 'de':
content = templatesDE.notification
break
case 'en':
content = templatesEN.notification
break
default:
content = templatesEN.notification
break
}
const subjectUnrendered = content.split('\n')[0].split('"')[1]
const subject = mustache.render(subjectUnrendered, renderParams, {})
return {
from,
to: email,
subject,
html: mustache.render(templates.layout, renderParams, { content }),
}
}

View File

@ -1,105 +0,0 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ recipientUser.name }}!</h1>
<p style="margin: 0;">Du hast eine neue Chat-Nachricht von <b>{{ senderUser.name }}</b> erhalten.</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Chat anzeigen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ recipientUser.name }}!</h1>
<p style="margin: 0;">You have received a new chat message from <b>{{ senderUser.name }}</b>.</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Show Chat</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,9 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line n/no-sync
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const notification = readFile('./notification.html')

View File

@ -1,83 +0,0 @@
<!-- emailSubject: "{{APPLICATION_NAME}} Benachrichtigung" -->
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }},</h1>
<p style="margin: 0;">Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button, um sie anzusehen:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Benachrichtigungen
ansehen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a href="{{{ settingsUrl }}}"
style="color: #17b53e;">Benachrichtigungseinstellungen</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->

View File

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

View File

@ -1,9 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line n/no-sync
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const notification = readFile('./notification.html')

View File

@ -1,83 +0,0 @@
<!-- emailSubject: "{{APPLICATION_NAME}} Notification" -->
<!-- Email Body English : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }},</h1>
<p style="margin: 0;">You received at least one notification. Click on this button to view them:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">View
notifications</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">PS: If you don't want to receive e-mails anymore, change your <a href="{{{ settingsUrl }}}"
style="color: #17b53e;">notification settings</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,15 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable security/detect-non-literal-fs-filename */
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line n/no-sync
const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const signup = readFile('./signup.html')
export const passwordReset = readFile('./resetPassword.html')
export const wrongAccount = readFile('./wrongAccount.html')
export const emailVerification = readFile('./emailVerification.html')
export const chatMessage = readFile('./chatMessage.html')
export const layout = readFile('./layout.html')

View File

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

View File

@ -1,185 +0,0 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }}!</h1>
<p style="margin: 0;">Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
kannst Du innerhalb der nächsten 24 Stunden Dein Passwort zurücksetzen:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Passwort
zurücksetzen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Falls Du kein neues Passwort angefordert hast, kannst Du diese E-Mail einfach
ignorieren. Wenn Du noch Fragen hast, melde Dich gerne <a href="{{{ supportUrl }}}"
style="color: #17b53e;">bei
unserem Support Team</a>!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
<td style="display: none;">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }}!</h1>
<p style="margin: 0;">So, you forgot your password? No problem! Just click the button below to reset
it within the next 24 hours:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Reset
password</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you didn't request a new password feel free to ignore this e-mail. You can
also <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> if you have any questions!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,214 +0,0 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Willkommen bei {{APPLICATION_NAME}}!</h1>
<p style="margin: 0;">Danke, dass Du dich angemeldet hast wir freuen uns, Dich dabei zu haben. Jetzt
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können ... Bitte bestätige
Deine E-Mail Adresse:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Bestätige
Deine E-Mail Adresse</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
<tr>
<td style="text-align: center; color :#17b53e">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0;">Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
<p style="margin: 0; margin-top: 10px;">Falls Du Dich nicht selbst bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> angemeldet hast, schau doch mal vorbei!
Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.</p>
<p style="margin: 0; margin-top: 10px;">PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese
E-Mail einfach ignorieren. ;)</p>
</td>
</tr>
<tr>
<td style="text-align: center; color :#17b53e">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
unserem Support Team</a>, wenn Du Fragen hast.</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
<td style="display: none;">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Welcome to {{APPLICATION_NAME}}!</h1>
<p style="margin: 0;">Thank you for joining our cause it's awesome to have you on board. There's
just one tiny step missing before we can start shaping the world together ... Please confirm your
e-mail address by clicking the button below:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Confirm
your e-mail address</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
<tr>
<td style="text-align: center; color :#17b53e">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work, you can also copy the following code into your browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0;">However, this only works if you have registered through our website.</p>
<p style="margin: 0; margin-top: 10px;">If you didn't sign up for <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> we recommend you to check it out!
It's a social network from people for people who want to connect and change the world together.</p>
<p style="margin: 0; margin-top: 10px;">PS: If you ignore this e-mail we will not create an account
for
you. ;)</p>
</td>
</tr>
<tr>
<td style="text-align: center; color :#17b53e">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> with any
questions you have.</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,185 +0,0 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo!</h1>
<p style="margin: 0;">Du hast bei uns ein neues Passwort angefordert leider haben wir aber keinen Account mit Deiner E-Mailadresse gefunden.
Kann es sein, dass Du mit einer anderen Adresse bei uns
angemeldet bist?</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Versuch'
es mit einer anderen E-Mail</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Wenn Du noch keinen Account bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> hast oder Dein Password gar nicht ändern willst,
kannst Du diese E-Mail einfach ignorieren!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Ansonsten hilft Dir <a href="{{{ supportUrl }}}" style="color: #17b53e;">unser
Support Team</a> gerne weiter.</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
<tr>
<td style="display: none;">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello!</h1>
<p style="margin: 0;">You requested a password reset but unfortunately we couldn't find an account associated with your e-mail address.
Did you maybe use another one when you signed up?</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Try
a different e-mail</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you don't have an account at <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a> yet or if you didn't want to reset your password,
please ignore this e-mail.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Otherwise <a href="{{{ supportUrl }}}" style="color: #17b53e;">our
support team</a> will be happy to help you out.</p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -1,7 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { applyMiddleware, IMiddleware } from 'graphql-middleware'
@ -17,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'

View File

@ -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',

View File

@ -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,

View File

@ -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 () => {

View File

@ -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()

View File

@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "3.5.2"
appVersion: "3.5.3"

View File

@ -21,4 +21,4 @@ version: 0.1.0
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "3.5.2"
appVersion: "3.5.3"

View File

@ -30,6 +30,8 @@ services:
environment:
- NODE_ENV="development"
- DEBUG=true
- SMTP_PORT=1025
- SMTP_HOST=mailserver
volumes:
- ./backend:/app

View File

@ -1,12 +1,12 @@
{
"name": "ocelot-social-frontend",
"version": "3.5.2",
"version": "3.5.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ocelot-social-frontend",
"version": "3.5.2",
"version": "3.5.3",
"license": "Apache-2.0",
"dependencies": {
"@intlify/unplugin-vue-i18n": "^2.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-frontend",
"version": "3.5.2",
"version": "3.5.3",
"description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends",
"main": "build/index.js",
"type": "module",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social",
"version": "3.5.2",
"version": "3.5.3",
"description": "Free and open source software program code available to run social networks.",
"author": "ocelot.social Community",
"license": "MIT",

View File

@ -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"
},
@ -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}!",
@ -335,7 +335,7 @@
},
"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",
"newPost": "Erstelle einen neuen Beitrag",
@ -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",

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-webapp",
"version": "3.5.2",
"version": "3.5.3",
"description": "ocelot.social Frontend",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",