mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' into chat-message-notification-e2e-tests
This commit is contained in:
commit
7c33de9cc5
35
CHANGELOG.md
35
CHANGELOG.md
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -5,98 +5,116 @@ export const CATEGORIES_MAX = 3
|
||||
export const categories = [
|
||||
{
|
||||
icon: 'networking',
|
||||
id: 'cat0',
|
||||
slug: 'networking',
|
||||
name: 'networking',
|
||||
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
|
||||
},
|
||||
{
|
||||
icon: 'home',
|
||||
id: 'cat1',
|
||||
slug: 'home',
|
||||
name: 'home',
|
||||
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
|
||||
},
|
||||
{
|
||||
icon: 'energy',
|
||||
id: 'cat2',
|
||||
slug: 'energy',
|
||||
name: 'energy',
|
||||
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
|
||||
},
|
||||
{
|
||||
icon: 'psyche',
|
||||
id: 'cat3',
|
||||
slug: 'psyche',
|
||||
name: 'psyche',
|
||||
description: 'Seele, Gefühle, Glück',
|
||||
},
|
||||
{
|
||||
icon: 'movement',
|
||||
id: 'cat4',
|
||||
slug: 'body-and-excercise',
|
||||
name: 'body-and-excercise',
|
||||
description: 'Sport, Yoga, Massage, Tanzen, Entspannung',
|
||||
},
|
||||
{
|
||||
icon: 'balance-scale',
|
||||
id: 'cat5',
|
||||
slug: 'law',
|
||||
name: 'law',
|
||||
description: 'Menschenrechte, Gesetze, Verordnungen',
|
||||
},
|
||||
{
|
||||
icon: 'finance',
|
||||
id: 'cat6',
|
||||
slug: 'finance',
|
||||
name: 'finance',
|
||||
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'child',
|
||||
id: 'cat7',
|
||||
slug: 'children',
|
||||
name: 'children',
|
||||
description: 'Familie, Pädagogik, Schule, Prägung',
|
||||
},
|
||||
{
|
||||
icon: 'mobility',
|
||||
id: 'cat8',
|
||||
slug: 'mobility',
|
||||
name: 'mobility',
|
||||
description: 'Reise, Verkehr, Elektromobilität',
|
||||
},
|
||||
{
|
||||
icon: 'shopping-cart',
|
||||
id: 'cat9',
|
||||
slug: 'economy',
|
||||
name: 'economy',
|
||||
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
|
||||
},
|
||||
{
|
||||
icon: 'peace',
|
||||
id: 'cat10',
|
||||
slug: 'peace',
|
||||
name: 'peace',
|
||||
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
|
||||
},
|
||||
{
|
||||
icon: 'politics',
|
||||
id: 'cat11',
|
||||
slug: 'politics',
|
||||
name: 'politics',
|
||||
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
|
||||
},
|
||||
{
|
||||
icon: 'nature',
|
||||
id: 'cat12',
|
||||
slug: 'nature',
|
||||
name: 'nature',
|
||||
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
|
||||
},
|
||||
{
|
||||
icon: 'science',
|
||||
id: 'cat13',
|
||||
slug: 'science',
|
||||
name: 'science',
|
||||
description: 'Bildung, Hochschule, Publikationen, ...',
|
||||
},
|
||||
{
|
||||
icon: 'health',
|
||||
id: 'cat14',
|
||||
slug: 'health',
|
||||
name: 'health',
|
||||
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
|
||||
},
|
||||
{
|
||||
icon: 'media',
|
||||
id: 'cat15',
|
||||
slug: 'it-and-media',
|
||||
name: 'it-and-media',
|
||||
description:
|
||||
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
|
||||
},
|
||||
{
|
||||
icon: 'spirituality',
|
||||
id: 'cat16',
|
||||
slug: 'spirituality',
|
||||
name: 'spirituality',
|
||||
description: 'Religion, Werte, Ethik',
|
||||
},
|
||||
{
|
||||
icon: 'culture',
|
||||
id: 'cat17',
|
||||
slug: 'culture',
|
||||
name: 'culture',
|
||||
description: 'Kunst, Theater, Musik, Fotografie, Film',
|
||||
},
|
||||
{
|
||||
icon: 'miscellaneous',
|
||||
id: 'cat18',
|
||||
slug: 'miscellaneous',
|
||||
name: 'miscellaneous',
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Driver } from 'neo4j-driver'
|
||||
|
||||
export const query =
|
||||
(driver: Driver) =>
|
||||
async ({ query, variables = {} }: { driver; query: string; variables: object }) => {
|
||||
async ({ query, variables = {} }: { query: string; variables?: object }) => {
|
||||
const session = driver.session()
|
||||
|
||||
const result = session.readTransaction(async (transaction) => {
|
||||
@ -19,9 +19,9 @@ export const query =
|
||||
}
|
||||
}
|
||||
|
||||
export const mutate =
|
||||
export const write =
|
||||
(driver: Driver) =>
|
||||
async ({ query, variables = {} }: { driver; query: string; variables: object }) => {
|
||||
async ({ query, variables = {} }: { query: string; variables?: object }) => {
|
||||
const session = driver.session()
|
||||
|
||||
const result = session.writeTransaction(async (transaction) => {
|
||||
@ -44,6 +44,6 @@ export default () => {
|
||||
driver,
|
||||
neode,
|
||||
query: query(driver),
|
||||
mutate: mutate(driver),
|
||||
write: write(driver),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,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)
|
||||
},
|
||||
|
||||
@ -1,38 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { categories } from '@constants/categories'
|
||||
import databaseContext from '@context/database'
|
||||
|
||||
import { getDriver } from './neo4j'
|
||||
const { query, write, driver } = databaseContext()
|
||||
|
||||
const createCategories = async () => {
|
||||
const driver = getDriver()
|
||||
const session = driver.session()
|
||||
const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
categories.forEach(({ icon, name }, index) => {
|
||||
const id = `cat${index + 1}`
|
||||
txc.run(
|
||||
`MERGE (c:Category {
|
||||
icon: "${icon}",
|
||||
slug: "${name}",
|
||||
name: "${name}",
|
||||
id: "${id}",
|
||||
createdAt: toString(datetime())
|
||||
})`,
|
||||
)
|
||||
})
|
||||
const result = await query({
|
||||
query: 'MATCH (category:Category) RETURN category { .* }',
|
||||
})
|
||||
try {
|
||||
await createCategoriesTxResultPromise
|
||||
console.log('Successfully created categories!') // eslint-disable-line no-console
|
||||
// eslint-disable-next-line no-catch-all/no-catch-all
|
||||
} catch (error) {
|
||||
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
|
||||
} finally {
|
||||
session.close()
|
||||
driver.close()
|
||||
}
|
||||
|
||||
const existingCategories = result.records.map((r) => r.get('category'))
|
||||
const existingCategoryIds = existingCategories.map((c) => c.id)
|
||||
|
||||
const newCategories = categories.filter((c) => !existingCategoryIds.includes(c.id))
|
||||
|
||||
await write({
|
||||
query: `UNWIND $newCategories AS map
|
||||
CREATE (category:Category)
|
||||
SET category = map
|
||||
SET category.createdAt = toString(datetime())`,
|
||||
variables: {
|
||||
newCategories,
|
||||
},
|
||||
})
|
||||
|
||||
const categoryIds = categories.map((c) => c.id)
|
||||
await write({
|
||||
query: `MATCH (category:Category)
|
||||
WHERE NOT category.id IN $categoryIds
|
||||
DETACH DELETE category`,
|
||||
variables: {
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Successfully created categories!')
|
||||
await driver.close()
|
||||
}
|
||||
|
||||
;(async function () {
|
||||
|
||||
@ -10,7 +10,7 @@ import { Factory } from 'rosie'
|
||||
import slugify from 'slug'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import generateInviteCode from '@graphql/resolvers/helpers/generateInviteCode'
|
||||
import { generateInviteCode } from '@graphql/resolvers/inviteCodes'
|
||||
|
||||
import { getDriver, getNeode } from './neo4j'
|
||||
|
||||
@ -268,17 +268,27 @@ const inviteCodeDefaults = {
|
||||
|
||||
Factory.define('inviteCode')
|
||||
.attrs(inviteCodeDefaults)
|
||||
.option('groupId', null)
|
||||
.option('group', ['groupId'], (groupId) => {
|
||||
if (groupId) {
|
||||
return neode.find('Group', groupId)
|
||||
}
|
||||
})
|
||||
.option('generatedById', null)
|
||||
.option('generatedBy', ['generatedById'], (generatedById) => {
|
||||
if (generatedById) return neode.find('User', generatedById)
|
||||
return Factory.build('user')
|
||||
})
|
||||
.after(async (buildObject, options) => {
|
||||
const [inviteCode, generatedBy] = await Promise.all([
|
||||
const [inviteCode, generatedBy, group] = await Promise.all([
|
||||
neode.create('InviteCode', buildObject),
|
||||
options.generatedBy,
|
||||
options.group,
|
||||
])
|
||||
await Promise.all([inviteCode.relateTo(generatedBy, 'generated')])
|
||||
await inviteCode.relateTo(generatedBy, 'generated')
|
||||
if (group) {
|
||||
await inviteCode.relateTo(group, 'invitesTo')
|
||||
}
|
||||
return inviteCode
|
||||
})
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -14,4 +14,10 @@ export default {
|
||||
target: 'User',
|
||||
direction: 'in',
|
||||
},
|
||||
invitesTo: {
|
||||
type: 'relationship',
|
||||
relationship: 'INVITES_TO',
|
||||
target: 'Group',
|
||||
direction: 'out',
|
||||
},
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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&nonce=123456">E-Mail Adresse bestätigen</a>
|
||||
<p>Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. </p>
|
||||
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst du deine neue E-Mail Adresse bestätigen:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456">E-Mail Adresse bestätigen</a>
|
||||
<p>Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. </p>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -239,16 +239,16 @@ footer {
|
||||
"text": "HALLO USER,
|
||||
|
||||
Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button
|
||||
kannst Du Deine neue E-Mail Adresse bestätigen:
|
||||
kannst du deine neue E-Mail Adresse bestätigen:
|
||||
|
||||
E-Mail Adresse bestätigen
|
||||
[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456]
|
||||
|
||||
Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese
|
||||
Falls du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese
|
||||
Nachricht einfach ignorieren.
|
||||
|
||||
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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&nonce=123456&inviteCode=welcome&method=invite-code">Bestätige Deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
|
||||
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
<p>Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code">Bestätige deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.</p>
|
||||
<p>Falls du dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
</p>
|
||||
<p>PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<p>PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -252,21 +252,21 @@ footer {
|
||||
|
||||
Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt
|
||||
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können
|
||||
… Bitte bestätige Deine E-Mail Adresse:
|
||||
… Bitte bestätige deine E-Mail Adresse:
|
||||
|
||||
Bestätige Deine E-Mail Adresse
|
||||
Bestätige deine E-Mail Adresse
|
||||
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code]
|
||||
|
||||
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert
|
||||
Das funktioniert allerdings nur, wenn du dich über unsere Website registriert
|
||||
hast.
|
||||
|
||||
Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
|
||||
PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach
|
||||
PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach
|
||||
ignorieren. ;)
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
@ -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&nonce=123456&method=invite-mail">Bestätige Deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
|
||||
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
<p>Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail">Bestätige deine E-Mail Adresse</a>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Das funktioniert allerdings nur, wenn du dich über unsere Website registriert hast.</p>
|
||||
<p>Falls du dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
</p>
|
||||
<p>PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<p>PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach ignorieren. ;)</p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -531,21 +531,21 @@ footer {
|
||||
|
||||
Danke, dass du dich angemeldet hast – wir freuen uns, dich dabei zu haben. Jetzt
|
||||
fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können
|
||||
… Bitte bestätige Deine E-Mail Adresse:
|
||||
… Bitte bestätige deine E-Mail Adresse:
|
||||
|
||||
Bestätige Deine E-Mail Adresse
|
||||
Bestätige deine E-Mail Adresse
|
||||
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail]
|
||||
|
||||
Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert
|
||||
Das funktioniert allerdings nur, wenn du dich über unsere Website registriert
|
||||
hast.
|
||||
|
||||
Falls Du Dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
Falls du dich nicht selbst bei ocelot.social angemeldet hast, schau doch mal
|
||||
vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk – von Menschen für Menschen.
|
||||
|
||||
PS: Wenn Du keinen Account bei uns möchtest, kannst Du diese E-Mail einfach
|
||||
PS: Wenn du keinen Account bei uns möchtest, kannst du diese E-Mail einfach
|
||||
ignorieren. ;)
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
@ -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&nonce=123456">Bestätige Deine E-Mail Adresse</a>
|
||||
<p>Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456">Bestätige deine E-Mail Adresse</a>
|
||||
<p>Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach ignorieren.</p>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<p>Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in dein Browserfenster kopieren: <span>123456</span></p>
|
||||
<div class="text-block">
|
||||
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
|
||||
<p>– Dein ocelot.social Team</p>
|
||||
@ -240,14 +240,14 @@ footer {
|
||||
Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button
|
||||
kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:
|
||||
|
||||
Bestätige Deine E-Mail Adresse
|
||||
Bestätige deine E-Mail Adresse
|
||||
[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456]
|
||||
|
||||
Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail einfach
|
||||
ignorieren.
|
||||
|
||||
Sollte der Button für dich nicht funktionieren, kannst du auch folgenden Code in
|
||||
Dein Browserfenster kopieren: 123456
|
||||
dein Browserfenster kopieren: 123456
|
||||
|
||||
Bis bald bei ocelot.social [https://ocelot.social]!
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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:",
|
||||
|
||||
@ -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: {
|
||||
|
||||
36
backend/src/graphql/queries/Group.ts
Normal file
36
backend/src/graphql/queries/Group.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
||||
12
backend/src/graphql/queries/GroupMembers.ts
Normal file
12
backend/src/graphql/queries/GroupMembers.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const GroupMembers = gql`
|
||||
query GroupMembers($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
15
backend/src/graphql/queries/currentUser.ts
Normal file
15
backend/src/graphql/queries/currentUser.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const currentUser = gql`
|
||||
query currentUser {
|
||||
currentUser {
|
||||
following {
|
||||
name
|
||||
}
|
||||
inviteCodes {
|
||||
code
|
||||
redeemedByCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
36
backend/src/graphql/queries/generateGroupInviteCode.ts
Normal file
36
backend/src/graphql/queries/generateGroupInviteCode.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const generateGroupInviteCode = gql`
|
||||
mutation generateGroupInviteCode($groupId: ID!, $expiresAt: String, $comment: String) {
|
||||
generateGroupInviteCode(groupId: $groupId, expiresAt: $expiresAt, comment: $comment) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
36
backend/src/graphql/queries/generatePersonalInviteCode.ts
Normal file
36
backend/src/graphql/queries/generatePersonalInviteCode.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const generatePersonalInviteCode = gql`
|
||||
mutation generatePersonalInviteCode($expiresAt: String, $comment: String) {
|
||||
generatePersonalInviteCode(expiresAt: $expiresAt, comment: $comment) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,14 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const groupMembersQuery = () => {
|
||||
return gql`
|
||||
query ($id: ID!) {
|
||||
GroupMembers(id: $id) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
myRoleInGroup
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const groupQuery = () => {
|
||||
return gql`
|
||||
query ($isMember: Boolean, $id: ID, $slug: String) {
|
||||
Group(isMember: $isMember, id: $id, slug: $slug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
createdAt
|
||||
updatedAt
|
||||
disabled
|
||||
deleted
|
||||
about
|
||||
description
|
||||
descriptionExcerpt
|
||||
groupType
|
||||
actionRadius
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
locationName
|
||||
location {
|
||||
name
|
||||
nameDE
|
||||
nameEN
|
||||
}
|
||||
myRole
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
36
backend/src/graphql/queries/invalidateInviteCode.ts
Normal file
36
backend/src/graphql/queries/invalidateInviteCode.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const invalidateInviteCode = gql`
|
||||
mutation invalidateInviteCode($code: String!) {
|
||||
invalidateInviteCode(code: $code) {
|
||||
code
|
||||
createdAt
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
redeemedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
expiresAt
|
||||
comment
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
7
backend/src/graphql/queries/redeemInviteCode.ts
Normal file
7
backend/src/graphql/queries/redeemInviteCode.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const redeemInviteCode = gql`
|
||||
mutation redeemInviteCode($code: String!) {
|
||||
redeemInviteCode(code: $code)
|
||||
}
|
||||
`
|
||||
49
backend/src/graphql/queries/validateInviteCode.ts
Normal file
49
backend/src/graphql/queries/validateInviteCode.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const unauthenticatedValidateInviteCode = gql`
|
||||
query validateInviteCode($code: String!) {
|
||||
validateInviteCode(code: $code) {
|
||||
code
|
||||
invitedTo {
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
generatedBy {
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const authenticatedValidateInviteCode = gql`
|
||||
query validateInviteCode($code: String!) {
|
||||
validateInviteCode(code: $code) {
|
||||
code
|
||||
invitedTo {
|
||||
id
|
||||
groupType
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
generatedBy {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
isValid
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,41 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
const driver = getDriver()
|
||||
const instance = getNeode()
|
||||
let regularUser, administrator, moderator, badge, verification
|
||||
|
||||
let authenticatedUser, regularUser, administrator, moderator, badge, verification, query, mutate
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let query, mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
describe('Badges', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode: instance,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
regularUser = await Factory.build(
|
||||
'user',
|
||||
@ -83,7 +85,6 @@ describe('Badges', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -122,7 +123,7 @@ describe('Badges', () => {
|
||||
})
|
||||
|
||||
describe('authenticated as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = moderator.toJson()
|
||||
})
|
||||
|
||||
@ -322,7 +323,7 @@ describe('Badges', () => {
|
||||
})
|
||||
|
||||
describe('authenticated as moderator', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = moderator.toJson()
|
||||
})
|
||||
|
||||
|
||||
@ -10,8 +10,8 @@ import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
|
||||
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
|
||||
import { groupMembersQuery } from '@graphql/queries/groupMembersQuery'
|
||||
import { groupQuery } from '@graphql/queries/groupQuery'
|
||||
import { Group as groupQuery } from '@graphql/queries/Group'
|
||||
import { GroupMembers as groupMembersQuery } from '@graphql/queries/GroupMembers'
|
||||
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
|
||||
import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation'
|
||||
import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation'
|
||||
@ -423,7 +423,7 @@ describe('in mode', () => {
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupQuery(), variables: {} })
|
||||
const { errors } = await query({ query: groupQuery, variables: {} })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -541,7 +541,7 @@ describe('in mode', () => {
|
||||
describe('in general finds only listed groups – no hidden groups where user is none or pending member', () => {
|
||||
describe('without any filters', () => {
|
||||
it('finds all listed groups – including the set descriptionExcerpts and locations', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: {} })
|
||||
const result = await query({ query: groupQuery, variables: {} })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
@ -586,9 +586,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('has set categories', async () => {
|
||||
await expect(
|
||||
query({ query: groupQuery(), variables: {} }),
|
||||
).resolves.toMatchObject({
|
||||
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
@ -622,7 +620,7 @@ describe('in mode', () => {
|
||||
describe('with given id', () => {
|
||||
describe("id = 'my-group'", () => {
|
||||
it('finds only the listed group with this id', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: { id: 'my-group' } })
|
||||
const result = await query({ query: groupQuery, variables: { id: 'my-group' } })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: [
|
||||
@ -642,7 +640,7 @@ describe('in mode', () => {
|
||||
describe("id = 'third-hidden-group'", () => {
|
||||
it("finds only the hidden group where I'm 'usual' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { id: 'third-hidden-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -664,7 +662,7 @@ describe('in mode', () => {
|
||||
describe("id = 'second-hidden-group'", () => {
|
||||
it("finds no hidden group where I'm 'pending' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { id: 'second-hidden-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -674,7 +672,7 @@ describe('in mode', () => {
|
||||
describe("id = 'hidden-group'", () => {
|
||||
it("finds no hidden group where I'm not(!) a member at all", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { id: 'hidden-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -686,7 +684,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'the-best-group'", () => {
|
||||
it('finds only the listed group with this slug', async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'the-best-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -708,7 +706,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'third-investigative-journalism-group'", () => {
|
||||
it("finds only the hidden group where I'm 'usual' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'third-investigative-journalism-group' },
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -730,7 +728,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'second-investigative-journalism-group'", () => {
|
||||
it("finds no hidden group where I'm 'pending' member", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'second-investigative-journalism-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -740,7 +738,7 @@ describe('in mode', () => {
|
||||
describe("slug = 'investigative-journalism-group'", () => {
|
||||
it("finds no hidden group where I'm not(!) a member at all", async () => {
|
||||
const result = await query({
|
||||
query: groupQuery(),
|
||||
query: groupQuery,
|
||||
variables: { slug: 'investigative-journalism-group' },
|
||||
})
|
||||
expect(result.data?.Group.length).toBe(0)
|
||||
@ -750,7 +748,7 @@ describe('in mode', () => {
|
||||
|
||||
describe('isMember = true', () => {
|
||||
it('finds only listed groups where user is member', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: { isMember: true } })
|
||||
const result = await query({ query: groupQuery, variables: { isMember: true } })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
@ -774,7 +772,7 @@ describe('in mode', () => {
|
||||
|
||||
describe('isMember = false', () => {
|
||||
it('finds only listed groups where user is not(!) member', async () => {
|
||||
const result = await query({ query: groupQuery(), variables: { isMember: false } })
|
||||
const result = await query({ query: groupQuery, variables: { isMember: false } })
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
Group: expect.arrayContaining([
|
||||
@ -1039,7 +1037,7 @@ describe('in mode', () => {
|
||||
variables = {
|
||||
id: 'not-existing-group',
|
||||
}
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1212,7 +1210,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1245,7 +1243,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1278,7 +1276,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1321,7 +1319,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1354,7 +1352,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1386,7 +1384,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1397,7 +1395,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1419,7 +1417,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1456,7 +1454,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1493,7 +1491,7 @@ describe('in mode', () => {
|
||||
|
||||
it('finds all members', async () => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables,
|
||||
})
|
||||
expect(result).toMatchObject({
|
||||
@ -1529,7 +1527,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -1540,7 +1538,7 @@ describe('in mode', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
const { errors } = await query({ query: groupMembersQuery(), variables })
|
||||
const { errors } = await query({ query: groupMembersQuery, variables })
|
||||
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
|
||||
})
|
||||
})
|
||||
@ -2418,7 +2416,7 @@ describe('in mode', () => {
|
||||
describe('here "closed-group" for example', () => {
|
||||
const memberInGroup = async (userId, groupId) => {
|
||||
const result = await query({
|
||||
query: groupMembersQuery(),
|
||||
query: groupMembersQuery,
|
||||
variables: {
|
||||
id: groupId,
|
||||
},
|
||||
|
||||
@ -436,6 +436,24 @@ export default {
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
inviteCodes: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Group!')
|
||||
}
|
||||
return (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})-[:GENERATED]->(inviteCodes:InviteCode)-[:INVITES_TO]->(g:Group {id: $parent.id})
|
||||
RETURN inviteCodes {.*}
|
||||
ORDER BY inviteCodes.createdAt ASC
|
||||
`,
|
||||
variables: {
|
||||
user: context.user,
|
||||
parent,
|
||||
},
|
||||
})
|
||||
).records.map((r) => r.get('inviteCodes'))
|
||||
},
|
||||
...Resolver('Group', {
|
||||
undefinedToNull: ['deleted', 'disabled', 'locationName', 'about'],
|
||||
hasMany: {
|
||||
@ -451,6 +469,18 @@ export default {
|
||||
'MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )',
|
||||
},
|
||||
}),
|
||||
name: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!context.user) {
|
||||
return parent.groupType === 'hidden' ? '' : parent.name
|
||||
}
|
||||
return parent.name
|
||||
},
|
||||
about: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!context.user) {
|
||||
return parent.groupType === 'hidden' ? '' : parent.about
|
||||
}
|
||||
return parent.about
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import registrationConstants from '@constants/registrationBranded'
|
||||
|
||||
export default function generateInviteCode() {
|
||||
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
|
||||
return Array.from(
|
||||
{ length: registrationConstants.INVITE_CODE_LENGTH },
|
||||
(n: number = Math.floor(Math.random() * 36)) => {
|
||||
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65
|
||||
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48
|
||||
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
|
||||
},
|
||||
).join('')
|
||||
}
|
||||
@ -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
@ -1,136 +1,294 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import generateInviteCode from './helpers/generateInviteCode'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { validateInviteCode } from './transactions/inviteCodes'
|
||||
import CONFIG from '@config/index'
|
||||
import registrationConstants from '@constants/registrationBranded'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { Context } from '@src/server'
|
||||
|
||||
const uniqueInviteCode = async (session, code) => {
|
||||
return session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(`MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, {
|
||||
code,
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
export const generateInviteCode = () => {
|
||||
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
|
||||
return Array.from(
|
||||
{ length: registrationConstants.INVITE_CODE_LENGTH },
|
||||
(n: number = Math.floor(Math.random() * 36)) => {
|
||||
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65
|
||||
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48
|
||||
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
|
||||
},
|
||||
).join('')
|
||||
}
|
||||
|
||||
const uniqueInviteCode = async (context: Context, code: string) => {
|
||||
return (
|
||||
(
|
||||
await context.database.query({
|
||||
query: `MATCH (inviteCode:InviteCode { code: toUpper($code) })
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR inviteCode.expiresAt >= datetime()
|
||||
RETURN toString(count(inviteCode)) AS count`,
|
||||
variables: { code },
|
||||
})
|
||||
).records[0].get('count') === '0'
|
||||
)
|
||||
}
|
||||
|
||||
export const validateInviteCode = async (context: Context, inviteCode) => {
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
OPTIONAL MATCH (inviteCode:InviteCode { code: toUpper($inviteCode) })
|
||||
RETURN
|
||||
CASE
|
||||
WHEN inviteCode IS NULL THEN false
|
||||
WHEN inviteCode.expiresAt IS NULL THEN true
|
||||
WHEN datetime(inviteCode.expiresAt) >= datetime() THEN true
|
||||
ELSE false END AS result
|
||||
`,
|
||||
variables: { inviteCode },
|
||||
})
|
||||
return parseInt(String(result.records[0].get('count'))) === 0
|
||||
})
|
||||
).records
|
||||
return result[0].get('result') === true
|
||||
}
|
||||
|
||||
export const redeemInviteCode = async (context: Context, code, newUser = false) => {
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User)
|
||||
OPTIONAL MATCH (inviteCode)-[:INVITES_TO]->(group:Group)
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR datetime(inviteCode.expiresAt) >= datetime()
|
||||
RETURN inviteCode {.*}, group {.*}, host {.*}`,
|
||||
variables: { code },
|
||||
})
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const inviteCode = result[0].get('inviteCode')
|
||||
const group = result[0].get('group')
|
||||
const host = result[0].get('host')
|
||||
|
||||
if (!inviteCode || !host) {
|
||||
return false
|
||||
}
|
||||
|
||||
// self
|
||||
if (host.id === context.user.id) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Personal Invite Link
|
||||
if (!group) {
|
||||
// We redeemed this link while having an account, hence we do nothing, but return true
|
||||
if (!newUser) {
|
||||
return true
|
||||
}
|
||||
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id}), (inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User)
|
||||
MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
|
||||
MERGE (user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
|
||||
MERGE (host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
|
||||
`,
|
||||
variables: { user: context.user, code },
|
||||
})
|
||||
// Group Invite Link
|
||||
} else {
|
||||
const role = ['closed', 'hidden'].includes(group.groupType as string) ? 'pending' : 'usual'
|
||||
|
||||
const optionalInvited = newUser
|
||||
? 'MERGE (host)-[:INVITED { createdAt: toString(datetime()) }]->(user)'
|
||||
: ''
|
||||
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id}), (group:Group)<-[:INVITES_TO]-(inviteCode:InviteCode {code: toUpper($code)})<-[:GENERATED]-(host:User)
|
||||
MERGE (user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
|
||||
${optionalInvited}
|
||||
MERGE (user)-[membership:MEMBER_OF]->(group)
|
||||
ON CREATE SET
|
||||
membership.createdAt = toString(datetime()),
|
||||
membership.updatedAt = null,
|
||||
membership.role = $role
|
||||
`,
|
||||
variables: { user: context.user, code, role },
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
getInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
|
||||
WHERE ic.expiresAt IS NULL
|
||||
OR datetime(ic.expiresAt) >= datetime()
|
||||
RETURN properties(ic) AS inviteCodes`,
|
||||
{
|
||||
userId,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCodes'))
|
||||
})
|
||||
try {
|
||||
const inviteCode = await readTxResultPromise
|
||||
if (inviteCode && inviteCode.length > 0) return inviteCode[0]
|
||||
let code = generateInviteCode()
|
||||
while (!(await uniqueInviteCode(session, code))) {
|
||||
code = generateInviteCode()
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})
|
||||
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
|
||||
ON CREATE SET
|
||||
ic.createdAt = toString(datetime()),
|
||||
ic.expiresAt = $expiresAt
|
||||
RETURN ic AS inviteCode`,
|
||||
{
|
||||
userId,
|
||||
code,
|
||||
expiresAt: null,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCode').properties)
|
||||
validateInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode { code: toUpper($args.code) })
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR datetime(inviteCode.expiresAt) >= datetime()
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { args },
|
||||
})
|
||||
const txResult = await writeTxResultPromise
|
||||
return txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
return null
|
||||
}
|
||||
},
|
||||
MyInviteCodes: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
|
||||
RETURN properties(ic) AS inviteCodes`,
|
||||
{
|
||||
userId,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCodes'))
|
||||
})
|
||||
try {
|
||||
const txResult = await readTxResultPromise
|
||||
return txResult
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
isValidInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const { code } = args
|
||||
const session = context.driver.session()
|
||||
if (!code) return false
|
||||
return validateInviteCode(session, code)
|
||||
|
||||
return result[0].get('inviteCode')
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
GenerateInviteCode: async (_parent, args, context, _resolveInfo) => {
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
generatePersonalInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const userInviteCodeAmount = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode)<-[:GENERATED]-(user:User {id: $user.id})
|
||||
WHERE NOT (inviteCode)-[:INVITES_TO]-(:Group)
|
||||
AND (inviteCode.expiresAt IS NULL OR inviteCode.expiresAt >= datetime())
|
||||
RETURN toString(count(inviteCode)) as count
|
||||
`,
|
||||
variables: { user: context.user },
|
||||
})
|
||||
).records[0].get('count')
|
||||
|
||||
if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_PERSONAL_PER_USER) {
|
||||
throw new Error('You have reached the maximum of Invite Codes you can generate')
|
||||
}
|
||||
|
||||
let code = generateInviteCode()
|
||||
while (!(await uniqueInviteCode(session, code))) {
|
||||
while (!(await uniqueInviteCode(context, code))) {
|
||||
code = generateInviteCode()
|
||||
}
|
||||
const writeTxResultPromise = session.writeTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (user:User {id: $userId})
|
||||
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
|
||||
ON CREATE SET
|
||||
ic.createdAt = toString(datetime()),
|
||||
ic.expiresAt = $expiresAt
|
||||
RETURN ic AS inviteCode`,
|
||||
{
|
||||
userId,
|
||||
code,
|
||||
expiresAt: args.expiresAt,
|
||||
},
|
||||
|
||||
return (
|
||||
await context.database.write({
|
||||
// We delete a potential old invite code if there is a collision on an expired code
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})
|
||||
OPTIONAL MATCH (oldInviteCode:InviteCode { code: toUpper($code) })
|
||||
DETACH DELETE oldInviteCode
|
||||
MERGE (user)-[:GENERATED]->(inviteCode:InviteCode { code: toUpper($code)})
|
||||
ON CREATE SET
|
||||
inviteCode.createdAt = toString(datetime()),
|
||||
inviteCode.expiresAt = $args.expiresAt,
|
||||
inviteCode.comment = $args.comment
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { user: context.user, code, args },
|
||||
})
|
||||
).records[0].get('inviteCode')
|
||||
},
|
||||
generateGroupInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const userInviteCodeAmount = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (:Group {id: $args.groupId})<-[:INVITES_TO]-(inviteCode:InviteCode)<-[:GENERATED]-(user:User {id: $user.id})
|
||||
WHERE inviteCode.expiresAt IS NULL
|
||||
OR inviteCode.expiresAt >= datetime()
|
||||
RETURN toString(count(inviteCode)) as count
|
||||
`,
|
||||
variables: { user: context.user, args },
|
||||
})
|
||||
).records[0].get('count')
|
||||
|
||||
if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_GROUP_PER_USER) {
|
||||
throw new Error(
|
||||
'You have reached the maximum of Invite Codes you can generate for this group',
|
||||
)
|
||||
return result.records.map((record) => record.get('inviteCode').properties)
|
||||
})
|
||||
try {
|
||||
const txResult = await writeTxResultPromise
|
||||
return txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
|
||||
let code = generateInviteCode()
|
||||
while (!(await uniqueInviteCode(context, code))) {
|
||||
code = generateInviteCode()
|
||||
}
|
||||
|
||||
const inviteCode = (
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH
|
||||
(user:User {id: $user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId})
|
||||
WHERE NOT membership.role = 'pending'
|
||||
OPTIONAL MATCH (oldInviteCode:InviteCode { code: toUpper($code) })
|
||||
DETACH DELETE oldInviteCode
|
||||
MERGE (user)-[:GENERATED]->(inviteCode:InviteCode { code: toUpper($code) })-[:INVITES_TO]->(group)
|
||||
ON CREATE SET
|
||||
inviteCode.createdAt = toString(datetime()),
|
||||
inviteCode.expiresAt = $args.expiresAt,
|
||||
inviteCode.comment = $args.comment
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { user: context.user, code, args },
|
||||
})
|
||||
).records
|
||||
|
||||
if (inviteCode.length !== 1) {
|
||||
// Not a member
|
||||
throw new Error('Not Authorized!')
|
||||
}
|
||||
|
||||
return inviteCode[0].get('inviteCode')
|
||||
},
|
||||
invalidateInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
const result = (
|
||||
await context.database.write({
|
||||
query: `
|
||||
MATCH (user:User {id: $user.id})-[:GENERATED]-(inviteCode:InviteCode {code: toUpper($args.code)})
|
||||
SET inviteCode.expiresAt = toString(datetime())
|
||||
RETURN inviteCode {.*}`,
|
||||
variables: { args, user: context.user },
|
||||
})
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
// Link not generated by this user or does not exist
|
||||
throw new Error('Not Authorized!')
|
||||
}
|
||||
|
||||
return result[0].get('inviteCode')
|
||||
},
|
||||
redeemInviteCode: async (_parent, args, context: Context, _resolveInfo) => {
|
||||
return redeemInviteCode(context, args.code)
|
||||
},
|
||||
},
|
||||
InviteCode: {
|
||||
invitedTo: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.code) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = (
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (inviteCode:InviteCode {code: $parent.code})-[:INVITES_TO]->(group:Group)
|
||||
RETURN group {.*}
|
||||
`,
|
||||
variables: { parent },
|
||||
})
|
||||
).records
|
||||
|
||||
if (result.length !== 1) {
|
||||
return null
|
||||
}
|
||||
return result[0].get('group')
|
||||
},
|
||||
isValid: async (parent, _args, context: Context, _resolveInfo) => {
|
||||
if (!parent.code) {
|
||||
return false
|
||||
}
|
||||
return validateInviteCode(context, parent.code)
|
||||
},
|
||||
...Resolver('InviteCode', {
|
||||
idAttribute: 'code',
|
||||
undefinedToNull: ['expiresAt'],
|
||||
undefinedToNull: ['expiresAt', 'comment'],
|
||||
count: {
|
||||
redeemedByCount: '<-[:REDEEMED]-(related:User)',
|
||||
},
|
||||
hasOne: {
|
||||
generatedBy: '<-[:GENERATED]-(related:User)',
|
||||
},
|
||||
|
||||
@ -24,6 +24,9 @@ export default {
|
||||
],
|
||||
}),
|
||||
distanceToMe: async (parent, _params, context, _resolveInfo) => {
|
||||
if (!parent.id) {
|
||||
throw new Error('Can not identify selected Location!')
|
||||
}
|
||||
const session = context.driver.session()
|
||||
|
||||
const query = session.readTransaction(async (transaction) => {
|
||||
|
||||
@ -1,55 +1,51 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import Image from '@db/models/Image'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { createPostMutation } from '@graphql/queries/createPostMutation'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
CONFIG.CATEGORIES_ACTIVE = true
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
let query
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let user
|
||||
|
||||
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||
let variables
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let query, mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
mutate = createTestClientResult.mutate
|
||||
query = createTestClientResult.query
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
const categoryIds = ['cat9', 'cat4', 'cat15']
|
||||
let variables
|
||||
|
||||
beforeEach(async () => {
|
||||
variables = {}
|
||||
user = await Factory.build(
|
||||
@ -64,22 +60,22 @@ beforeEach(async () => {
|
||||
},
|
||||
)
|
||||
await Promise.all([
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat4',
|
||||
name: 'Environment & Nature',
|
||||
icon: 'tree',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat15',
|
||||
name: 'Consumption & Sustainability',
|
||||
icon: 'shopping-cart',
|
||||
}),
|
||||
neode.create('Category', {
|
||||
database.neode.create('Category', {
|
||||
id: 'cat27',
|
||||
name: 'Animal Protection',
|
||||
icon: 'paw',
|
||||
@ -88,7 +84,6 @@ beforeEach(async () => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -233,7 +228,6 @@ describe('Post', () => {
|
||||
Post(filter: $filter) {
|
||||
id
|
||||
author {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
@ -249,7 +243,7 @@ describe('Post', () => {
|
||||
Post: [
|
||||
{
|
||||
id: 'post-by-followed-user',
|
||||
author: { id: 'followed-by-me', name: 'Followed User' },
|
||||
author: { name: 'Followed User' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -976,11 +970,11 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
it('updates the image', async () => {
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -990,9 +984,9 @@ describe('UpdatePost', () => {
|
||||
variables = { ...variables, image: null }
|
||||
})
|
||||
it('deletes the image', async () => {
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(6)
|
||||
await expect(database.neode.all('Image')).resolves.toHaveLength(6)
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(neode.all('Image')).resolves.toHaveLength(5)
|
||||
await expect(database.neode.all('Image')).resolves.toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1002,11 +996,11 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
it('keeps the image unchanged', async () => {
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
await mutate({ mutation: updatePostMutation, variables })
|
||||
await expect(
|
||||
neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
|
||||
).resolves.toBeFalsy()
|
||||
})
|
||||
})
|
||||
@ -1253,18 +1247,18 @@ describe('pin posts', () => {
|
||||
|
||||
it('removes previous `pinned` attribute', async () => {
|
||||
const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post'
|
||||
pinnedPost = await neode.cypher(cypher, {})
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(cypher, {})
|
||||
pinnedPost = await database.neode.cypher(cypher, {})
|
||||
expect(pinnedPost.records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('removes previous PINNED relationship', async () => {
|
||||
variables = { ...variables, id: 'only-pinned-post' }
|
||||
await mutate({ mutation: pinPostMutation, variables })
|
||||
pinnedPost = await neode.cypher(
|
||||
pinnedPost = await database.neode.cypher(
|
||||
`MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`,
|
||||
{},
|
||||
)
|
||||
@ -1593,7 +1587,7 @@ describe('emotions', () => {
|
||||
`
|
||||
|
||||
beforeEach(async () => {
|
||||
author = await neode.create('User', { id: 'u257' })
|
||||
author = await database.neode.create('User', { id: 'u257' })
|
||||
postToEmote = await Factory.build(
|
||||
'post',
|
||||
{
|
||||
@ -1628,7 +1622,7 @@ describe('emotions', () => {
|
||||
`
|
||||
let postsEmotionsQueryVariables
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
postsEmotionsQueryVariables = { id: 'p1376' }
|
||||
})
|
||||
|
||||
|
||||
@ -1,49 +1,48 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import EmailAddress from '@db/models/EmailAddress'
|
||||
import User from '@db/models/User'
|
||||
import { getDriver, getNeode } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
let variables
|
||||
const driver = getDriver()
|
||||
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
mutate = createTestClient(server).mutate
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {}
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -98,7 +97,7 @@ describe('Signup', () => {
|
||||
describe('creates a EmailAddress node', () => {
|
||||
it('with `createdAt` attribute', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
const emailAddress = await neode.first<typeof EmailAddress>(
|
||||
const emailAddress = await database.neode.first<typeof EmailAddress>(
|
||||
'EmailAddress',
|
||||
{ email: 'someuser@example.org' },
|
||||
undefined,
|
||||
@ -112,7 +111,7 @@ describe('Signup', () => {
|
||||
|
||||
it('with a cryptographic `nonce`', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
const emailAddress = await neode.first<typeof EmailAddress>(
|
||||
const emailAddress = await database.neode.first<typeof EmailAddress>(
|
||||
'EmailAddress',
|
||||
{ email: 'someuser@example.org' },
|
||||
undefined,
|
||||
@ -153,12 +152,12 @@ describe('Signup', () => {
|
||||
|
||||
it('creates no additional `EmailAddress` node', async () => {
|
||||
// admin account and the already existing user
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||
data: { Signup: { email: 'someuser@example.org' } },
|
||||
errors: undefined,
|
||||
})
|
||||
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
await expect(database.neode.all('EmailAddress')).resolves.toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -194,7 +193,7 @@ describe('SignupVerification', () => {
|
||||
}
|
||||
`
|
||||
describe('given valid password and email', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
...variables,
|
||||
nonce: '12345',
|
||||
@ -207,7 +206,7 @@ describe('SignupVerification', () => {
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
@ -215,8 +214,8 @@ describe('SignupVerification', () => {
|
||||
beforeEach(async () => {
|
||||
const { email, nonce } = variables
|
||||
const [emailAddress, user] = await Promise.all([
|
||||
neode.model('EmailAddress').create({ email, nonce }),
|
||||
neode
|
||||
database.neode.model('EmailAddress').create({ email, nonce }),
|
||||
database.neode
|
||||
.model('User')
|
||||
.create({ name: 'Somebody', password: '1234', email: 'john@example.org' }),
|
||||
])
|
||||
@ -242,7 +241,7 @@ describe('SignupVerification', () => {
|
||||
email: 'john@example.org',
|
||||
nonce: '12345',
|
||||
}
|
||||
await neode.model('EmailAddress').create(args)
|
||||
await database.neode.model('EmailAddress').create(args)
|
||||
})
|
||||
|
||||
describe('sending a valid nonce', () => {
|
||||
@ -258,7 +257,7 @@ describe('SignupVerification', () => {
|
||||
|
||||
it('sets `verifiedAt` attribute of EmailAddress', async () => {
|
||||
await mutate({ mutation, variables })
|
||||
const email = await neode.first(
|
||||
const email = await database.neode.first(
|
||||
'EmailAddress',
|
||||
{ email: 'john@example.org' },
|
||||
undefined,
|
||||
@ -276,14 +275,18 @@ describe('SignupVerification', () => {
|
||||
RETURN email
|
||||
`
|
||||
await mutate({ mutation, variables })
|
||||
const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' })
|
||||
const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('sets `about` attribute of User', async () => {
|
||||
variables = { ...variables, about: 'Find this description in the user profile' }
|
||||
await mutate({ mutation, variables })
|
||||
const user = await neode.first<typeof User>('User', { name: 'John Doe' }, undefined)
|
||||
const user = await database.neode.first<typeof User>(
|
||||
'User',
|
||||
{ name: 'John Doe' },
|
||||
undefined,
|
||||
)
|
||||
await expect(user.toJson()).resolves.toMatchObject({
|
||||
about: 'Find this description in the user profile',
|
||||
})
|
||||
@ -306,7 +309,7 @@ describe('SignupVerification', () => {
|
||||
RETURN email
|
||||
`
|
||||
await mutate({ mutation, variables })
|
||||
const { records: emails } = await neode.cypher(cypher, { name: 'John Doe' })
|
||||
const { records: emails } = await database.neode.cypher(cypher, { name: 'John Doe' })
|
||||
expect(emails).toHaveLength(1)
|
||||
})
|
||||
|
||||
|
||||
@ -7,10 +7,12 @@ import { UserInputError } from 'apollo-server'
|
||||
import { hash } from 'bcryptjs'
|
||||
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
import existingEmailAddress from './helpers/existingEmailAddress'
|
||||
import generateNonce from './helpers/generateNonce'
|
||||
import normalizeEmail from './helpers/normalizeEmail'
|
||||
import { redeemInviteCode } from './inviteCodes'
|
||||
|
||||
const neode = getNeode()
|
||||
|
||||
@ -33,7 +35,7 @@ export default {
|
||||
throw new UserInputError(e.message)
|
||||
}
|
||||
},
|
||||
SignupVerification: async (_parent, args, context) => {
|
||||
SignupVerification: async (_parent, args, context: Context) => {
|
||||
const { termsAndConditionsAgreedVersion } = args
|
||||
const regEx = /^[0-9]+\.[0-9]+\.[0-9]+$/g
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
@ -52,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
|
||||
}
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
export async function validateInviteCode(session, inviteCode) {
|
||||
const readTxResultPromise = session.readTransaction(async (txc) => {
|
||||
const result = await txc.run(
|
||||
`MATCH (ic:InviteCode { code: toUpper($inviteCode) })
|
||||
RETURN
|
||||
CASE
|
||||
WHEN ic.expiresAt IS NULL THEN true
|
||||
WHEN datetime(ic.expiresAt) >= datetime() THEN true
|
||||
ELSE false END AS result`,
|
||||
{
|
||||
inviteCode,
|
||||
},
|
||||
)
|
||||
return result.records.map((record) => record.get('result'))
|
||||
})
|
||||
try {
|
||||
const txResult = await readTxResultPromise
|
||||
return !!txResult[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
|
||||
import Resolver from './helpers/Resolver'
|
||||
@ -467,6 +468,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 [
|
||||
{
|
||||
|
||||
@ -43,6 +43,9 @@ type Group {
|
||||
posts: [Post] @relation(name: "IN", direction: "IN")
|
||||
|
||||
isMutedByMe: Boolean! @cypher(statement: "MATCH (this) RETURN EXISTS( (this)<-[:MUTED]-(:User {id: $cypherParams.currentUserId}) )")
|
||||
|
||||
"inviteCodes to this group the current user has generated"
|
||||
inviteCodes: [InviteCode]! @neo4j_ignore
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -3,16 +3,23 @@ type InviteCode {
|
||||
createdAt: String!
|
||||
generatedBy: User @relation(name: "GENERATED", direction: "IN")
|
||||
redeemedBy: [User] @relation(name: "REDEEMED", direction: "IN")
|
||||
redeemedByCount: Int! @cypher(statement: "MATCH (this)<-[:REDEEMED]-(related:User)")
|
||||
expiresAt: String
|
||||
}
|
||||
comment: String
|
||||
|
||||
invitedTo: Group @neo4j_ignore
|
||||
# invitedFrom: User! @neo4j_ignore # -> see generatedBy
|
||||
|
||||
type Mutation {
|
||||
GenerateInviteCode(expiresAt: String = null): InviteCode
|
||||
isValid: Boolean! @neo4j_ignore
|
||||
}
|
||||
|
||||
type Query {
|
||||
MyInviteCodes: [InviteCode]
|
||||
isValidInviteCode(code: ID!): Boolean
|
||||
getInviteCode: InviteCode
|
||||
validateInviteCode(code: String!): InviteCode
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
generatePersonalInviteCode(expiresAt: String = null, comment: String = null): InviteCode!
|
||||
generateGroupInviteCode(groupId: ID!, expiresAt: String = null, comment: String = null): InviteCode!
|
||||
invalidateInviteCode(code: String!): InviteCode
|
||||
redeemInviteCode(code: String!): Boolean!
|
||||
}
|
||||
|
||||
@ -72,9 +72,6 @@ type User {
|
||||
followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN")
|
||||
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
inviteCodes: [InviteCode] @relation(name: "GENERATED", direction: "OUT")
|
||||
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||
|
||||
# Is the currently logged in user following that user?
|
||||
followedByCurrentUser: Boolean! @cypher(
|
||||
statement: """
|
||||
@ -125,6 +122,7 @@ type User {
|
||||
|
||||
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
|
||||
|
||||
# Badges
|
||||
badgeVerification: Badge! @neo4j_ignore
|
||||
badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||
badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
|
||||
@ -132,6 +130,11 @@ type User {
|
||||
badgeTrophiesUnused: [Badge]! @neo4j_ignore
|
||||
badgeTrophiesUnusedCount: Int! @neo4j_ignore
|
||||
|
||||
"personal inviteCodes the user has generated"
|
||||
inviteCodes: [InviteCode]! @neo4j_ignore
|
||||
# inviteCodes: [InviteCode]! @relation(name: "GENERATED", direction: "OUT")
|
||||
redeemedInviteCode: InviteCode @relation(name: "REDEEMED", direction: "OUT")
|
||||
|
||||
emotions: [EMOTED]
|
||||
|
||||
activeCategories: [String] @cypher(
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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(/&/g, '&'),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendMailCallback = async (templateArgs) => {
|
||||
transporter.use(
|
||||
'compile',
|
||||
htmlToText({
|
||||
ignoreImage: true,
|
||||
wordwrap: false,
|
||||
}),
|
||||
)
|
||||
|
||||
await transporter.sendMail(templateArgs)
|
||||
}
|
||||
}
|
||||
|
||||
export const sendMail = sendMailCallback
|
||||
@ -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,
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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 }),
|
||||
}
|
||||
}
|
||||
@ -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 -->
|
||||
@ -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')
|
||||
@ -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 -->
|
||||
@ -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 -->
|
||||
@ -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')
|
||||
@ -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 -->
|
||||
@ -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')
|
||||
@ -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>
|
||||
@ -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 -->
|
||||
@ -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 -->
|
||||
@ -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 -->
|
||||
@ -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'
|
||||
|
||||
@ -1,42 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import CONFIG from '@config/index'
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getDriver, getNeode } from '@db/neo4j'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
const instance = getNeode()
|
||||
const driver = getDriver()
|
||||
let variables
|
||||
let owner, anotherRegularUser, administrator, moderator
|
||||
|
||||
let query, mutate, variables
|
||||
let authenticatedUser, owner, anotherRegularUser, administrator, moderator
|
||||
const database = databaseContext()
|
||||
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let query, mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => ({
|
||||
driver,
|
||||
instance,
|
||||
user: authenticatedUser,
|
||||
}),
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
})
|
||||
|
||||
// TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543
|
||||
afterEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
@ -109,7 +112,7 @@ describe('authorization', () => {
|
||||
query({ query: userQuery, variables: { name: 'Owner' } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
data: { User: [null] },
|
||||
data: { User: null },
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -242,7 +245,7 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('as anyone', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
@ -267,7 +270,7 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('as anyone with valid invite code', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
email: 'some@email.org',
|
||||
inviteCode: 'ABCDEF',
|
||||
@ -287,7 +290,7 @@ describe('authorization', () => {
|
||||
})
|
||||
|
||||
describe('as anyone without valid invite', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
variables = {
|
||||
email: 'some@email.org',
|
||||
inviteCode: 'no valid invite code',
|
||||
|
||||
@ -9,7 +9,9 @@ import { rule, shield, deny, allow, or, and } from 'graphql-shield'
|
||||
import CONFIG from '@config/index'
|
||||
import SocialMedia from '@db/models/SocialMedia'
|
||||
import { getNeode } from '@db/neo4j'
|
||||
import { validateInviteCode } from '@graphql/resolvers/transactions/inviteCodes'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { validateInviteCode } from '@graphql/resolvers/inviteCodes'
|
||||
import { Context } from '@src/server'
|
||||
|
||||
const debug = !!CONFIG.DEBUG
|
||||
const allowExternalErrors = true
|
||||
@ -370,11 +372,28 @@ const noEmailFilter = rule({
|
||||
|
||||
const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
|
||||
|
||||
const inviteRegistration = rule()(async (_parent, args, { _user, driver }) => {
|
||||
const inviteRegistration = rule()(async (_parent, args, context: Context) => {
|
||||
if (!CONFIG.INVITE_REGISTRATION) return false
|
||||
const { inviteCode } = args
|
||||
const session = driver.session()
|
||||
return validateInviteCode(session, inviteCode)
|
||||
return validateInviteCode(context, inviteCode)
|
||||
})
|
||||
|
||||
const isAllowedToGenerateGroupInviteCode = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_parent, args, context: Context) => {
|
||||
if (!context.user) return false
|
||||
|
||||
return !!(
|
||||
await context.database.query({
|
||||
query: `
|
||||
MATCH (user:User{id: user.id})-[membership:MEMBER_OF]->(group:Group {id: $args.groupId})
|
||||
WHERE (group.type IN ['closed','hidden'] AND membership.role IN ['admin', 'owner'])
|
||||
OR (NOT group.type IN ['closed','hidden'] AND NOT membership.role = 'pending')
|
||||
RETURN count(group) as count
|
||||
`,
|
||||
variables: { user: context.user, args },
|
||||
})
|
||||
).records[0].get('count')
|
||||
})
|
||||
|
||||
// Permissions
|
||||
@ -399,7 +418,7 @@ export default shield(
|
||||
Post: allow,
|
||||
profilePagePosts: allow,
|
||||
Comment: allow,
|
||||
User: or(noEmailFilter, isAdmin),
|
||||
User: and(isAuthenticated, or(noEmailFilter, isAdmin)),
|
||||
Badge: allow,
|
||||
PostsEmotionsCountByEmotion: allow,
|
||||
PostsEmotionsByCurrentUser: isAuthenticated,
|
||||
@ -408,15 +427,15 @@ export default shield(
|
||||
notifications: isAuthenticated,
|
||||
Donations: isAuthenticated,
|
||||
userData: isAuthenticated,
|
||||
MyInviteCodes: isAuthenticated,
|
||||
isValidInviteCode: allow,
|
||||
VerifyNonce: allow,
|
||||
queryLocations: isAuthenticated,
|
||||
availableRoles: isAdmin,
|
||||
getInviteCode: isAuthenticated, // and inviteRegistration
|
||||
Room: isAuthenticated,
|
||||
Message: isAuthenticated,
|
||||
UnreadRooms: isAuthenticated,
|
||||
|
||||
// Invite Code
|
||||
validateInviteCode: allow,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
@ -465,7 +484,13 @@ export default shield(
|
||||
pinPost: isAdmin,
|
||||
unpinPost: isAdmin,
|
||||
UpdateDonations: isAdmin,
|
||||
GenerateInviteCode: isAuthenticated,
|
||||
|
||||
// InviteCode
|
||||
generatePersonalInviteCode: isAuthenticated,
|
||||
generateGroupInviteCode: isAllowedToGenerateGroupInviteCode,
|
||||
invalidateInviteCode: isAuthenticated,
|
||||
redeemInviteCode: isAuthenticated,
|
||||
|
||||
switchUserRole: isAdmin,
|
||||
markTeaserAsViewed: allow,
|
||||
saveCategorySettings: isAuthenticated,
|
||||
@ -480,8 +505,27 @@ export default shield(
|
||||
resetTrophyBadgesSelected: isAuthenticated,
|
||||
},
|
||||
User: {
|
||||
'*': isAuthenticated,
|
||||
name: allow,
|
||||
avatar: allow,
|
||||
email: or(isMyOwn, isAdmin),
|
||||
emailNotificationSettings: isMyOwn,
|
||||
inviteCodes: isMyOwn,
|
||||
},
|
||||
Group: {
|
||||
'*': isAuthenticated, // TODO - only those who are allowed to see the group
|
||||
avatar: allow,
|
||||
name: allow,
|
||||
about: allow,
|
||||
groupType: allow,
|
||||
},
|
||||
InviteCode: {
|
||||
'*': allow,
|
||||
redeemedBy: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
redeemedByCount: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
createdAt: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
expiresAt: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
comment: isAuthenticated, // TODO only for self generated, must be done in resolver
|
||||
},
|
||||
Location: {
|
||||
distanceToMe: isAuthenticated,
|
||||
|
||||
@ -2,47 +2,46 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
import databaseContext from '@context/database'
|
||||
import Factory, { cleanDatabase } from '@db/factories'
|
||||
import { getNeode, getDriver } from '@db/neo4j'
|
||||
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
|
||||
import { createPostMutation } from '@graphql/queries/createPostMutation'
|
||||
import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation'
|
||||
import { updateGroupMutation } from '@graphql/queries/updateGroupMutation'
|
||||
import createServer from '@src/server'
|
||||
import createServer, { getContext } from '@src/server'
|
||||
|
||||
let authenticatedUser
|
||||
let variables
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
const descriptionAdditional100 =
|
||||
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
|
||||
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
const database = databaseContext()
|
||||
|
||||
const { mutate } = createTestClient(server)
|
||||
let server: ApolloServer
|
||||
let authenticatedUser
|
||||
let mutate
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanDatabase()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
|
||||
const contextUser = async (_req) => authenticatedUser
|
||||
const context = getContext({ user: contextUser, database })
|
||||
|
||||
server = createServer({ context }).server
|
||||
|
||||
const createTestClientResult = createTestClient(server)
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDatabase()
|
||||
await driver.close()
|
||||
afterAll(() => {
|
||||
void server.stop()
|
||||
void database.driver.close()
|
||||
database.neode.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@ -19,6 +19,7 @@ import pubsubContext from '@context/pubsub'
|
||||
import CONFIG from './config'
|
||||
import schema from './graphql/schema'
|
||||
import decode from './jwt/decode'
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import middleware from './middleware'
|
||||
|
||||
const serverDatabase = databaseContext()
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -30,6 +30,8 @@ services:
|
||||
environment:
|
||||
- NODE_ENV="development"
|
||||
- DEBUG=true
|
||||
- SMTP_PORT=1025
|
||||
- SMTP_HOST=mailserver
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user