Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social into brand-reformer-network-first-step

# Conflicts:
#	webapp/constants/links.js
#	webapp/locales/de.json
#	webapp/locales/en.json
This commit is contained in:
Wolfgang Huß 2025-05-08 09:39:34 +02:00
commit c6923893ba
390 changed files with 14352 additions and 7952 deletions

View File

@ -57,19 +57,10 @@ updates:
applies-to: version-updates
patterns:
- "*apollo-server*"
babel:
applies-to: version-updates
patterns:
- "@babel*"
metascraper:
applies-to: version-updates
patterns:
- "metascraper*"
typescript:
applies-to: version-updates
patterns:
- "ts*"
- "*types?"
# webapp
- package-ecosystem: docker

View File

@ -3,8 +3,62 @@ name: ocelot.social end-to-end test CI
on: push
jobs:
docker_preparation:
name: Fullstack test preparation
prepare_neo4j_image:
name: Fullstack | prepare neo4j image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Build docker image
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
- name: Cache docker image
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-e2e-neo4j-cache
prepare_backend_image:
name: Fullstack | prepare backend image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Build docker image
run: |
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
- name: Cache docker image
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-e2e-backend-cache
prepare_webapp_image:
name: Fullstack | prepare webapp image
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Build docker image
run: |
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
- name: Cache docker image
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-e2e-webapp-cache
prepare_cypress:
name: Fullstack | prepare cypress
runs-on: ubuntu-latest
steps:
- name: Checkout code
@ -13,18 +67,7 @@ jobs:
- name: Copy env files
run: |
cp webapp/.env.template webapp/.env
cp frontend/.env.dist frontend/.env
cp backend/.env.template backend/.env
- name: Build docker images
run: |
mkdir /tmp/images
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar
cp backend/.env.test_e2e backend/.env
- name: Install cypress requirements
run: |
@ -35,21 +78,20 @@ jobs:
cd ..
yarn install
- name: Cache docker images
id: cache
- name: Cache docker image
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: ${{ github.run_id }}-e2e-preparation-cache
key: ${{ github.run_id }}-e2e-cypress
fullstack_tests:
name: Fullstack tests
name: Fullstack | tests
if: success()
needs: docker_preparation
needs: [prepare_neo4j_image, prepare_backend_image, prepare_webapp_image, prepare_cypress]
runs-on: ubuntu-latest
env:
jobs: 8
@ -58,26 +100,42 @@ jobs:
# run copies of the current job in parallel
job: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- name: Restore cache
- name: Restore cypress cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
id: cache
with:
path: |
/opt/cucumber-json-formatter
/home/runner/.cache/Cypress
/home/runner/work/Ocelot-Social/Ocelot-Social
/tmp/images/
key: ${{ github.run_id }}-e2e-preparation-cache
fail-on-cache-miss: true
key: ${{ github.run_id }}-e2e-cypress
restore-keys: ${{ github.run_id }}-e2e-cypress
- name: Restore neo4j cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/neo4j.tar
key: ${{ github.run_id }}-e2e-neo4j-cache
- name: Restore backend cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/backend.tar
key: ${{ github.run_id }}-e2e-backend-cache
- name: Restore webapp cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.0.2
with:
path: /tmp/webapp.tar
key: ${{ github.run_id }}-e2e-webapp-cache
- name: Boot up test system | docker compose
run: |
chmod +x /opt/cucumber-json-formatter
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
docker load < /tmp/images/neo4j.tar
docker load < /tmp/images/backend.tar
docker load < /tmp/images/webapp.tar
docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend --build
docker load < /tmp/neo4j.tar
docker load < /tmp/backend.tar
docker load < /tmp/webapp.tar
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build --detach --no-deps webapp neo4j backend mailserver
sleep 90s
- name: Full stack tests | run tests
@ -98,17 +156,24 @@ jobs:
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
cleanup:
name: Cleanup
needs: [docker_preparation, fullstack_tests]
cleanup_cache:
name: Cleanup Cache
needs: fullstack_tests
runs-on: ubuntu-latest
permissions: write-all
continue-on-error: true
steps:
- name: Delete cache
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Full stack tests | cleanup cache
run: |
gh extension install actions/gh-actions-cache
KEY="${{ github.run_id }}-e2e-preparation-cache"
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
cacheKeys=$(gh cache list --json key --jq '.[] | select(.key | startswith("${{ github.run_id }}-e2e-")) | .key')
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeys
do
gh cache delete "$cacheKey"
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: amannn/action-semantic-pull-request@04501d43b574e4c1d23c629ffe4dcec27acfdeff # v5.5.3
- uses: amannn/action-semantic-pull-request@335288255954904a41ddda8947c8f2c844b8bfeb # v5.5.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@ -4,8 +4,63 @@ 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)
> 6 May 2025
- v3.5.1 [`#8496`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8496)
- fix emails in production [`#8493`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8493)
#### [3.5.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.4.0...3.5.0)
> 6 May 2025
- v3.5.0 [`#8492`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8492)
- feat(webapp): user teaser popover [`#8450`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8450)
- feat(backend): signup email localized [`#8459`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8459)
- lint json [`#8472`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8472)
- refactor(other): cypress: simplify cucumber preprocessor imports and some linting [`#8489`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8489)
- fix backend node23 [`#8488`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8488)
- refactor(workflow): parallelize e2e preparation [`#8481`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8481)
- refactor(backend): types for context + `slug` [`#8486`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8486)
- feat(backend): emails for notifications [`#8435`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8435)
- remove some dependabot groups & no alpine version to allow update [`#8475`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8475)
- build(deps-dev): bump the babel group with 3 updates [`#8478`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8478)
- build(deps-dev): bump @types/node from 22.15.2 to 22.15.3 in /backend [`#8479`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8479)
- refactor(backend): refactor context [`#8434`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8434)
- build(deps): bump amannn/action-semantic-pull-request [`#8480`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8480)
- build(deps-dev): bump eslint-plugin-prettier in /webapp [`#8332`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8332)
- remove all helpers on src/helpers [`#8469`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8469)
- move models into database folder [`#8471`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8471)
- also lint cjs files [`#8467`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8467)
- refactor(backend): refactor badges [`#8465`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8465)
- refactor(backend): move resolvers into graphql folder [`#8470`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8470)
- refactor(webapp): remove unused packages [`#8468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8468)
- refactor(backend): remove unused packages [`#8466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8466)
- fix(backend): fix backend dev and dev:debug command [`#8439`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8439)
- move distanceToMe onto Location [`#8464`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8464)
- feat(backend): distanceToMe [`#8462`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8462)
- fix(webapp): fixed padding for mobile in basic layout [`#8455`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8455)
- Fix ocelot.social link for imprint and donation [`#8461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8461)
#### [3.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.3.0...3.4.0)
> 28 April 2025
- v3.4.0 [`#8454`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8454)
- fix(webapp): fix badge focus [`#8452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8452)
- feat(backend): branding middlewares [`#8429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8429)
- refactor(webapp): make login, registration, password-reset layout brandable [`#8440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8440)
@ -1383,15 +1438,31 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- updated CHANGELOG.md [`9d9075f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d9075f2117b2eb4b607e7d59ab18c7e655c6ea7)
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...0.6.4)
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.4...0.6.4)
> 8 February 2021
- regenerated `CHANGELOG.md` [`ee688ec`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ee688ece24cf592b3989e83340701ca8772e876e)
- fetch full history [`5ecee4d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5ecee4d73a92d2e5c5ae971d79848ed27f65a72c)
- don't fail if tag exists (release) [`39c82fc`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/39c82fcb37d5c8e7e78a79288e1ef6280f8d0892)
- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
- - first steps towards docker image deployment & github autotagging [`5503216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5503216ad4a0230ac533042e4a69806590fc2a5a)
- - deploy structure image [`a60400b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a60400b4fe6f59bbb80e1073db4def3ba205e1a7)
#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...0.6.3)
#### [v0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...v0.6.4)
> 9 February 2021
- chore(release): 0.6.4 [`8b7570d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8b7570dc35d0ea431f673a711ac051f1e1320acb)
- change user roles is working, test fails [`8c3310a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8c3310abaf87c0e5597fec4f93fb37d27122c9e7)
- change user role: tests are working [`f10da4b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f10da4b09388fe1e2b85abd53f6ffc67c785d4c1)
#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.3...0.6.3)
> 8 February 2021
- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
- - fixed changelog [`cf70b12`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/cf70b12ed74011924ea788ab932fc9d7ac0e6bd9)
- - yarn install to allow yarn auto-changelog [`fc496aa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc496aa04cb7e804da4335da0cb5cda26f874ea2)
#### [v0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...v0.6.3)
> 8 February 2021

View File

@ -1,2 +0,0 @@
coverage:
range: "60...100"

41
backend/.env.test_e2e Normal file
View File

@ -0,0 +1,41 @@
DEBUG=true
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=letmein
GRAPHQL_URI=http://localhost:4000
CLIENT_URI=http://localhost:3000
# E-Mail default settings
EMAIL_SUPPORT="devops@ocelot.social"
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
SMTP_HOST=mailserver
SMTP_PORT=1025
SMTP_IGNORE_TLS=true
SMTP_MAX_CONNECTIONS=5
SMTP_MAX_MESSAGES=Infinity
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_SECURE="false" # true for 465, false for other ports
SMTP_DKIM_DOMAINNAME=
SMTP_DKIM_KEYSELECTOR=
SMTP_DKIM_PRIVATKEY=
JWT_SECRET="b/&&7b78BF&fv/Vd"
JWT_EXPIRES="2y"
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
SENTRY_DSN_BACKEND=
COMMIT=
PUBLIC_REGISTRATION=false
INVITE_REGISTRATION=true
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT=
AWS_REGION=
AWS_BUCKET=
CATEGORIES_ACTIVE=false

View File

@ -133,7 +133,7 @@ module.exports = {
'error',
{ allowModules: ['apollo-server-testing', 'rosie', '@faker-js/faker', 'ts-jest'] },
], // part of n/recommended
// 'n/no-unpublished-require': 'error', // part of n/recommended
'n/no-unpublished-require': ['error', { allowModules: ['ts-jest', 'require-json5'] }], // part of n/recommended
// 'n/no-unsupported-features/es-builtins': 'error', // part of n/recommended
// 'n/no-unsupported-features/es-syntax': 'error', // part of n/recommended
// 'n/no-unsupported-features/node-builtins': 'error', // part of n/recommended
@ -204,7 +204,6 @@ module.exports = {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
@ -224,5 +223,10 @@ module.exports = {
'jest/unbound-method': 'error',
},
},
{
extends: ['plugin:jsonc/recommended-with-jsonc'],
files: ['*.json', '*.json5', '*.jsonc'],
parser: 'jsonc-eslint-parser',
},
],
}

View File

@ -1,3 +0,0 @@
{
"schemaPath": "./src/schema.graphql"
}

View File

@ -1,4 +1,4 @@
FROM node:20.12.1-alpine3.19 AS base
FROM node:23.11.0-alpine AS base
LABEL org.label-schema.name="ocelot.social:backend"
LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social"
LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
@ -10,7 +10,7 @@ LABEL maintainer="devops@ocelot.social"
ENV NODE_ENV="production"
ENV PORT="4000"
EXPOSE ${PORT}
RUN apk --no-cache add git python3 make g++ bash
RUN apk --no-cache add git python3 make g++ bash linux-headers
RUN mkdir -p /app
WORKDIR /app
CMD ["/bin/bash", "-c", "yarn run start"]

View File

@ -1,15 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "10"
}
}
]
],
"plugins": [
"@babel/plugin-proposal-throw-expressions"
]
}

View File

@ -1,6 +1,7 @@
/* eslint-disable import/no-commonjs */
const { pathsToModuleNameMapper } = require('ts-jest')
const requireJSON5 = require('require-json5')
const { pathsToModuleNameMapper } = require('ts-jest')
const { compilerOptions } = requireJSON5('./tsconfig.json')
module.exports = {

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "3.4.0",
"version": "3.5.3",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -10,9 +10,9 @@
"scripts": {
"start": "node build/src/",
"build": "tsc && tsc-alias && ./scripts/build.copy.files.sh",
"dev": "nodemon --exec ts-node --require tsconfig-paths/register src/ -e js,ts,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql",
"lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts .",
"dev": "nodemon --exec ts-node --require tsconfig-paths/register src/index.ts -e js,ts,gql",
"dev:debug": "nodemon --exec node --inspect=0.0.0.0:9229 build/src/index.js -e js,ts,gql",
"lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts,.cjs,.json,.json5,.jsonc .",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles",
"db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts",
"db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts",
@ -27,30 +27,16 @@
"prod:db:data:branding": "node build/src/db/data-branding.js"
},
"dependencies": {
"@babel/cli": "~7.27.0",
"@babel/core": "^7.26.10",
"@babel/node": "~7.26.0",
"@babel/plugin-proposal-throw-expressions": "^7.25.9",
"@babel/preset-env": "~7.26.9",
"@babel/register": "^7.23.7",
"@sentry/node": "^5.15.4",
"apollo-cache-inmemory": "~1.6.6",
"apollo-client": "~2.6.10",
"apollo-link-context": "~1.0.20",
"apollo-link-http": "~1.5.17",
"apollo-server": "~2.14.2",
"apollo-server-express": "^2.14.2",
"aws-sdk": "^2.1692.0",
"babel-core": "~7.0.0-0",
"babel-eslint": "~10.1.0",
"babel-jest": "~29.7.0",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~3.0.2",
"body-parser": "^1.20.3",
"cheerio": "~1.0.0",
"cors": "~2.8.5",
"cross-env": "~7.0.3",
"dotenv": "~16.5.0",
"email-templates": "^12.0.2",
"express": "^5.1.0",
"graphql": "^14.6.0",
"graphql-middleware": "~4.0.2",
@ -92,10 +78,11 @@
"node-fetch": "^2.7.0",
"nodemailer": "^6.10.1",
"nodemailer-html-to-text": "^3.2.0",
"preview-email": "^3.1.0",
"pug": "^3.0.3",
"request": "~2.88.2",
"sanitize-html": "~2.16.0",
"slug": "~9.1.0",
"subscriptions-transport-ws": "^0.9.19",
"trunc-html": "~1.1.2",
"uuid": "~9.0.1",
"validator": "^13.15.0",
@ -104,9 +91,11 @@
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
"@faker-js/faker": "9.7.0",
"@types/email-templates": "^10.0.4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.16",
"@types/node": "^22.15.2",
"@types/node": "^22.15.3",
"@types/slug": "^5.0.9",
"@types/uuid": "~9.0.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
@ -117,6 +106,7 @@
"eslint-import-resolver-typescript": "^4.3.4",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsonc": "^2.20.0",
"eslint-plugin-n": "^17.17.0",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.2.6",
@ -135,7 +125,10 @@
},
"resolutions": {
"**/**/fs-capacitor": "^6.2.0",
"**/graphql-upload": "^11.0.0"
"**/graphql-upload": "^11.0.0",
"**/strip-ansi": "6.0.1",
"**/string-width": "4.2.0",
"**/wrap-ansi": "7.0.0"
},
"engines": {
"node": ">=20.12.1"

View File

@ -15,7 +15,8 @@
<path
fill="#333"
d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"
id="path1" />
id="path1"
style="fill:#868383;fill-opacity:1" />
<g
fill="#ffffff"
id="g2"

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1 +1,45 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><path d="M35.01 367.726c-.08-21.169-.205-53.162 21.257-71.332 3.817-3.253 9.93-7.497 17.321-9.224 2.575-.523 4.956-.756 7.262-.979 4.438-.431 8.27-.804 12.054-2.9l4.954-2.846c9.87-5.655 19.194-10.996 28.226-17.377 5.085-3.632 6.726-15.73 6.095-25.428-.214-2.792-1.893-5.7-3.67-8.777-1.097-1.901-2.232-3.867-3.065-5.916l-.073-.199a56.976 56.976 0 0 1-.422-1.443c-1.195-4.205-1.933-6.378-2.386-7.476-7.029-.944-11.8-8.647-12.888-21.006l-.031-.557c-.645-12.785.808-16.13 2.316-17.716.24-.254.505-.475.783-.666-1.754-16.051 3.115-32.521 13.358-44.704 9.314-11.079 21.955-17.18 35.592-17.18 3.73 0 7.55.458 11.355 1.362 25.63 6.228 41.679 30.27 40.062 59.227.53.251 1.018.61 1.44 1.066 2.752 2.964 2.47 10.97 2.22 14.276l-.024.41c-.335 5.236-.684 10.65-3.052 15.73-1.739 3.918-4.405 6.242-6.76 8.29-2.396 2.089-4.288 3.735-5.294 6.885-.7 2.416-1.645 4.866-2.559 7.235-1.752 4.538-3.407 8.827-3.54 13.244-.427 10.222 1.17 18.391 4.172 21.359 5.097 5.163 13.003 9.391 19.978 13.121 1.6.855 3.166 1.692 4.654 2.517 9.28 5.052 16.07 7.915 25.309 8.557 9.118.849 18.056 5.193 24.754 11.97.736.641 1.82 1.744 3.694 3.648 4.416 4.492 4.416 4.492 4.426 5.852l.007.758c10.783 17.702 11.14 40.656 11.415 58.169l.05 3.28-3.278.028c-42.05.363-84.058.677-126.058.993-42.12.314-84.232.632-126.367.994l-3.273.029-.014-3.274zM329.011 135.763a5.232 5.232 0 0 0-5.223 5.23 5.232 5.232 0 0 0 5.223 5.23 5.236 5.236 0 0 0 5.231-5.23 5.236 5.236 0 0 0-5.23-5.23m0 40.237C309.705 176 294 160.297 294 140.993 294 121.698 309.706 106 329.011 106 348.303 106 364 121.698 364 140.993 364 160.297 348.303 176 329.011 176" fill="#FFF"/><path d="M330.511 101C308.173 101 290 119.164 290 141.492 290 163.828 308.173 182 330.511 182 352.836 182 371 163.828 371 141.492 371 119.164 352.836 101 330.511 101m0 51.022c5.823 0 10.531-4.716 10.531-10.53a10.517 10.517 0 0 0-10.53-10.529 10.51 10.51 0 0 0-10.523 10.529c0 5.814 4.7 10.53 10.522 10.53m0-40.496c16.563 0 29.963 13.406 29.963 29.966 0 16.555-13.4 29.982-29.963 29.982-16.555 0-29.985-13.427-29.985-29.982 0-16.56 13.43-29.966 29.985-29.966" fill="#AD245D"/><path d="M331 106.209c-20.305 0-36.825 16.06-36.825 35.799 0 19.747 16.52 35.813 36.825 35.813 20.306 0 36.827-16.066 36.827-35.813 0-19.74-16.521-35.8-36.827-35.8zM314.287 215l-4.11-21.345c-.324-.129-.648-.265-.972-.404l-18.012 12.169-23.607-23.609 12.186-18.009a63.31 63.31 0 0 1-.403-.968L258 158.712v-33.383l21.361-4.13c.131-.327.267-.652.407-.979l-12.18-18.025 23.608-23.612 18.015 12.198c.322-.137.643-.27.964-.4L314.287 69h33.416l4.13 21.387c.319.13.638.26.956.396l18.024-12.2 23.608 23.612-12.186 18.031c.139.324.273.648.402.971L404 125.33v33.381l-21.37 4.124c-.13.32-.262.64-.398.96l12.19 18.017-23.606 23.609-18.021-12.171c-.32.137-.642.27-.964.402L347.701 215h-33.414z" fill="#FFF"/><path d="M330 171.448c-17.342 0-31.45-13.656-31.45-30.44 0-16.778 14.108-30.427 31.45-30.427 17.341 0 31.449 13.649 31.449 30.426 0 16.785-14.108 30.441-31.45 30.441zM350.979 63h-41.97l-1.64 8.517-1.953 10.156-8.55-5.788-7.18-4.862-6.132 6.132-17.399 17.4-6.126 6.126 4.85 7.178 5.8 8.583-10.173 1.966-8.506 1.646v41.932l8.51 1.643 10.16 1.962-5.787 8.553-4.858 7.179 6.13 6.13 17.399 17.399 6.126 6.126 7.177-4.85 8.56-5.782 1.954 10.141 1.64 8.513H350.975l1.645-8.507 1.964-10.15 8.566 5.787 7.178 4.846 6.124-6.124 17.4-17.399 6.13-6.13-4.858-7.18-5.788-8.554 10.153-1.96 8.51-1.643v-41.932l-8.507-1.646-10.165-1.965 5.8-8.584 4.85-7.178-6.125-6.126-17.4-17.4-6.13-6.13-7.18 4.858-8.558 5.792-1.964-10.166L350.98 63zm-20.98 118.948c23.176 0 41.95-18.318 41.95-40.94 0-22.607-18.774-40.927-41.95-40.927-23.174 0-41.948 18.32-41.948 40.926 0 22.623 18.774 40.941 41.949 40.941zM342.313 73.5l3.855 19.963a47.184 47.184 0 0 1 6.037 2.502l16.824-11.386 17.4 17.4-11.362 16.818a53.171 53.171 0 0 1 2.502 6.066l19.932 3.855v24.601l-19.932 3.848a56.644 56.644 0 0 1-2.502 6.066l11.362 16.795-17.4 17.4-16.824-11.364a44.931 44.931 0 0 1-6.037 2.504l-3.855 19.932H317.68l-3.84-19.932a43.821 43.821 0 0 1-6.043-2.504l-16.818 11.364-17.4-17.4 11.364-16.795a53.759 53.759 0 0 1-2.51-6.066l-19.933-3.848v-24.601l19.933-3.855a50.617 50.617 0 0 1 2.51-6.066l-11.364-16.818 17.4-17.4 16.818 11.386a45.957 45.957 0 0 1 6.043-2.502l3.84-19.963h24.632z" fill="#AD245D"/></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="513"
height="444"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5" />
<g
fill="none"
fill-rule="evenodd"
id="g5">
<path
fill="#ad245d"
d="M 384.5,0.297 512.325,221.9 384.325,443.602 128.5,443.704 0.675,222.1 128.675,0.4 Z"
id="path1"
style="display:inline;fill:#e67919;fill-opacity:1" />
<path
d="m 83.051688,348.226 c -0.08,-21.169 -0.205,-53.162 21.257002,-71.332 3.817,-3.253 9.93,-7.497 17.321,-9.224 2.575,-0.523 4.956,-0.756 7.262,-0.979 4.438,-0.431 8.27,-0.804 12.054,-2.9 l 4.954,-2.846 c 9.87,-5.655 19.194,-10.996 28.226,-17.377 5.085,-3.632 6.726,-15.73 6.095,-25.428 -0.214,-2.792 -1.893,-5.7 -3.67,-8.777 -1.097,-1.901 -2.232,-3.867 -3.065,-5.916 l -0.073,-0.199 a 56.976,56.976 0 0 1 -0.422,-1.443 c -1.195,-4.205 -1.933,-6.378 -2.386,-7.476 -7.029,-0.944 -11.8,-8.647 -12.888,-21.006 l -0.031,-0.557 c -0.645,-12.785 0.808,-16.13 2.316,-17.716 0.24,-0.254 0.505,-0.475 0.783,-0.666 -1.754,-16.051 3.115,-32.521 13.358,-44.704 9.314,-11.079 21.955,-17.18 35.592,-17.18 3.73,0 7.55,0.458 11.355,1.362 25.63,6.228 41.679,30.27 40.062,59.227 0.53,0.251 1.018,0.61 1.44,1.066 2.752,2.964 2.47,10.97 2.22,14.276 l -0.024,0.41 c -0.335,5.236 -0.684,10.65 -3.052,15.73 -1.739,3.918 -4.405,6.242 -6.76,8.29 -2.396,2.089 -4.288,3.735 -5.294,6.885 -0.7,2.416 -1.645,4.866 -2.559,7.235 -1.752,4.538 -3.407,8.827 -3.54,13.244 -0.427,10.222 1.17,18.391 4.172,21.359 5.097,5.163 13.003,9.391 19.978,13.121 1.6,0.855 3.166,1.692 4.654,2.517 9.28,5.052 16.07,7.915 25.309,8.557 9.118,0.849 18.056,5.193 24.754,11.97 0.736,0.641 1.82,1.744 3.694,3.648 4.416,4.492 4.416,4.492 4.426,5.852 l 0.007,0.758 c 10.783,17.702 11.14,40.656 11.415,58.169 l 0.05,3.28 -3.278,0.028 c -42.05,0.363 -84.058,0.677 -126.058,0.993 -42.12,0.314 -84.232,0.632 -126.367002,0.994 l -3.273,0.029 z"
id="path2-5"
style="display:inline;fill:#ffffff" />
<path
d="m 351.97006,182.43811 c -2.88526,0.005 -5.2219,2.34474 -5.223,5.23 0.001,2.88526 2.33774,5.22504 5.223,5.23 2.88747,-0.003 5.22769,-2.34253 5.231,-5.23 -0.003,-2.88708 -2.34292,-5.22669 -5.23,-5.23 m 0,40.237 c -19.307,0 -35.012,-15.703 -35.012,-35.007 0,-19.295 15.706,-34.993 35.011,-34.993 19.292,0 34.989,15.698 34.989,34.993 0,19.304 -15.697,35.007 -34.989,35.007"
fill="#ffffff"
id="path2"
style="display:inline" />
<path
d="m 351.97006,147.67511 c -22.338,0 -40.511,18.164 -40.511,40.492 0,22.336 18.173,40.508 40.511,40.508 22.325,0 40.489,-18.172 40.489,-40.508 0,-22.328 -18.164,-40.492 -40.489,-40.492 m 0,51.022 c 5.823,0 10.531,-4.716 10.531,-10.53 a 10.517,10.517 0 0 0 -10.53,-10.529 10.51,10.51 0 0 0 -10.523,10.529 c 0,5.814 4.7,10.53 10.522,10.53 m 0,-40.496 c 16.563,0 29.963,13.406 29.963,29.966 0,16.555 -13.4,29.982 -29.963,29.982 -16.555,0 -29.985,-13.427 -29.985,-29.982 0,-16.56 13.43,-29.966 29.985,-29.966"
fill="#ad245d"
id="path3"
style="display:inline;fill:#e67919;fill-opacity:1" />
<path
d="m 351.95906,152.88411 c -20.305,0 -36.825,16.06 -36.825,35.799 0,19.747 16.52,35.813 36.825,35.813 20.306,0 36.827,-16.066 36.827,-35.813 0,-19.74 -16.521,-35.8 -36.827,-35.8 z m -16.713,108.791 -4.11,-21.345 c -0.324,-0.129 -0.648,-0.265 -0.972,-0.404 l -18.012,12.169 -23.607,-23.609 12.186,-18.009 a 63.31,63.31 0 0 1 -0.403,-0.968 l -21.369,-4.122 v -33.383 l 21.361,-4.13 c 0.131,-0.327 0.267,-0.652 0.407,-0.979 l -12.18,-18.025 23.608,-23.612 18.015,12.198 c 0.322,-0.137 0.643,-0.27 0.964,-0.4 l 4.112,-21.381 h 33.416 l 4.13,21.387 c 0.319,0.13 0.638,0.26 0.956,0.396 l 18.024,-12.2 23.608,23.612 -12.186,18.031 c 0.139,0.324 0.273,0.648 0.402,0.971 l 21.363,4.133 v 33.381 l -21.37,4.124 c -0.13,0.32 -0.262,0.64 -0.398,0.96 l 12.19,18.017 -23.606,23.609 -18.021,-12.171 c -0.32,0.137 -0.642,0.27 -0.964,0.402 l -4.13,21.348 z"
fill="#ffffff"
id="path4"
style="display:inline" />
<path
d="m 351.95956,218.12311 c -17.342,0 -31.45,-13.656 -31.45,-30.44 0,-16.778 14.108,-30.427 31.45,-30.427 17.341,0 31.449,13.649 31.449,30.426 0,16.785 -14.108,30.441 -31.45,30.441 z m 20.979,-108.448 h -41.97 l -1.64,8.517 -1.953,10.156 -8.55,-5.788 -7.18,-4.862 -6.132,6.132 -17.399,17.4 -6.126,6.126 4.85,7.178 5.8,8.583 -10.173,1.966 -8.506,1.646 v 41.932 l 8.51,1.643 10.16,1.962 -5.787,8.553 -4.858,7.179 6.13,6.13 17.399,17.399 6.126,6.126 7.177,-4.85 8.56,-5.782 1.954,10.141 1.64,8.513 h 41.964 l 1.645,-8.507 1.964,-10.15 8.566,5.787 7.178,4.846 6.124,-6.124 17.4,-17.399 6.13,-6.13 -4.858,-7.18 -5.788,-8.554 10.153,-1.96 8.51,-1.643 v -41.932 l -8.507,-1.646 -10.165,-1.965 5.8,-8.584 4.85,-7.178 -6.125,-6.126 -17.4,-17.4 -6.13,-6.13 -7.18,4.858 -8.558,5.792 -1.964,-10.166 -1.64,-8.509 z m -20.98,118.948 c 23.176,0 41.95,-18.318 41.95,-40.94 0,-22.607 -18.774,-40.927 -41.95,-40.927 -23.174,0 -41.948,18.32 -41.948,40.926 0,22.623 18.774,40.941 41.949,40.941 z m 12.314,-108.448 3.855,19.963 a 47.184,47.184 0 0 1 6.037,2.502 l 16.824,-11.386 17.4,17.4 -11.362,16.818 a 53.171,53.171 0 0 1 2.502,6.066 l 19.932,3.855 v 24.601 l -19.932,3.848 a 56.644,56.644 0 0 1 -2.502,6.066 l 11.362,16.795 -17.4,17.4 -16.824,-11.364 a 44.931,44.931 0 0 1 -6.037,2.504 l -3.855,19.932 h -24.633 l -3.84,-19.932 a 43.821,43.821 0 0 1 -6.043,-2.504 l -16.818,11.364 -17.4,-17.4 11.364,-16.795 a 53.759,53.759 0 0 1 -2.51,-6.066 l -19.933,-3.848 v -24.601 l 19.933,-3.855 a 50.617,50.617 0 0 1 2.51,-6.066 l -11.364,-16.818 17.4,-17.4 16.818,11.386 a 45.957,45.957 0 0 1 6.043,-2.502 l 3.84,-19.963 h 24.632 z"
fill="#ad245d"
id="path5"
style="display:inline;fill:#e67919;fill-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -1 +1,38 @@
<svg width="513" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"/><g fill="#FFF"><path d="M35.01 367.726c-.08-21.169-.205-53.162 21.257-71.332 3.817-3.253 9.93-7.497 17.321-9.224 2.575-.523 4.956-.756 7.262-.979 4.438-.431 8.27-.804 12.054-2.9l4.954-2.846c9.87-5.655 19.194-10.996 28.226-17.377 5.085-3.632 6.726-15.73 6.095-25.428-.214-2.792-1.893-5.7-3.67-8.777-1.097-1.901-2.232-3.867-3.065-5.916l-.073-.199a56.976 56.976 0 0 1-.422-1.443c-1.195-4.205-1.933-6.378-2.386-7.476-7.029-.944-11.8-8.647-12.888-21.006l-.031-.557c-.645-12.785.808-16.13 2.316-17.716.24-.254.505-.475.783-.666-1.754-16.051 3.115-32.521 13.358-44.704 9.314-11.079 21.955-17.18 35.592-17.18 3.73 0 7.55.458 11.355 1.362 25.63 6.228 41.679 30.27 40.062 59.227.53.251 1.018.61 1.44 1.066 2.752 2.964 2.47 10.97 2.22 14.276l-.024.41c-.335 5.236-.684 10.65-3.052 15.73-1.739 3.918-4.405 6.242-6.76 8.29-2.396 2.089-4.288 3.735-5.294 6.885-.7 2.416-1.645 4.866-2.559 7.235-1.752 4.538-3.407 8.827-3.54 13.244-.427 10.222 1.17 18.391 4.172 21.359 5.097 5.163 13.003 9.391 19.978 13.121 1.6.855 3.166 1.692 4.654 2.517 9.28 5.052 16.07 7.915 25.309 8.557 9.118.849 18.056 5.193 24.754 11.97.736.641 1.82 1.744 3.694 3.648 4.416 4.492 4.416 4.492 4.426 5.852l.007.758c10.783 17.702 11.14 40.656 11.415 58.169l.05 3.28-3.278.028c-42.05.363-84.058.677-126.058.993-42.12.314-84.232.632-126.367.994l-3.273.029-.014-3.274z"/><text font-family="Impact" font-size="118" font-style="condensed" font-weight="700" transform="translate(1 -1)"><tspan x="256" y="208">&lt;/&gt;</tspan></text></g></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="513"
height="444"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
fill="none"
fill-rule="evenodd"
id="g3">
<path
fill="#AD245D"
d="M384.5.297L512.325 221.9l-128 221.702-255.825.102L.675 222.1 128.675.4z"
id="path1"
style="fill:#e67919;fill-opacity:1" />
<path
d="m 80.877371,348.226 c -0.08,-21.169 -0.205,-53.162 21.256999,-71.332 3.817,-3.253 9.93,-7.497 17.321,-9.224 2.575,-0.523 4.956,-0.756 7.262,-0.979 4.438,-0.431 8.27,-0.804 12.054,-2.9 l 4.954,-2.846 c 9.87,-5.655 19.194,-10.996 28.226,-17.377 5.085,-3.632 6.726,-15.73 6.095,-25.428 -0.214,-2.792 -1.893,-5.7 -3.67,-8.777 -1.097,-1.901 -2.232,-3.867 -3.065,-5.916 l -0.073,-0.199 a 56.976,56.976 0 0 1 -0.422,-1.443 c -1.195,-4.205 -1.933,-6.378 -2.386,-7.476 -7.029,-0.944 -11.8,-8.647 -12.888,-21.006 l -0.031,-0.557 c -0.645,-12.785 0.808,-16.13 2.316,-17.716 0.24,-0.254 0.505,-0.475 0.783,-0.666 -1.754,-16.051 3.115,-32.521 13.358,-44.704 9.314,-11.079 21.955,-17.18 35.592,-17.18 3.73,0 7.55,0.458 11.355,1.362 25.63,6.228 41.679,30.27 40.062,59.227 0.53,0.251 1.018,0.61 1.44,1.066 2.752,2.964 2.47,10.97 2.22,14.276 l -0.024,0.41 c -0.335,5.236 -0.684,10.65 -3.052,15.73 -1.739,3.918 -4.405,6.242 -6.76,8.29 -2.396,2.089 -4.288,3.735 -5.294,6.885 -0.7,2.416 -1.645,4.866 -2.559,7.235 -1.752,4.538 -3.407,8.827 -3.54,13.244 -0.427,10.222 1.17,18.391 4.172,21.359 5.097,5.163 13.003,9.391 19.978,13.121 1.6,0.855 3.166,1.692 4.654,2.517 9.28,5.052 16.07,7.915 25.309,8.557 9.118,0.849 18.056,5.193 24.754,11.97 0.736,0.641 1.82,1.744 3.694,3.648 4.416,4.492 4.416,4.492 4.426,5.852 l 0.007,0.758 c 10.783,17.702 11.14,40.656 11.415,58.169 l 0.05,3.28 -3.278,0.028 c -42.05,0.363 -84.058,0.677 -126.058,0.993 -42.12,0.314 -84.232,0.632 -126.366999,0.994 l -3.273,0.029 z"
id="path2-5"
style="display:inline;fill:#ffffff" />
<text
font-family="Impact"
font-size="118px"
font-style="condensed"
font-weight="700"
id="text2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:96px;font-family:Monospace;-inkscape-font-specification:Monospace;display:inline;fill:#ffffff"
x="6.8672509"
y="17"><tspan
x="262.86725"
y="225"
id="tspan2"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:96px;font-family:Monospace;-inkscape-font-specification:Monospace;fill:#ffffff">&lt;/&gt;</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1 +1,29 @@
<svg width="512" height="444" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#AD245D" d="M384 .297L511.392 221.65l-128 221.702-255.392.352L.608 222.35 128.608.65z"/><g fill="#FFF"><path d="M34.944 367.726c-.081-21.169-.205-53.162 21.215-71.332 3.81-3.253 9.91-7.497 17.288-9.224 2.57-.523 4.946-.756 7.247-.979 4.43-.431 8.254-.804 12.03-2.9l4.945-2.846c9.851-5.655 19.157-10.996 28.171-17.377 5.075-3.632 6.713-15.73 6.082-25.428-.213-2.792-1.888-5.7-3.662-8.777-1.095-1.901-2.228-3.867-3.059-5.916l-.073-.199a57.061 57.061 0 0 1-.42-1.443c-1.194-4.205-1.93-6.378-2.382-7.476-7.015-.944-11.778-8.647-12.864-21.006l-.03-.557c-.644-12.785.806-16.13 2.31-17.716.241-.254.505-.475.783-.666-1.75-16.051 3.11-32.521 13.331-44.704 9.296-11.079 21.912-17.18 35.524-17.18 3.722 0 7.535.458 11.332 1.362 25.58 6.228 41.597 30.27 39.983 59.227.53.251 1.016.61 1.439 1.066 2.745 2.964 2.464 10.97 2.215 14.276l-.024.41c-.335 5.236-.683 10.65-3.046 15.73-1.736 3.918-4.397 6.242-6.747 8.29-2.391 2.089-4.28 3.735-5.284 6.885-.698 2.416-1.642 4.866-2.554 7.235-1.749 4.538-3.4 8.827-3.534 13.244-.425 10.222 1.17 18.391 4.165 21.359 5.087 5.163 12.977 9.391 19.939 13.121 1.597.855 3.16 1.692 4.645 2.517 9.262 5.052 16.038 7.915 25.259 8.557 9.1.849 18.02 5.193 24.706 11.97.735.641 1.817 1.744 3.687 3.648 4.408 4.492 4.408 4.492 4.417 5.852l.007.758c10.762 17.702 11.12 40.656 11.392 58.169l.05 3.28-3.271.028c-41.968.363-83.894.677-125.812.993-42.038.314-84.067.632-126.12.994l-3.267.029-.013-3.274zM332.387 115.763h15.318v86.734h-15.318v.513l-22.127-17.64v.12h-10.21V211h-22.128v-25.51h-20.424v-52.722h52.762v-.508l22.127-17.065v.568zm34.313 72.093l-7.988-4.591c15.803-27.453 1.717-46.642 1.106-47.443l7.304-5.607c.774.993 18.547 24.675-.422 57.641zm27.361 21.216l-13.866-7.975c27.437-47.66 2.98-80.973 1.918-82.36L394.795 109c1.343 1.723 32.2 42.839-.734 100.072z"/></g></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="444"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<g
fill="none"
fill-rule="evenodd"
id="g3">
<path
fill="#AD245D"
d="M384 .297L511.392 221.65l-128 221.702-255.392.352L.608 222.35 128.608.65z"
id="path1"
style="fill:#e67919;fill-opacity:1" />
<path
d="m 85.310542,348.226 c -0.08,-21.169 -0.205,-53.162 21.256998,-71.332 3.817,-3.253 9.93,-7.497 17.321,-9.224 2.575,-0.523 4.956,-0.756 7.262,-0.979 4.438,-0.431 8.27,-0.804 12.054,-2.9 l 4.954,-2.846 c 9.87,-5.655 19.194,-10.996 28.226,-17.377 5.085,-3.632 6.726,-15.73 6.095,-25.428 -0.214,-2.792 -1.893,-5.7 -3.67,-8.777 -1.097,-1.901 -2.232,-3.867 -3.065,-5.916 l -0.073,-0.199 a 56.976,56.976 0 0 1 -0.422,-1.443 c -1.195,-4.205 -1.933,-6.378 -2.386,-7.476 -7.029,-0.944 -11.8,-8.647 -12.888,-21.006 l -0.031,-0.557 c -0.645,-12.785 0.808,-16.13 2.316,-17.716 0.24,-0.254 0.505,-0.475 0.783,-0.666 -1.754,-16.051 3.115,-32.521 13.358,-44.704 9.314,-11.079 21.955,-17.18 35.592,-17.18 3.73,0 7.55,0.458 11.355,1.362 25.63,6.228 41.679,30.27 40.062,59.227 0.53,0.251 1.018,0.61 1.44,1.066 2.752,2.964 2.47,10.97 2.22,14.276 l -0.024,0.41 c -0.335,5.236 -0.684,10.65 -3.052,15.73 -1.739,3.918 -4.405,6.242 -6.76,8.29 -2.396,2.089 -4.288,3.735 -5.294,6.885 -0.7,2.416 -1.645,4.866 -2.559,7.235 -1.752,4.538 -3.407,8.827 -3.54,13.244 -0.427,10.222 1.17,18.391 4.172,21.359 5.097,5.163 13.003,9.391 19.978,13.121 1.6,0.855 3.166,1.692 4.654,2.517 9.28,5.052 16.07,7.915 25.309,8.557 9.118,0.849 18.056,5.193 24.754,11.97 0.736,0.641 1.82,1.744 3.694,3.648 4.416,4.492 4.416,4.492 4.426,5.852 l 0.007,0.758 c 10.783,17.702 11.14,40.656 11.415,58.169 l 0.05,3.28 -3.278,0.028 c -42.05,0.363 -84.058,0.677 -126.058,0.993 -42.12,0.314 -84.232,0.632 -126.366998,0.994 l -3.273,0.029 z"
id="path2-5"
style="display:inline;fill:#ffffff" />
<path
d="m 349.88573,158.65792 h 15.318 v 86.734 h -15.318 v 0.513 l -22.127,-17.64 v 0.12 h -10.21 v 25.51 h -22.128 v -25.51 h -20.424 v -52.722 h 52.762 v -0.508 l 22.127,-17.065 z m 34.313,72.093 -7.988,-4.591 c 15.803,-27.453 1.717,-46.642 1.106,-47.443 l 7.304,-5.607 c 0.774,0.993 18.547,24.675 -0.422,57.641 z m 27.361,21.216 -13.866,-7.975 c 27.437,-47.66 2.98,-80.973 1.918,-82.36 l 12.682,-9.737 c 1.343,1.723 32.2,42.839 -0.734,100.072 z"
id="path2-1"
style="display:inline;fill:#ffffff" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,257 +1,234 @@
[
{
"provider_name": "Codepen",
"provider_url": "https:\/\/codepen.io",
"endpoints": [
{
"schemes": [
"http:\/\/codepen.io\/*",
"https:\/\/codepen.io\/*"
],
"url": "http:\/\/codepen.io\/api\/oembed"
}
]
},
{
"provider_name": "DTube",
"provider_url": "https:\/\/d.tube\/",
"endpoints": [
{
"schemes": [
"https:\/\/d.tube\/v\/*"
],
"url": "https:\/\/api.d.tube\/oembed",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Post)",
"provider_url": "https:\/\/www.facebook.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/www.facebook.com\/*\/posts\/*",
"https:\/\/www.facebook.com\/photos\/*",
"https:\/\/www.facebook.com\/*\/photos\/*",
"https:\/\/www.facebook.com\/photo.php*",
"https:\/\/www.facebook.com\/photo.php",
"https:\/\/www.facebook.com\/*\/activity\/*",
"https:\/\/www.facebook.com\/permalink.php",
"https:\/\/www.facebook.com\/media\/set?set=*",
"https:\/\/www.facebook.com\/questions\/*",
"https:\/\/www.facebook.com\/notes\/*\/*\/*"
],
"url": "https:\/\/www.facebook.com\/plugins\/post\/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Video)",
"provider_url": "https:\/\/www.facebook.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/www.facebook.com\/*\/videos\/*",
"https:\/\/www.facebook.com\/video.php"
],
"url": "https:\/\/www.facebook.com\/plugins\/video\/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Flickr",
"provider_url": "https:\/\/www.flickr.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/*.flickr.com\/photos\/*",
"http:\/\/flic.kr\/p\/*",
"https:\/\/*.flickr.com\/photos\/*",
"https:\/\/flic.kr\/p\/*"
],
"url": "https:\/\/www.flickr.com\/services\/oembed\/",
"discovery": true
}
]
},
{
"provider_name": "GIPHY",
"provider_url": "https:\/\/giphy.com",
"endpoints": [
{
"schemes": [
"https:\/\/giphy.com\/gifs\/*",
"http:\/\/gph.is\/*",
"https:\/\/media.giphy.com\/media\/*\/giphy.gif"
],
"url": "https:\/\/giphy.com\/services\/oembed",
"discovery": true
}
]
},
{
"provider_name": "Instagram",
"provider_url": "https:\/\/instagram.com",
"endpoints": [
{
"schemes": [
"http:\/\/instagram.com\/p\/*",
"http:\/\/instagr.am\/p\/*",
"http:\/\/www.instagram.com\/p\/*",
"http:\/\/www.instagr.am\/p\/*",
"https:\/\/instagram.com\/p\/*",
"https:\/\/instagr.am\/p\/*",
"https:\/\/www.instagram.com\/p\/*",
"https:\/\/www.instagr.am\/p\/*"
],
"url": "https:\/\/api.instagram.com\/oembed",
"formats": [
"json"
]
}
]
},
{
"provider_name": "Meetup",
"provider_url": "http:\/\/www.meetup.com",
"endpoints": [
{
"schemes": [
"http:\/\/meetup.com\/*",
"https:\/\/www.meetup.com\/*",
"https:\/\/meetup.com\/*",
"http:\/\/meetu.ps\/*"
],
"url": "https:\/\/api.meetup.com\/oembed",
"formats": [
"json"
]
}
]
},
{
"provider_name": "MixCloud",
"provider_url": "https:\/\/mixcloud.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/www.mixcloud.com\/*\/*\/",
"https:\/\/www.mixcloud.com\/*\/*\/"
],
"url": "https:\/\/www.mixcloud.com\/oembed\/"
}
]
},
{
"provider_name": "Reddit",
"provider_url": "https:\/\/reddit.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/reddit.com\/r\/*\/comments\/*\/*",
"https:\/\/www.reddit.com\/r\/*\/comments\/*\/*"
],
"url": "https:\/\/www.reddit.com\/oembed"
}
]
},
{
"provider_name": "SlideShare",
"provider_url": "http:\/\/www.slideshare.net\/",
"endpoints": [
{
"schemes": [
"http:\/\/www.slideshare.net\/*\/*",
"http:\/\/fr.slideshare.net\/*\/*",
"http:\/\/de.slideshare.net\/*\/*",
"http:\/\/es.slideshare.net\/*\/*",
"http:\/\/pt.slideshare.net\/*\/*"
],
"url": "http:\/\/www.slideshare.net\/api\/oembed\/2",
"discovery": true
}
]
},
{
"provider_name": "SoundCloud",
"provider_url": "http:\/\/soundcloud.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/soundcloud.com\/*",
"https:\/\/soundcloud.com\/*"
],
"url": "https:\/\/soundcloud.com\/oembed"
}
]
},
{
"provider_name": "Twitch",
"provider_url": "https:\/\/www.twitch.tv",
"endpoints": [
{
"schemes": [
"http:\/\/clips.twitch.tv\/*",
"https:\/\/clips.twitch.tv\/*",
"http:\/\/www.twitch.tv\/*",
"https:\/\/www.twitch.tv\/*",
"http:\/\/twitch.tv\/*",
"https:\/\/twitch.tv\/*"
],
"url": "https:\/\/api.twitch.tv\/v4\/oembed",
"formats": [
"json"
]
}
]
},
{
"provider_name": "Twitter",
"provider_url": "http:\/\/www.twitter.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/twitter.com\/*\/status\/*",
"https:\/\/*.twitter.com\/*\/status\/*"
],
"url": "https:\/\/publish.twitter.com\/oembed"
}
]
},
{
"provider_name": "Vimeo",
"provider_url": "https:\/\/vimeo.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/vimeo.com\/*",
"https:\/\/vimeo.com\/album\/*\/video\/*",
"https:\/\/vimeo.com\/channels\/*\/*",
"https:\/\/vimeo.com\/groups\/*\/videos\/*",
"https:\/\/vimeo.com\/ondemand\/*\/*",
"https:\/\/player.vimeo.com\/video\/*"
],
"url": "https:\/\/vimeo.com\/api\/oembed.{format}",
"discovery": true
}
]
},
{
"provider_name": "YouTube",
"provider_url": "https:\/\/www.youtube.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/*.youtube.com\/watch*",
"https:\/\/*.youtube.com\/v\/*",
"https:\/\/youtu.be\/*"
],
"url": "https:\/\/www.youtube.com\/oembed",
"discovery": true
}
]
}
]
{
"provider_name": "Codepen",
"provider_url": "https://codepen.io",
"endpoints": [
{
"schemes": ["http://codepen.io/*", "https://codepen.io/*"],
"url": "http://codepen.io/api/oembed"
}
]
},
{
"provider_name": "DTube",
"provider_url": "https://d.tube/",
"endpoints": [
{
"schemes": ["https://d.tube/v/*"],
"url": "https://api.d.tube/oembed",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Post)",
"provider_url": "https://www.facebook.com/",
"endpoints": [
{
"schemes": [
"https://www.facebook.com/*/posts/*",
"https://www.facebook.com/photos/*",
"https://www.facebook.com/*/photos/*",
"https://www.facebook.com/photo.php*",
"https://www.facebook.com/photo.php",
"https://www.facebook.com/*/activity/*",
"https://www.facebook.com/permalink.php",
"https://www.facebook.com/media/set?set=*",
"https://www.facebook.com/questions/*",
"https://www.facebook.com/notes/*/*/*"
],
"url": "https://www.facebook.com/plugins/post/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Facebook (Video)",
"provider_url": "https://www.facebook.com/",
"endpoints": [
{
"schemes": ["https://www.facebook.com/*/videos/*", "https://www.facebook.com/video.php"],
"url": "https://www.facebook.com/plugins/video/oembed.json",
"discovery": true
}
]
},
{
"provider_name": "Flickr",
"provider_url": "https://www.flickr.com/",
"endpoints": [
{
"schemes": [
"http://*.flickr.com/photos/*",
"http://flic.kr/p/*",
"https://*.flickr.com/photos/*",
"https://flic.kr/p/*"
],
"url": "https://www.flickr.com/services/oembed/",
"discovery": true
}
]
},
{
"provider_name": "GIPHY",
"provider_url": "https://giphy.com",
"endpoints": [
{
"schemes": [
"https://giphy.com/gifs/*",
"http://gph.is/*",
"https://media.giphy.com/media/*/giphy.gif"
],
"url": "https://giphy.com/services/oembed",
"discovery": true
}
]
},
{
"provider_name": "Instagram",
"provider_url": "https://instagram.com",
"endpoints": [
{
"schemes": [
"http://instagram.com/p/*",
"http://instagr.am/p/*",
"http://www.instagram.com/p/*",
"http://www.instagr.am/p/*",
"https://instagram.com/p/*",
"https://instagr.am/p/*",
"https://www.instagram.com/p/*",
"https://www.instagr.am/p/*"
],
"url": "https://api.instagram.com/oembed",
"formats": ["json"]
}
]
},
{
"provider_name": "Meetup",
"provider_url": "http://www.meetup.com",
"endpoints": [
{
"schemes": [
"http://meetup.com/*",
"https://www.meetup.com/*",
"https://meetup.com/*",
"http://meetu.ps/*"
],
"url": "https://api.meetup.com/oembed",
"formats": ["json"]
}
]
},
{
"provider_name": "MixCloud",
"provider_url": "https://mixcloud.com/",
"endpoints": [
{
"schemes": ["http://www.mixcloud.com/*/*/", "https://www.mixcloud.com/*/*/"],
"url": "https://www.mixcloud.com/oembed/"
}
]
},
{
"provider_name": "Reddit",
"provider_url": "https://reddit.com/",
"endpoints": [
{
"schemes": [
"https://reddit.com/r/*/comments/*/*",
"https://www.reddit.com/r/*/comments/*/*"
],
"url": "https://www.reddit.com/oembed"
}
]
},
{
"provider_name": "SlideShare",
"provider_url": "http://www.slideshare.net/",
"endpoints": [
{
"schemes": [
"http://www.slideshare.net/*/*",
"http://fr.slideshare.net/*/*",
"http://de.slideshare.net/*/*",
"http://es.slideshare.net/*/*",
"http://pt.slideshare.net/*/*"
],
"url": "http://www.slideshare.net/api/oembed/2",
"discovery": true
}
]
},
{
"provider_name": "SoundCloud",
"provider_url": "http://soundcloud.com/",
"endpoints": [
{
"schemes": ["http://soundcloud.com/*", "https://soundcloud.com/*"],
"url": "https://soundcloud.com/oembed"
}
]
},
{
"provider_name": "Twitch",
"provider_url": "https://www.twitch.tv",
"endpoints": [
{
"schemes": [
"http://clips.twitch.tv/*",
"https://clips.twitch.tv/*",
"http://www.twitch.tv/*",
"https://www.twitch.tv/*",
"http://twitch.tv/*",
"https://twitch.tv/*"
],
"url": "https://api.twitch.tv/v4/oembed",
"formats": ["json"]
}
]
},
{
"provider_name": "Twitter",
"provider_url": "http://www.twitter.com/",
"endpoints": [
{
"schemes": ["https://twitter.com/*/status/*", "https://*.twitter.com/*/status/*"],
"url": "https://publish.twitter.com/oembed"
}
]
},
{
"provider_name": "Vimeo",
"provider_url": "https://vimeo.com/",
"endpoints": [
{
"schemes": [
"https://vimeo.com/*",
"https://vimeo.com/album/*/video/*",
"https://vimeo.com/channels/*/*",
"https://vimeo.com/groups/*/videos/*",
"https://vimeo.com/ondemand/*/*",
"https://player.vimeo.com/video/*"
],
"url": "https://vimeo.com/api/oembed.{format}",
"discovery": true
}
]
},
{
"provider_name": "YouTube",
"provider_url": "https://www.youtube.com/",
"endpoints": [
{
"schemes": [
"https://*.youtube.com/watch*",
"https://*.youtube.com/v/*",
"https://youtu.be/*"
],
"url": "https://www.youtube.com/oembed",
"discovery": true
}
]
}
]

View File

@ -3,15 +3,12 @@
# public
cp -r public/ build/public/
# html files
mkdir -p build/src/middleware/helpers/email/templates/
cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/
# email files
mkdir -p build/src/emails/templates/
cp -r src/emails/templates/ build/src/emails/
mkdir -p build/src/middleware/helpers/email/templates/en/
cp -r src/middleware/helpers/email/templates/en/*.html build/src/middleware/helpers/email/templates/en/
mkdir -p build/src/middleware/helpers/email/templates/de/
cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/
mkdir -p build/src/emails/locales/
cp -r src/emails/locales/ build/src/emails/
# gql files
mkdir -p build/src/graphql/types/

View File

@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable n/no-process-env */
import { config } from 'dotenv'
// eslint-disable-next-line import/no-namespace
import * as SMTPTransport from 'nodemailer/lib/smtp-pool'
import emails from './emails'
import metadata from './metadata'
@ -13,18 +15,20 @@ 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',
}
const required = {
@ -34,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 = {
@ -69,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,
}
@ -109,10 +132,11 @@ export default {
...environment,
...server,
...required,
...smtp,
...neo4j,
...sentry,
...redis,
...s3,
...options,
}
export { nodemailerTransportOptions }

View File

@ -0,0 +1,3 @@
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED'
export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED'

View File

@ -0,0 +1,49 @@
import { getDriver, getNeode } from '@db/neo4j'
import type { Driver } from 'neo4j-driver'
export const query =
(driver: Driver) =>
async ({ query, variables = {} }: { driver; query: string; variables: object }) => {
const session = driver.session()
const result = session.readTransaction(async (transaction) => {
const response = await transaction.run(query, variables)
return response
})
try {
return await result
} finally {
await session.close()
}
}
export const mutate =
(driver: Driver) =>
async ({ query, variables = {} }: { driver; query: string; variables: object }) => {
const session = driver.session()
const result = session.writeTransaction(async (transaction) => {
const response = await transaction.run(query, variables)
return response
})
try {
return await result
} finally {
await session.close()
}
}
export default () => {
const driver = getDriver()
const neode = getNeode()
return {
driver,
neode,
query: query(driver),
mutate: mutate(driver),
}
}

View File

@ -0,0 +1,25 @@
import { RedisPubSub } from 'graphql-redis-subscriptions'
import { PubSub } from 'graphql-subscriptions'
import Redis from 'ioredis'
import CONFIG from '@config/index'
export default () => {
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
if (!(REDIS_DOMAIN && REDIS_PORT && REDIS_PASSWORD)) {
return new PubSub()
}
const options = {
host: REDIS_DOMAIN,
port: REDIS_PORT,
password: REDIS_PASSWORD,
retryStrategy: (times) => {
return Math.min(times * 50, 2000)
},
}
return new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
})
}

View File

@ -10,7 +10,7 @@ import { Factory } from 'rosie'
import slugify from 'slug'
import { v4 as uuid } from 'uuid'
import generateInviteCode from '@schema/resolvers/helpers/generateInviteCode'
import generateInviteCode from '@graphql/resolvers/helpers/generateInviteCode'
import { getDriver, getNeode } from './neo4j'

View File

@ -10,7 +10,7 @@ import { throwError, concat } from 'rxjs'
import { flatMap, mergeMap, map, catchError, filter } from 'rxjs/operators'
import { getDriver } from '@db/neo4j'
import normalizeEmail from '@schema/resolvers/helpers/normalizeEmail'
import normalizeEmail from '@graphql/resolvers/helpers/normalizeEmail'
export const description = `
This migration merges duplicate :User and :EmailAddress nodes. It became

View File

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

View File

@ -1,12 +1,9 @@
/* 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'
import CONFIG from '@config/index'
import models from '@models/index'
import models from '@db/models/index'
let driver: Driver
const defaultOptions = {

View File

@ -26,6 +26,8 @@ if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
}
CONFIG.SEND_MAIL = true
const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
;(async function () {

View File

@ -9,155 +9,155 @@ export const trophies = async () => {
trophyBear: await Factory.build('badge', {
id: 'trophy_bear',
type: 'trophy',
description: 'You earned a Bear',
description: 'Has earned a Bear',
icon: '/img/badges/trophy_blue_bear.svg',
}),
trophyPanda: await Factory.build('badge', {
id: 'trophy_panda',
type: 'trophy',
description: 'You earned a Panda',
description: 'Has earned a Panda',
icon: '/img/badges/trophy_blue_panda.svg',
}),
trophyRabbit: await Factory.build('badge', {
id: 'trophy_rabbit',
type: 'trophy',
description: 'You earned a Rabbit',
description: 'Has earned a Rabbit',
icon: '/img/badges/trophy_blue_rabbit.svg',
}),
trophyRacoon: await Factory.build('badge', {
id: 'trophy_racoon',
type: 'trophy',
description: 'You earned a Racoon',
description: 'Has earned a Racoon',
icon: '/img/badges/trophy_blue_racoon.svg',
}),
trophyRhino: await Factory.build('badge', {
id: 'trophy_rhino',
type: 'trophy',
description: 'You earned a Rhino',
description: 'Has earned a Rhino',
icon: '/img/badges/trophy_blue_rhino.svg',
}),
trophyTiger: await Factory.build('badge', {
id: 'trophy_tiger',
type: 'trophy',
description: 'You earned a Tiger',
description: 'Has earned a Tiger',
icon: '/img/badges/trophy_blue_tiger.svg',
}),
trophyTurtle: await Factory.build('badge', {
id: 'trophy_turtle',
type: 'trophy',
description: 'You earned a Turtle',
description: 'Has earned a Turtle',
icon: '/img/badges/trophy_blue_turtle.svg',
}),
trophyWhale: await Factory.build('badge', {
id: 'trophy_whale',
type: 'trophy',
description: 'You earned a Whale',
description: 'Has earned a Whale',
icon: '/img/badges/trophy_blue_whale.svg',
}),
trophyWolf: await Factory.build('badge', {
id: 'trophy_wolf',
type: 'trophy',
description: 'You earned a Wolf',
description: 'Has earned a Wolf',
icon: '/img/badges/trophy_blue_wolf.svg',
}),
// Green Transports
trophyAirship: await Factory.build('badge', {
id: 'trophy_airship',
type: 'trophy',
description: 'You earned an Airship',
description: 'Has earned an Airship',
icon: '/img/badges/trophy_green_airship.svg',
}),
trophyAlienship: await Factory.build('badge', {
id: 'trophy_alienship',
type: 'trophy',
description: 'You earned an Alienship',
description: 'Has earned an Alienship',
icon: '/img/badges/trophy_green_alienship.svg',
}),
trophyBalloon: await Factory.build('badge', {
id: 'trophy_balloon',
type: 'trophy',
description: 'You earned a Balloon',
description: 'Has earned a Balloon',
icon: '/img/badges/trophy_green_balloon.svg',
}),
trophyBigballoon: await Factory.build('badge', {
id: 'trophy_bigballoon',
type: 'trophy',
description: 'You earned a Big Balloon',
description: 'Has earned a Big Balloon',
icon: '/img/badges/trophy_green_bigballoon.svg',
}),
trophyCrane: await Factory.build('badge', {
id: 'trophy_crane',
type: 'trophy',
description: 'You earned a Crane',
description: 'Has earned a Crane',
icon: '/img/badges/trophy_green_crane.svg',
}),
trophyGlider: await Factory.build('badge', {
id: 'trophy_glider',
type: 'trophy',
description: 'You earned a Glider',
description: 'Has earned a Glider',
icon: '/img/badges/trophy_green_glider.svg',
}),
trophyHelicopter: await Factory.build('badge', {
id: 'trophy_helicopter',
type: 'trophy',
description: 'You earned a Helicopter',
description: 'Has earned a Helicopter',
icon: '/img/badges/trophy_green_helicopter.svg',
}),
// Green Animals
trophyBee: await Factory.build('badge', {
id: 'trophy_bee',
type: 'trophy',
description: 'You earned a Bee',
description: 'Has earned a Bee',
icon: '/img/badges/trophy_green_bee.svg',
}),
trophyButterfly: await Factory.build('badge', {
id: 'trophy_butterfly',
type: 'trophy',
description: 'You earned a Butterfly',
description: 'Has earned a Butterfly',
icon: '/img/badges/trophy_green_butterfly.svg',
}),
// Green Plants
trophyFlower: await Factory.build('badge', {
id: 'trophy_flower',
type: 'trophy',
description: 'You earned a Flower',
description: 'Has earned a Flower',
icon: '/img/badges/trophy_green_flower.svg',
}),
trophyLifetree: await Factory.build('badge', {
id: 'trophy_lifetree',
type: 'trophy',
description: 'You earned the tree of life',
description: 'Has earned the tree of life',
icon: '/img/badges/trophy_green_lifetree.svg',
}),
// Green Misc
trophyDoublerainbow: await Factory.build('badge', {
id: 'trophy_doublerainbow',
type: 'trophy',
description: 'You earned the Double Rainbow',
description: 'Has earned the Double Rainbow',
icon: '/img/badges/trophy_green_doublerainbow.svg',
}),
trophyEndrainbow: await Factory.build('badge', {
id: 'trophy_endrainbow',
type: 'trophy',
description: 'You earned the End of the Rainbow',
description: 'Has earned the End of the Rainbow',
icon: '/img/badges/trophy_green_endrainbow.svg',
}),
trophyMagicrainbow: await Factory.build('badge', {
id: 'trophy_magicrainbow',
type: 'trophy',
description: 'You earned the Magic Rainbow',
description: 'Has earned the Magic Rainbow',
icon: '/img/badges/trophy_green_magicrainbow.svg',
}),
trophyStarter: await Factory.build('badge', {
id: 'trophy_starter',
type: 'trophy',
description: 'You earned the Starter Badge',
description: 'Has earned the Starter Badge',
icon: '/img/badges/trophy_green_starter.svg',
}),
trophySuperfounder: await Factory.build('badge', {
id: 'trophy_superfounder',
type: 'trophy',
description: 'You earned the Super Founder Badge',
description: 'Has earned the Super Founder Badge',
icon: '/img/badges/trophy_green_superfounder.svg',
}),
}
@ -169,19 +169,19 @@ export const verification = async () => {
verificationModerator: await Factory.build('badge', {
id: 'verification_moderator',
type: 'verification',
description: 'You are a Moderator',
description: 'Is a Moderator',
icon: '/img/badges/verification_red_moderator.svg',
}),
verificationAdmin: await Factory.build('badge', {
id: 'verification_admin',
type: 'verification',
description: 'You are an Administrator',
description: 'Is an Administrator',
icon: '/img/badges/verification_red_admin.svg',
}),
verificationDeveloper: await Factory.build('badge', {
id: 'verification_developer',
type: 'verification',
description: 'You are a Developer',
description: 'Is a Developer',
icon: '/img/badges/verification_red_developer.svg',
}),
}

View File

@ -0,0 +1,32 @@
import { Integer, Node } from 'neo4j-driver'
export interface UserDbProperties {
allowEmbedIframes: boolean
awaySince?: string
createdAt: string
deleted: boolean
disabled: boolean
emailNotificationsChatMessage?: boolean
emailNotificationsCommentOnObservedPost?: boolean
emailNotificationsFollowingUsers?: boolean
emailNotificationsGroupMemberJoined?: boolean
emailNotificationsGroupMemberLeft?: boolean
emailNotificationsGroupMemberRemoved?: boolean
emailNotificationsGroupMemberRoleChanged?: boolean
emailNotificationsMention?: boolean
emailNotificationsPostInGroup?: boolean
encryptedPassword: string
id: string
lastActiveAt?: string
lastOnlineStatus?: string
locale: string
name: string
role: string
showShoutsPublicly: boolean
slug: string
termsAndConditionsAgreedAt: string
termsAndConditionsAgreedVersion: string
updatedAt: string
}
export type User = Node<Integer, UserDbProperties>

View File

@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendChatMessageMail English chat_message template 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hello chatReceiver,</h2>
<div class="wrapper">
<div class="content"></div>
<p>you have received a new chat message from <a class="user" href="http://webapp:3000/user/chatSender/chatsender">chatSender</a>.
</p><a class="button" href="http://webapp:3000/chat">Show Chat</a>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p><br>
<p>PS: If you don't want to receive e-mails anymore, change your <a class="settings" href="http://webapp:3000/settings/notifications">notification settings</a>!</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "ocelot.social Notification: New chat message",
"text": "HELLO CHATRECEIVER,
you have received a new chat message from chatSender
[http://webapp:3000/user/chatSender/chatsender].
Show Chat [http://webapp:3000/chat]
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
PS: If you don't want to receive e-mails anymore, change your notification
settings [http://webapp:3000/settings/notifications]!
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendChatMessageMail German chat_message template 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hallo chatReceiver,</h2>
<div class="wrapper">
<div class="content"></div>
<p>du hast eine neue Chat-Nachricht von <a class="user" href="http://webapp:3000/user/chatSender/chatsender">chatSender</a> erhalten.
</p><a class="button" href="http://webapp:3000/chat">Chat anzeigen</a>
<div class="text-block">
<p>Bis bald bei <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> Dein ocelot.social Team</p><br>
<p>PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a class="settings" href="http://webapp:3000/settings/notifications">Benachrichtigungseinstellungen</a>!</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "ocelot.social Benachrichtigung: Neue Chat Nachricht",
"text": "HALLO CHATRECEIVER,
du hast eine neue Chat-Nachricht von chatSender
[http://webapp:3000/user/chatSender/chatsender] erhalten.
Chat anzeigen [http://webapp:3000/chat]
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine
Benachrichtigungseinstellungen [http://webapp:3000/settings/notifications]!
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -0,0 +1,261 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendEmailVerification English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hello User,</h2>
<div class="wrapper">
<div class="content"></div>
<p>So, you want to change your e-mail? No problem! Just click the button below to verify your new address:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&amp;nonce=123456">Verify e-mail address</a>
<p>If you don't want to change your e-mail address feel free to ignore this message. </p>
<p>If the above button doesn't work, you can also copy the following code into your browser window: <span>123456</span></p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "New E-Mail Address ocelot.social",
"text": "HELLO USER,
So, you want to change your e-mail? No problem! Just click the button below to
verify your new address:
Verify e-mail address
[http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&nonce=123456]
If you don't want to change your e-mail address feel free to ignore this
message.
If the above button doesn't work, you can also copy the following code into your
browser window: 123456
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendEmailVerification German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hallo User,</h2>
<div class="wrapper">
<div class="content"></div>
<p>Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button kannst Du Deine neue E-Mail Adresse bestätigen:</p><a class="button" href="http://webapp:3000/settings/my-email-address/verify?email=user%40example.org&amp;nonce=123456">E-Mail Adresse bestätigen</a>
<p>Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht einfach ignorieren. </p>
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<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>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Neue E-Mail Addresse ocelot.social",
"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:
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
Nachricht einfach ignorieren.
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]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,559 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendRegistrationMail with invite code English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Welcome to ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>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><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;inviteCode=welcome&amp;method=invite-code">Confirm your e-mail address</a>
<p>If the above button doesn't work, you can also copy the following code into your browser window: <span>123456</span></p>
<p>However, this only works if you have registered through our website.</p>
<p>If you didn't sign up for <a>ocelot.social</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>PS: If you ignore this e-mail we will not create an account for you. ;)</p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Welcome to ocelot.social",
"text": "WELCOME TO OCELOT.SOCIAL!
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:
Confirm your e-mail address
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&inviteCode=welcome&method=invite-code]
If the above button doesn't work, you can also copy the following code into your
browser window: 123456
However, this only works if you have registered through our website.
If you didn't sign up for ocelot.social 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.
PS: If you ignore this e-mail we will not create an account for you. ;)
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendRegistrationMail with invite code German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Willkommen bei ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;inviteCode=welcome&amp;method=invite-code">Bestätige Deine E-Mail Adresse</a>
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.
</p>
<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>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Willkommen bei ocelot.social",
"text": "WILLKOMMEN BEI OCELOT.SOCIAL!
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:
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
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
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
ignorieren. ;)
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendRegistrationMail without invite code English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Welcome to ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>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><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;method=invite-mail">Confirm your e-mail address</a>
<p>If the above button doesn't work, you can also copy the following code into your browser window: <span>123456</span></p>
<p>However, this only works if you have registered through our website.</p>
<p>If you didn't sign up for <a>ocelot.social</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>PS: If you ignore this e-mail we will not create an account for you. ;)</p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Welcome to ocelot.social",
"text": "WELCOME TO OCELOT.SOCIAL!
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:
Confirm your e-mail address
[http://webapp:3000/registration?email=user%40example.org&nonce=123456&method=invite-mail]
If the above button doesn't work, you can also copy the following code into your
browser window: 123456
However, this only works if you have registered through our website.
If you didn't sign up for ocelot.social 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.
PS: If you ignore this e-mail we will not create an account for you. ;)
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendRegistrationMail without invite code German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Willkommen bei ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>Danke, dass du dich angemeldet hast wir freuen uns, dich dabei zu haben. Jetzt fehlt nur noch eine Kleinigkeit, bevor wir gemeinsam die Welt verbessern können … Bitte bestätige Deine E-Mail Adresse:</p><a class="button" href="http://webapp:3000/registration?email=user%40example.org&amp;nonce=123456&amp;method=invite-mail">Bestätige Deine E-Mail Adresse</a>
<p>Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in Dein Browserfenster kopieren: <span>123456</span></p>
<p>Das funktioniert allerdings nur, wenn du Dich über unsere Website registriert hast.</p>
<p>Falls Du Dich nicht selbst bei <a>ocelot.social</a> angemeldet hast, schau doch mal vorbei! Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.
</p>
<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>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Willkommen bei ocelot.social",
"text": "WILLKOMMEN BEI OCELOT.SOCIAL!
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:
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
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
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
ignorieren. ;)
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -0,0 +1,260 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendResetPasswordMail English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hello Jenny Rostock,</h2>
<div class="wrapper">
<div class="content"></div>
<p>So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&amp;nonce=123456">Confirm your e-mail address</a>
<p>If you didn't request a new password feel free to ignore this e-mail.</p>
<p>If the above button doesn't work you can also copy the following code into your browser window: <span>123456</span></p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Reset Password ocelot.social",
"text": "HELLO JENNY ROSTOCK,
So, you forgot your password? No problem! Just click the button below to reset
it within the next 24 hours:
Confirm your e-mail address
[http://webapp:3000/password-reset/change-password?email=user%40example.org&nonce=123456]
If you didn't request a new password feel free to ignore this e-mail.
If the above button doesn't work you can also copy the following code into your
browser window: 123456
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendResetPasswordMail German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Hallo Jenny Rostock,</h2>
<div class="wrapper">
<div class="content"></div>
<p>Du hast also dein Passwort vergessen? Kein Problem! Mit Klick auf diesen Button kannst du innerhalb der nächsten 24 Stunden dein Passwort zurücksetzen:</p><a class="button" href="http://webapp:3000/password-reset/change-password?email=user%40example.org&amp;nonce=123456">Bestätige Deine E-Mail Adresse</a>
<p>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>
<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>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Neues Passwort ocelot.social",
"text": "HALLO JENNY ROSTOCK,
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
[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
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sendWrongEmail English renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="en">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Welcome to ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>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><a class="button" href="http://webapp:3000/password-reset/request">Try a different e-mail</a>
<p>If you don't have an account at <a>ocelot.social</a> yet or if you didn't want to reset your password, please ignore this e-mail.
</p>
<div class="text-block">
<p>See you soon on <a class="organization" href="https://ocelot.social">ocelot.social</a>!</p>
<p> The ocelot.social Team</p>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Wrong E-mail? ocelot.social",
"text": "WELCOME TO OCELOT.SOCIAL!
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?
Try a different e-mail [http://webapp:3000/password-reset/request]
If you don't have an account at ocelot.social yet or if you didn't want to reset
your password, please ignore this e-mail.
See you soon on ocelot.social [https://ocelot.social]!
The ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;
exports[`sendWrongEmail German renders correctly 1`] = `
{
"attachments": [],
"from": "ocelot.social <devops@ocelot.social>",
"html": "<!DOCTYPE html>
<html lang="de">
<head>
<meta content="multipart/html; charset=UTF-8" http-equiv="content-type">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}</style>
<style>body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="head"><img class="head-logo" alt="Welcome Image" loading="lazy" src="http://webapp:3000/img/custom/logo-squared.svg">
</div>
</header>
<h2>Willkommen bei ocelot.social!</h2>
<div class="wrapper">
<div class="content"></div>
<p>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><a class="button" href="http://webapp:3000/password-reset/request">Versuch' es mit einer anderen E-Mail</a>
<p>Wenn du noch keinen Account bei <a>ocelot.social</a> hast oder dein Password gar nicht ändern willst, 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>
</div>
</div>
<footer>
<div class="footer"></div><a href="https://ocelot.social">ocelot.social Community</a>
</footer>
</div>
</body>
</html>",
"subject": "Falsche Mailaddresse? ocelot.social",
"text": "WILLKOMMEN BEI OCELOT.SOCIAL!
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?
Versuch' es mit einer anderen E-Mail [http://webapp:3000/password-reset/request]
Wenn du noch keinen Account bei ocelot.social hast oder dein Password gar nicht
ändern willst, kannst du diese E-Mail einfach ignorieren!
Bis bald bei ocelot.social [https://ocelot.social]!
Dein ocelot.social Team
ocelot.social Community [https://ocelot.social]",
"to": "user@example.org",
}
`;

View File

@ -0,0 +1,71 @@
{
"notification": "Benachrichtigung",
"subjects": {
"changedGroupMemberRole": "Rolle in Gruppe geändert",
"chatMessage": "Neue Chat Nachricht",
"commentedOnPost": "Neuer Kommentar zu Beitrag",
"followedUserPosted": "Neuer Beitrag von gefolgtem Nutzer",
"mentionedInComment": "Erwähnung in Kommentar",
"mentionedInPost": "Erwähnung in Beitrag",
"newEmail": "Neue E-Mail Addresse",
"removedUserFromGroup": "Aus Gruppe entfernt",
"postInGroup": "Neuer Beitrag in Gruppe",
"resetPassword": "Neues Passwort",
"userJoinedGroup": "Nutzer tritt Gruppe bei",
"userLeftGroup": "Nutzer verlässt Gruppe",
"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 ",
"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. ;)"
},
"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. "
},
"buttons": {
"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",
"viewChat": "Chat anzeigen",
"viewComment": "Kommentar ansehen",
"viewGroup": "Gruppe ansehen",
"viewPost": "Beitrag ansehen"
},
"general": {
"greeting": "Hallo",
"seeYou": "Bis bald bei ",
"yourTeam": " Dein {team} Team",
"settingsHint": "PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine ",
"settingsName": "Benachrichtigungseinstellungen",
"welcome": "Willkommen bei"
},
"resetPassword": {
"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: ",
"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?"
},
"changedGroupMemberRole": "deine Rolle in der Gruppe „{groupName}“ wurde geändert. Klicke auf den Knopf, um diese Gruppe zu sehen:",
"chatMessageStart": "du hast eine neue Chat-Nachricht von ",
"chatMessageEnd": " erhalten.",
"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:",
"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:",
"userLeftGroup": " hat die Gruppe „{groupName}“ verlassen. Klicke auf den Knopf, um diese Gruppe zu sehen:"
}

View File

@ -0,0 +1,70 @@
{
"notification": "Notification",
"subjects": {
"changedGroupMemberRole": "Role in group changed",
"chatMessage": "New chat message",
"commentedOnPost": "New comment on post",
"followedUserPosted": "New post by followd user",
"mentionedInComment": "Mentioned in comment",
"mentionedInPost": "Mentioned in post",
"newEmail": "New E-Mail Address",
"removedUserFromGroup": "Removed from group",
"postInGroup": "New post in group",
"resetPassword": "Reset Password",
"userJoinedGroup": "User joined group",
"userLeftGroup": "User left group",
"wrongEmail": "Wrong E-mail?"
},
"registration": {
"introduction": "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:",
"codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ",
"codeHintException": "However, this only works if you have registered through our website.",
"notYouStart": "If you didn't sign up for ",
"notYouEnd": " 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.",
"ps": "PS: If you ignore this e-mail we will not create an account for you. ;)"
},
"emailVerification": {
"codeHint": "If the above button doesn't work, you can also copy the following code into your browser window: ",
"introduction": "So, you want to change your e-mail? No problem! Just click the button below to verify your new address:",
"doNotChange": "If you don't want to change your e-mail address feel free to ignore this message. "
},
"buttons": {
"confirmEmail": "Confirm your e-mail address",
"resetPassword": "Reset password",
"tryAgain": "Try a different e-mail",
"verifyEmail": "Verify e-mail address",
"viewChat": "Show Chat",
"viewComment": "View comment",
"viewGroup": "View group",
"viewPost": "View post"
},
"general": {
"greeting": "Hello",
"seeYou": "See you soon on ",
"yourTeam": " The {team} Team",
"settingsHint": "PS: If you don't want to receive e-mails anymore, change your ",
"settingsName": "notification settings",
"welcome": "Welcome to"
},
"resetPassword": {
"codeHint": "If the above button doesn't work you can also copy the following code into your browser window: ",
"ignore": "If you didn't request a new password feel free to ignore this e-mail.",
"introduction": "So, you forgot your password? No problem! Just click the button below to reset it within the next 24 hours:"
},
"wrongEmail": {
"ignoreEnd": " yet or if you didn't want to reset your password, please ignore this e-mail.",
"ignoreStart": "If you don't have an account at ",
"introduction": "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?"
},
"changedGroupMemberRole": "your role in the group “{groupName}” has been changed. Click on the button to view this group:",
"chatMessageStart": "you have received a new chat message from ",
"chatMessageEnd": ".",
"commentedOnPost": " commented on a post that you are observing with the title “{postTitle}”. Click on the button to view this comment:",
"followedUserPosted": ", a user you are following, wrote a new post with the title “{postTitle}”. Click on the button to view this post:",
"mentionedInComment": " mentioned you in a comment to the post with the title “{postTitle}”. Click on the button to view this comment:",
"mentionedInPost": " mentioned you in a post with the title “{postTitle}”. Click on the button to view this post:",
"removedUserFromGroup": "you have been removed from the group “{groupName}”.",
"postInGroup": "someone wrote a new post with the title “{postTitle}” in one of your groups. Click on the button to view this post:",
"userJoinedGroup": " joined the group “{groupName}”. Click on the button to view this group:",
"userLeftGroup": " left the group “{groupName}”. Click on the button to view this group:"
}

View File

@ -0,0 +1,87 @@
import { sendChatMessageMail } from './sendEmail'
const senderUser = {
allowEmbedIframes: false,
createdAt: '2025-04-30T00:16:49.610Z',
deleted: false,
disabled: false,
emailNotificationsChatMessage: true,
emailNotificationsCommentOnObservedPost: true,
emailNotificationsFollowingUsers: true,
emailNotificationsGroupMemberJoined: true,
emailNotificationsGroupMemberLeft: true,
emailNotificationsGroupMemberRemoved: true,
emailNotificationsGroupMemberRoleChanged: true,
emailNotificationsMention: true,
emailNotificationsPostInGroup: true,
encryptedPassword: '$2b$10$n.WujXapJrvn498lS97MD.gn8QwjWI9xlf8ckEYYtMTOPadMidcbG',
id: 'chatSender',
locale: 'en',
name: 'chatSender',
role: 'user',
showShoutsPublicly: false,
slug: 'chatsender',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
termsAndConditionsAgreedVersion: '0.0.1',
updatedAt: '2025-04-30T00:16:49.610Z',
}
const recipientUser = {
allowEmbedIframes: false,
createdAt: '2025-04-30T00:16:49.716Z',
deleted: false,
disabled: false,
emailNotificationsChatMessage: true,
emailNotificationsCommentOnObservedPost: true,
emailNotificationsFollowingUsers: true,
emailNotificationsGroupMemberJoined: true,
emailNotificationsGroupMemberLeft: true,
emailNotificationsGroupMemberRemoved: true,
emailNotificationsGroupMemberRoleChanged: true,
emailNotificationsMention: true,
emailNotificationsPostInGroup: true,
encryptedPassword: '$2b$10$KOrCHvEB5CM7D.P3VcX2z.pSSBZKZhPqHW/QKym6V1S6fiG..xtBq',
id: 'chatReceiver',
locale: 'en',
name: 'chatReceiver',
role: 'user',
showShoutsPublicly: false,
slug: 'chatreceiver',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
termsAndConditionsAgreedVersion: '0.0.1',
updatedAt: '2025-04-30T00:16:49.716Z',
}
describe('sendChatMessageMail', () => {
describe('English', () => {
beforeEach(() => {
recipientUser.locale = 'en'
})
it('chat_message template', async () => {
await expect(
sendChatMessageMail({
email: 'user@example.org',
senderUser,
recipientUser,
}),
).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
recipientUser.locale = 'de'
})
it('chat_message template', async () => {
await expect(
sendChatMessageMail({
email: 'user@example.org',
senderUser,
recipientUser,
}),
).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,321 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
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, { nodemailerTransportOptions } from '@config/index'
import logosWebapp from '@config/logos'
import metadata from '@config/metadata'
import { UserDbProperties } from '@db/types/User'
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const settingsUrl = new URL('/settings/notifications', 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,
settingsUrl,
renderSettingsUrl: true,
}
const from = `${CONFIG.APPLICATION_NAME} <${CONFIG.EMAIL_DEFAULT_SENDER}>`
const transport = createTransport(nodemailerTransportOptions)
const email = new Email({
message: {
from,
},
transport,
i18n: {
locales: ['en', 'de'],
defaultLocale: 'en',
retryInDefaultLocale: false,
directory: path.join(__dirname, 'locales'),
updateFiles: false,
objectNotation: true,
mustacheConfig: {
tags: ['{', '}'],
disable: false,
},
},
send: CONFIG.SEND_MAIL,
preview: false,
// This is very useful to see the emails sent by the unit tests
/*
preview: {
open: {
app: 'brave-browser',
},
},
*/
})
interface OriginalMessage {
to: string
from: string
attachments: string[]
subject: string
html: string
text: string
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const sendNotificationMail = async (notification: any): Promise<OriginalMessage> => {
const locale = notification?.to?.locale
const to = notification?.email
const name = notification?.to?.name
const template = notification?.reason
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', template),
message: {
to,
},
locals: {
...defaultParams,
locale,
name,
postTitle:
notification?.from?.__typename === 'Comment'
? notification?.from?.post?.title
: notification?.from?.title,
postUrl: new URL(
notification?.from?.__typename === 'Comment'
? `/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}`
: `/post/${notification?.from?.id}/${notification?.from?.slug}`,
CONFIG.CLIENT_URI,
),
postAuthorName:
notification?.from?.__typename === 'Comment'
? undefined
: notification?.from?.author?.name,
postAuthorUrl:
notification?.from?.__typename === 'Comment'
? undefined
: new URL(
`user/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
CONFIG.CLIENT_URI,
),
commenterName:
notification?.from?.__typename === 'Comment'
? notification?.from?.author?.name
: undefined,
commenterUrl:
notification?.from?.__typename === 'Comment'
? new URL(
`/user/${notification?.from?.author?.id}/${notification?.from?.author?.slug}`,
CONFIG.CLIENT_URI,
)
: undefined,
commentUrl:
notification?.from?.__typename === 'Comment'
? new URL(
`/post/${notification?.from?.post?.id}/${notification?.from?.post?.slug}#commentId-${notification?.from?.id}`,
CONFIG.CLIENT_URI,
)
: undefined,
// chattingUser: 'SR-71',
// chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
groupUrl:
notification?.from?.__typename === 'Group'
? new URL(
`/group/${notification?.from?.id}/${notification?.from?.slug}`,
CONFIG.CLIENT_URI,
)
: undefined,
groupName:
notification?.from?.__typename === 'Group' ? notification?.from?.name : undefined,
groupRelatedUserName:
notification?.from?.__typename === 'Group' ? notification?.relatedUser?.name : undefined,
groupRelatedUserUrl:
notification?.from?.__typename === 'Group'
? new URL(
`/user/${notification?.relatedUser?.id}/${notification?.relatedUser?.slug}`,
CONFIG.CLIENT_URI,
)
: undefined,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
export interface ChatMessageEmailInput {
senderUser: UserDbProperties
recipientUser: UserDbProperties
email: string
}
export const sendChatMessageMail = async (
data: ChatMessageEmailInput,
): Promise<OriginalMessage> => {
const { senderUser, recipientUser } = data
const to = data.email
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'chat_message'),
message: {
to,
},
locals: {
...defaultParams,
locale: recipientUser.locale,
name: recipientUser.name,
chattingUser: senderUser.name,
chattingUserUrl: new URL(`/user/${senderUser.id}/${senderUser.slug}`, CONFIG.CLIENT_URI),
chatUrl: new URL('/chat', CONFIG.CLIENT_URI),
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
interface VerifyMailInput {
email: string
nonce: string
locale: string
}
interface RegistrationMailInput extends VerifyMailInput {
inviteCode?: string
}
export const sendRegistrationMail = async (
data: RegistrationMailInput,
): Promise<OriginalMessage> => {
const { nonce, locale, inviteCode } = data
const to = data.email
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', to)
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')
}
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'registration'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
nonce,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
interface EmailVerificationInput extends VerifyMailInput {
name: string
}
export const sendEmailVerification = async (
data: EmailVerificationInput,
): Promise<OriginalMessage> => {
const { nonce, locale, name } = data
const to = data.email
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', to)
actionUrl.searchParams.set('nonce', nonce)
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'emailVerification'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
nonce,
name,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
export const sendResetPasswordMail = async (
data: EmailVerificationInput,
): Promise<OriginalMessage> => {
const { nonce, locale, name } = data
const to = data.email
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', to)
actionUrl.searchParams.set('nonce', nonce)
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'resetPassword'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
nonce,
name,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}
export const sendWrongEmail = async (data: {
locale: string
email: string
}): Promise<OriginalMessage> => {
const { locale } = data
const to = data.email
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
try {
const { originalMessage } = await email.send({
template: path.join(__dirname, 'templates', 'wrongEmail'),
message: {
to,
},
locals: {
...defaultParams,
locale,
actionUrl,
renderSettingsUrl: false,
},
})
return originalMessage as OriginalMessage
} catch (error) {
throw new Error(error)
}
}

View File

@ -0,0 +1,35 @@
import { sendEmailVerification } from './sendEmail'
describe('sendEmailVerification', () => {
const data: {
email: string
nonce: string
locale: string
name: string
} = {
email: 'user@example.org',
nonce: '123456',
locale: 'en',
name: 'User',
}
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
})
it('renders correctly', async () => {
await expect(sendEmailVerification(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
})
it('renders correctly', async () => {
await expect(sendEmailVerification(data)).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,475 @@
import { sendNotificationMail } from './sendEmail'
describe('sendNotificationMail', () => {
let locale = 'en'
describe('English', () => {
beforeEach(() => {
locale = 'en'
})
it('followed_user_posted template', async () => {
await expect(
sendNotificationMail({
reason: 'followed_user_posted',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('post_in_group template', async () => {
await expect(
sendNotificationMail({
reason: 'post_in_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_post template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('commented_on_post template', async () => {
await expect(
sendNotificationMail({
reason: 'commented_on_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_comment template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_comment',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('changed_group_member_role template', async () => {
await expect(
sendNotificationMail({
reason: 'changed_group_member_role',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
it('user_joined_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_joined_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('user_left_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_left_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('removed_user_from_group template', async () => {
await expect(
sendNotificationMail({
reason: 'removed_user_from_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
locale = 'de'
})
it('followed_user_posted template', async () => {
await expect(
sendNotificationMail({
reason: 'followed_user_posted',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('post_in_group template', async () => {
await expect(
sendNotificationMail({
reason: 'post_in_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_post template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
},
}),
).resolves.toMatchSnapshot()
})
it('commented_on_post template', async () => {
await expect(
sendNotificationMail({
reason: 'commented_on_post',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('mentioned_in_comment template', async () => {
await expect(
sendNotificationMail({
reason: 'mentioned_in_comment',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Comment',
id: 'c1',
slug: 'new-comment',
author: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
post: {
id: 'p1',
slug: 'new-post',
title: 'New Post',
},
},
}),
).resolves.toMatchSnapshot()
})
it('changed_group_member_role template', async () => {
await expect(
sendNotificationMail({
reason: 'changed_group_member_role',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
it('user_joined_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_joined_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('user_left_group template', async () => {
await expect(
sendNotificationMail({
reason: 'user_left_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
relatedUser: {
id: 'u2',
name: 'Peter Lustig',
slug: 'peter-lustig',
},
}),
).resolves.toMatchSnapshot()
})
it('removed_user_from_group template', async () => {
await expect(
sendNotificationMail({
reason: 'removed_user_from_group',
email: 'user@example.org',
to: {
name: 'Jenny Rostock',
id: 'u1',
slug: 'jenny-rostock',
locale,
},
from: {
__typename: 'Group',
id: 'g1',
slug: 'the-group',
name: 'The Group',
},
}),
).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,63 @@
import { sendRegistrationMail } from './sendEmail'
describe('sendRegistrationMail', () => {
const data: {
email: string
nonce: string
locale: string
inviteCode?: string
} = {
email: 'user@example.org',
nonce: '123456',
locale: 'en',
inviteCode: 'welcome',
}
describe('with invite code', () => {
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
data.inviteCode = 'welcome'
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
data.inviteCode = 'welcome'
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
})
describe('without invite code', () => {
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
delete data.inviteCode
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
delete data.inviteCode
})
it('renders correctly', async () => {
await expect(sendRegistrationMail(data)).resolves.toMatchSnapshot()
})
})
})
})

View File

@ -0,0 +1,35 @@
import { sendResetPasswordMail } from './sendEmail'
describe('sendResetPasswordMail', () => {
const data: {
email: string
nonce: string
locale: string
name: string
} = {
email: 'user@example.org',
nonce: '123456',
locale: 'en',
name: 'Jenny Rostock',
}
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
})
it('renders correctly', async () => {
await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
})
it('renders correctly', async () => {
await expect(sendResetPasswordMail(data)).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,31 @@
import { sendWrongEmail } from './sendEmail'
describe('sendWrongEmail', () => {
const data: {
email: string
locale: string
} = {
email: 'user@example.org',
locale: 'en',
}
describe('English', () => {
beforeEach(() => {
data.locale = 'en'
})
it('renders correctly', async () => {
await expect(sendWrongEmail(data)).resolves.toMatchSnapshot()
})
})
describe('German', () => {
beforeEach(() => {
data.locale = 'de'
})
it('renders correctly', async () => {
await expect(sendWrongEmail(data)).resolves.toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,7 @@
extend ../layout.pug
block content
.content
- var groupUrl = groupUrl
p= t('changedGroupMemberRole', { groupName })
a.button(href=groupUrl)= t('buttons.viewGroup')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.changedGroupMemberRole')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p= t('chatMessageStart')
a.user(href=chattingUserUrl)= chattingUser
= t('chatMessageEnd')
a.button(href=chatUrl)= t('buttons.viewChat')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.chatMessage')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=commenterUrl)= commenterName
= t('commentedOnPost', { postTitle})
a.button(href=commentUrl)= t('buttons.viewComment')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.commentedOnPost')}`

View File

@ -0,0 +1,10 @@
extend ../layout.pug
block content
.content
p= t('emailVerification.introduction')
a.button(href=actionUrl)= t('buttons.verifyEmail')
p= t('emailVerification.doNotChange')
p= t('emailVerification.codeHint')
span= nonce

View File

@ -0,0 +1 @@
= `${t('subjects.newEmail')} ${APPLICATION_NAME}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=postAuthorUrl)= postAuthorName
= t('followedUserPosted', { postTitle })
a.button(href=postUrl)= t('buttons.viewPost')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.followedUserPosted')}`

View File

@ -0,0 +1,5 @@
footer
.footer
- var organizationUrl = ORGANIZATION_URL
- var organizationName = ORGANIZATION_NAME
a(href=organizationUrl)= organizationName

View File

@ -0,0 +1,17 @@
//- This sets the greeting at the end of every e-mail
.text-block
- var organizationUrl = ORGANIZATION_URL
- var team = APPLICATION_NAME
- var settingsUrl = settingsUrl
- var renderSettingsUrl = renderSettingsUrl
p= t('general.seeYou')
a.organization(href=organizationUrl)= team
| !
p= t('general.yourTeam', { team })
if renderSettingsUrl
br
p= t('general.settingsHint')
a.settings(href=settingsUrl)= t('general.settingsName')
| !

View File

@ -0,0 +1,9 @@
header
.head
- var img = welcomeImageUrl
img.head-logo(
alt="Welcome Image"
loading="lazy"
src=img
)

View File

@ -0,0 +1 @@
h2= `${t('general.greeting')} ${name},`

View File

@ -0,0 +1,69 @@
body{
display: block;
font-family: Lato, sans-serif;
font-size: 17px;
text-align: left;
text-align: -webkit-left;
justify-content: center;
padding: 15px;
margin: 0px;
}
h2 {
margin-top: 25px;
font-size: 25px;
font-weight: normal;
line-height: 22px;
color: #333333;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 60%;
height: auto;
display: block;
margin-left: auto;
margin-right: auto;
}
a {
color: #17b53e;
}
a.button {
background: #17b53e;
font-family: Lato, sans-serif;
font-size: 16px;
line-height: 15px;
text-decoration: none;
text-align:center;
padding: 13px 17px;
color: #ffffff;
display: table;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
}
span {
color: #17b53e;
}
.text-block {
margin-top: 20px;
color: #000000;
}
footer {
padding: 20px;
font-family: Lato, sans-serif;
font-size: 12px;
line-height: 15px;
text-align: center;
color: #888888;
}

View File

@ -0,0 +1 @@
h2= `${t('general.welcome')} ${APPLICATION_NAME}!`

View File

@ -0,0 +1,30 @@
doctype html
html(lang=locale)
head
meta(
content="multipart/html; charset=UTF-8"
http-equiv="content-type"
)
meta(
name="viewport"
content="width=device-width, initial-scale=1"
)
style.
.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}
style
include includes/webflow.css
- var name = name
body
div.container
include includes/header.pug
if name
include includes/salutation.pug
else
include includes/welcome.pug
.wrapper
block content
include includes/greeting.pug
include includes/footer.pug

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=commenterUrl)= commenterName
= t('mentionedInComment', { postTitle})
a.button(href=commentUrl)= t('buttons.viewComment')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.mentionedInComment')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=postAuthorUrl)= postAuthorName
= t('mentionedInPost', { postTitle })
a.button(href=postUrl)= t('buttons.viewPost')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.mentionedInPost')}`

View File

@ -0,0 +1,7 @@
extend ../layout.pug
block content
.content
- var postUrl = postUrl
p= t('postInGroup', { postTitle})
a.button(href=postUrl)= t('buttons.viewPost')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.postInGroup')}`

View File

@ -0,0 +1,15 @@
extend ../layout.pug
block content
.content
p= t('registration.introduction')
a.button(href=actionUrl)= t('buttons.confirmEmail')
p= t('registration.codeHint')
span= nonce
p= t('registration.codeHintException')
p= t('registration.notYouStart')
a(href=ORGANIZATION_LINK)= APPLICATION_NAME
= t('registration.notYouEnd')
p= t('registration.ps')

View File

@ -0,0 +1 @@
= `${t('general.welcome')} ${APPLICATION_NAME}`

View File

@ -0,0 +1,5 @@
extend ../layout.pug
block content
.content
p= t('removedUserFromGroup', { groupName })

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.removedUserFromGroup')}`

View File

@ -0,0 +1,9 @@
extend ../layout.pug
block content
.content
p= t('resetPassword.introduction')
a.button(href=actionUrl)= t('buttons.confirmEmail')
p= t('resetPassword.ignore')
p= t('resetPassword.codeHint')
span= nonce

View File

@ -0,0 +1 @@
= `${t('subjects.resetPassword')} ${APPLICATION_NAME}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=groupRelatedUserUrl)= groupRelatedUserName
= t('userJoinedGroup', { groupName })
a.button(href=groupUrl)= t('buttons.viewGroup')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.userJoinedGroup')}`

View File

@ -0,0 +1,8 @@
extend ../layout.pug
block content
.content
p
a.user(href=groupRelatedUserUrl)= groupRelatedUserName
= t('userLeftGroup', { groupName })
a.button(href=groupUrl)= t('buttons.viewGroup')

View File

@ -0,0 +1 @@
= `${APPLICATION_NAME} ${t('notification')}: ${t('subjects.userLeftGroup')}`

View File

@ -0,0 +1,10 @@
extend ../layout.pug
block content
.content
p= t('wrongEmail.introduction')
a.button(href=actionUrl)= t('buttons.tryAgain')
p= t('wrongEmail.ignoreStart')
a(href=ORGANIZATION_LINK)= APPLICATION_NAME
= t('wrongEmail.ignoreEnd')

View File

@ -0,0 +1 @@
= `${t('subjects.wrongEmail')} ${APPLICATION_NAME}`

Some files were not shown because too many files have changed in this diff Show More