refactor: move to bun

This commit is contained in:
2024-12-11 18:59:02 +01:00
parent dcc4e26235
commit b94bf56e77
79 changed files with 2038 additions and 20013 deletions

View File

@@ -1,198 +0,0 @@
import fs from "fs";
import Handlebars from 'handlebars';
import i18next from "i18next";
import Backend from 'i18next-fs-backend';
import nodemailer from 'nodemailer';
import { MailOptions } from 'nodemailer/lib/json-transport';
import Mail from 'nodemailer/lib/mailer';
import path from 'path';
import { config } from './config';
import { MailServerConfigError } from './errors/MailErrors';
/**
* This class is responsible for all mail sending.
* This uses the html and plaintext templates from src/templates.
*/
export class Mailer {
private transport: Mail;
private static interpolations = { copyright_owner: config.copyright_owner, event_name: config.event_name, contact_mail: config.contact_mail }
/**
* Main constructor.
* Initializes i18n(ext), Handlebars and puppeteer.
*/
constructor() {
this.init();
}
/**
* Main constructor.
* Initializes i18n(ext), Handlebars and puppeteer.
*/
public async init() {
await i18next
.use(Backend)
.init({
fallbackLng: 'en',
lng: 'en',
backend: {
loadPath: path.join(__dirname, '/locales/{{lng}}.json')
}
});
await Handlebars.registerHelper('__',
function (str) {
return i18next.t(str, Mailer.interpolations).toString();
}
);
this.transport = nodemailer.createTransport({
host: config.mail_server,
port: config.mail_port,
auth: {
user: config.mail_user,
pass: config.mail_password
}
});
this.transport.verify(function (error, success) {
if (error) {
throw new MailServerConfigError();
}
});
}
/**
* Function for sending a reset mail from the reset mail template.
* @param to_address The address the mail will be sent to. Should always get pulled from a user object.
* @param token The requested password reset token - will be combined with the app_url to generate a password reset link.
*/
public async sendResetMail(to_address: string, token: string, locale: string = "en") {
await i18next.changeLanguage(locale);
const replacements = {
recipient_mail: to_address,
copyright_owner: config.copyright_owner,
link_imprint: `${config.app_url}/imprint`,
link_privacy: `${config.app_url}/privacy`,
reset_link: `${config.app_url}/reset/${(Buffer.from(token)).toString("base64")}`
}
const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/pw-reset.html', { encoding: 'utf8' }));
const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/pw-reset.txt', { encoding: 'utf8' }));
const body_html = template_html(replacements);
const body_txt = template_txt(replacements);
const mail: MailOptions = {
to: to_address,
subject: i18next.t("lfk-password-reset", Mailer.interpolations).toString(),
text: body_txt,
html: body_html
};
await this.sendMail(mail);
}
/**
* Function for sending a test mail from the test mail template.
* @param to_address The address the mail will be sent to - usually the FROM address.
*/
public async sendTestMail(locale: string = "en") {
await i18next.changeLanguage(locale);
const to_address: string = config.mail_from;
const replacements = {
recipient_mail: to_address,
copyright_owner: config.copyright_owner,
link_imprint: `${config.app_url}/imprint`,
link_privacy: `${config.app_url}/privacy`
}
const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/test.html', { encoding: 'utf8' }));
const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/test.txt', { encoding: 'utf8' }));
const body_html = template_html(replacements);
const body_txt = template_txt(replacements);
const mail: MailOptions = {
to: to_address,
subject: i18next.t("test-mail", Mailer.interpolations).toString(),
text: body_txt,
html: body_html
};
await this.sendMail(mail);
}
/**
* Function for sending a reset mail from the reset mail template.
* @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
* @param token The runner's selfservice token - will be combined with the app_url to generate a selfservice profile link.
*/
public async sendWelcomeMail(to_address: string, token: string, locale: string = "en") {
await i18next.changeLanguage(locale);
token = Buffer.from(token).toString("base64");
const replacements = {
recipient_mail: to_address,
copyright_owner: config.copyright_owner,
link_imprint: `${config.app_url}/imprint`,
link_privacy: `${config.app_url}/privacy`,
selfservice_link: `${config.app_url}/selfservice/profile/${token}`,
forgot_link: `${config.app_url}/selfservice/`,
contact_mail: config.contact_mail,
event_name: config.event_name
}
const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/welcome_runner.html', { encoding: 'utf8' }));
const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/welcome_runner.txt', { encoding: 'utf8' }));
const body_html = template_html(replacements);
const body_txt = template_txt(replacements);
const mail: MailOptions = {
to: to_address,
subject: i18next.t("event_name-registration", Mailer.interpolations).toString(),
text: body_txt,
html: body_html
};
await this.sendMail(mail);
}
/**
* Function for sending a selfservice link forgotten mail from the runner_forgot template.
* @param to_address The address the mail will be sent to. Should always get pulled from a runner object.
* @param token The runner's selfservice token - will be combined with the app_url to generate a selfservice profile link.
*/
public async sendSelfserviceForgottenMail(to_address: string, token: string, locale: string = "en") {
await i18next.changeLanguage(locale);
token = Buffer.from(token).toString("base64");
const replacements = {
recipient_mail: to_address,
copyright_owner: config.copyright_owner,
link_imprint: `${config.app_url}/imprint`,
link_privacy: `${config.app_url}/privacy`,
selfservice_link: `${config.app_url}/selfservice/profile/${token}`,
forgot_link: `${config.app_url}/selfservice/`,
contact_mail: config.contact_mail,
event_name: config.event_name
}
const template_html = Handlebars.compile(fs.readFileSync(__dirname + '/templates/runner_forgot.html', { encoding: 'utf8' }));
const template_txt = Handlebars.compile(fs.readFileSync(__dirname + '/templates/runner_forgot.txt', { encoding: 'utf8' }));
const body_html = template_html(replacements);
const body_txt = template_txt(replacements);
const mail: MailOptions = {
to: to_address,
subject: i18next.t("your-event_name-profile", Mailer.interpolations).toString(),
text: body_txt,
html: body_html
};
await this.sendMail(mail);
}
/**
* Wrapper function for sending a mail via this object's transporter.
* @param mail MailOptions object containing the
*/
public async sendMail(mail: MailOptions) {
mail.from = config.mail_from;
await this.transport.sendMail(mail);
}
}

View File

@@ -1,33 +0,0 @@
import { MetadataArgsStorage } from 'routing-controllers';
import { routingControllersToSpec } from 'routing-controllers-openapi';
import { config } from './config';
/**
* This function generates a the openapi spec from route metadata and type schemas.
* @param storage MetadataArgsStorage object generated by routing-controllers.
* @param schemas MetadataArgsStorage object generated by class-validator-jsonschema.
*/
export function generateSpec(storage: MetadataArgsStorage, schemas) {
return routingControllersToSpec(
storage,
{},
{
components: {
schemas,
"securitySchemes": {
"AuthToken": {
"type": "apiKey",
"in": "query",
"name": "key",
description: "A simple api key. See the README's env section for more details."
}
}
},
info: {
description: "The the API for the LfK! mailer.",
title: "LfK! mailer API",
version: config.version
},
}
);
}

View File

@@ -1,31 +0,0 @@
import consola from "consola";
import "reflect-metadata";
import { createExpressServer } from "routing-controllers";
import { config, e as errors } from './config';
import loaders from "./loaders/index";
import AuthChecker from './middlewares/AuthChecker';
import { ErrorHandler } from './middlewares/ErrorHandler';
const CONTROLLERS_FILE_EXTENSION = process.env.NODE_ENV === 'production' ? 'js' : 'ts';
const app = createExpressServer({
middlewares: [ErrorHandler],
authorizationChecker: AuthChecker,
development: config.development,
cors: true,
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
});
async function main() {
await loaders(app);
app.listen(config.internal_port, () => {
consola.success(
`⚡️[server]: Server is running at http://localhost:${config.internal_port}`
);
});
}
if (errors === 0) {
main();
} else {
consola.error("error");
// something's wrong
}

View File

@@ -1,49 +0,0 @@
import consola from "consola";
import { config as configDotenv } from 'dotenv';
configDotenv();
export const config = {
internal_port: parseInt(process.env.APP_PORT) || 4010,
development: process.env.NODE_ENV === "production",
version: process.env.VERSION || require('../package.json').version,
api_key: getApiKey(),
app_url: process.env.APP_URL || "http://localhost:8080",
mail_server: process.env.MAIL_SERVER,
mail_port: Number(process.env.MAIL_PORT) || 25,
mail_user: process.env.MAIL_USER,
mail_password: process.env.MAIL_PASSWORD,
mail_from: process.env.MAIL_FROM,
privacy_url: process.env.PRIVACY_URL || "/privacy",
imprint_url: process.env.IMPRINT_URL || "/imprint",
copyright_owner: process.env.COPYRIGHT_OWNER || "LfK!",
event_name: process.env.EVENT_NAME || "Testing 4 Kaya!",
contact_mail: process.env.CONTACT_MAIL || process.env.MAIL_FROM,
}
let errors = 0
if (typeof config.internal_port !== "number") {
errors++
}
if (typeof config.development !== "boolean") {
errors++
}
function getApiKey(): string {
const key = process.env.API_KEY;
if (!key) {
consola.info("No API key set - generating a random one...");
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (var i = 0; i < 64; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
consola.info(`API KEY: ${result}`)
return result;
}
if (key.length < 64) {
consola.error(`API key is too short - minimum: 64, current: ${key.length}`)
throw new Error("API_KEY too short.")
}
return key
}
export let e = errors

15
src/config/env.ts Normal file
View File

@@ -0,0 +1,15 @@
export const config = {
smtp: {
host: process.env.SMTP_HOST || 'smtp.mailtrap.io',
port: Number(process.env.SMTP_PORT) || 2525,
user: process.env.SMTP_USER || '',
pass: process.env.SMTP_PASS || ''
},
email: {
from: process.env.EMAIL_FROM || 'noreply@example.com',
replyTo: process.env.EMAIL_REPLYTO || 'noreply@example.com',
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379'
}
}

View File

@@ -1,82 +0,0 @@
import { Authorized, Body, JsonController, Post, QueryParam } from 'routing-controllers';
import { OpenAPI } from 'routing-controllers-openapi';
import { Mailer } from '../Mailer';
import { locales } from '../models/LocaleEnum';
import { MailTypes } from '../models/MailTypeEnum';
import { ResetMail } from '../models/ResetMail';
import { SuccessResponse } from '../models/SuccessResponse';
import { WelcomeMail } from '../models/WelcomeMail';
/**
* The mail controller handels all endpoints concerning Mail sending.
*/
@JsonController()
@Authorized()
@OpenAPI({ security: [{ "AuthToken": [] }] })
export class MailController {
private mailer: Mailer = new Mailer();
private initialized: boolean = false;
@Post('/reset')
@OpenAPI({ description: "Sends reset mails", parameters: [{ in: "query", name: "locale", schema: { type: "string", enum: ["de", "en"] } }] })
async sendReset(@Body({ validate: true }) mailOptions: ResetMail, @QueryParam("locale") locale: locales) {
if (!this.initialized) {
await this.mailer.init();
this.initialized = true;
}
try {
this.mailer.sendResetMail(mailOptions.address, mailOptions.resetKey, locale?.toString())
} catch (error) {
throw error;
}
return new SuccessResponse(MailTypes.PASSWORD_RESET, locale);
}
@Post('/test')
@OpenAPI({ description: "Sends test mails", parameters: [{ in: "query", name: "locale", schema: { type: "string", enum: ["de", "en"] } }] })
async sendTest(@QueryParam("locale") locale: locales) {
if (!this.initialized) {
await this.mailer.init();
this.initialized = true;
}
try {
this.mailer.sendTestMail(locale?.toString())
} catch (error) {
console.log(error)
throw error;
}
return new SuccessResponse(MailTypes.TEST, locale);
}
@Post('/registration')
@OpenAPI({ description: "Sends registration welcome mails", parameters: [{ in: "query", name: "locale", schema: { type: "string", enum: ["de", "en"] } }] })
async sendRegistrationWelcome(@Body({ validate: true }) mailOptions: WelcomeMail, @QueryParam("locale") locale: locales) {
if (!this.initialized) {
await this.mailer.init();
this.initialized = true;
}
try {
this.mailer.sendWelcomeMail(mailOptions.address, mailOptions.selfserviceToken, locale?.toString())
} catch (error) {
console.log(error)
throw error;
}
return new SuccessResponse(MailTypes.RUNNER_WELCOME, locale);
}
@Post('/registration_forgot')
@OpenAPI({ description: "Sends selfservice link forgotten mails", parameters: [{ in: "query", name: "locale", schema: { type: "string", enum: ["de", "en"] } }] })
async sendSelfserviceForgotten(@Body({ validate: true }) mailOptions: WelcomeMail, @QueryParam("locale") locale: locales) {
if (!this.initialized) {
await this.mailer.init();
this.initialized = true;
}
try {
this.mailer.sendSelfserviceForgottenMail(mailOptions.address, mailOptions.selfserviceToken, locale?.toString())
} catch (error) {
console.log(error)
throw error;
}
return new SuccessResponse(MailTypes.RUNNER_FORGOT, locale);
}
}

View File

@@ -1,17 +0,0 @@
import { Get, JsonController } from 'routing-controllers';
import { OpenAPI } from 'routing-controllers-openapi';
import { config } from '../config';
/**
* The statuscontroller provides simple endpoints concerning basic information about the server.
*/
@JsonController()
export class StatusController {
@Get('/version')
@OpenAPI({ description: "A very basic endpoint that just returns the curent package version." })
getVersion() {
return {
"version": config.version
}
}
}

View File

@@ -1,12 +0,0 @@
import { IsString } from 'class-validator'
/**
* Error to throw when a permission couldn't be found.
*/
export class MailServerConfigError extends Error {
@IsString()
name = "MailServerConfigError"
@IsString()
message = "The SMTP server you provided couldn't be reached!"
}

26
src/index.ts Normal file
View File

@@ -0,0 +1,26 @@
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { emailRouter } from './routes/email'
import { createSwaggerUI } from './swagger'
const app = new Hono()
app.use('*', logger())
app.use('*', prettyJSON())
if ((process.env.AUTHKEY || "") === "") {
console.warn("process.env.AUTHKEY was not provided!");
}
app.route('/api/v1/email', emailRouter)
app.get('/docs', createSwaggerUI())
app.get('/swagger', createSwaggerUI())
const port = process.env.PORT || 3000
console.log(`Server is running on port ${port}`)
export default {
port,
fetch: app.fetch
}

View File

@@ -1,11 +0,0 @@
import { Application } from "express";
/**
* Loader for express related configurations.
* Configures proxy trusts, globally used middlewares and other express features.
*/
export default async (app: Application) => {
app.enable('trust proxy');
app.disable('x-powered-by');
app.disable('x-served-by');
return app;
};

View File

@@ -1,13 +0,0 @@
import { Application } from "express";
import expressLoader from "./express";
import openapiLoader from "./openapi";
/**
* Index Loader that executes the other loaders in the right order.
* This basicly exists for abstraction and a overall better dev experience.
*/
export default async (app: Application) => {
await openapiLoader(app);
await expressLoader(app);
return app;
};

View File

@@ -1,24 +0,0 @@
import { validationMetadatasToSchemas } from "@odit/class-validator-jsonschema";
import express, { Application } from "express";
import path from 'path';
import { getMetadataArgsStorage } from "routing-controllers";
import { generateSpec } from '../apispec';
/**
* Loader for everything openapi related - from creating the schema to serving it via a static route and swaggerUiExpress.
* All auth schema related stuff also has to be configured here
*/
export default async (app: Application) => {
const storage = getMetadataArgsStorage();
const schemas = validationMetadatasToSchemas({
refPointerPrefix: "#/components/schemas/",
});
//Spec creation based on the previously created schemas
const spec = generateSpec(storage, schemas);
app.get(["/docs/openapi.json", "/docs/swagger.json"], (req, res) => {
res.json(spec);
});
app.use('/docs', express.static(path.join(__dirname, '../static/docs'), { index: "index.html", extensions: ['html'] }));
return app;
};

View File

@@ -1,31 +0,0 @@
{
"a-password-reset-for-your-account-got-requested": "Ein Passwort Reset wurde für dein Konto beantragt.",
"all-rights-reserved": "Alle Rechte vorbehalten",
"event_name-registration": "{{event_name}} Registrierung",
"if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems": "Solltest du dich nicht selbst registriert haben, sende uns bitte eine Mail und wir entfernen deine Daten aus unserem System: ",
"if-you-didnt-request-the-reset-please-ignore-this-mail": "Solltest du den Reset nicht beantragt haben kannst du diese Mail einfach ignorieren.",
"if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website": "Auf unserer Website kannst du jederzeit einen neuen Login-Link beantragen:",
"imprint": "Impressum",
"lfk-mail-test": "{{copyright_owner}} - Mail test",
"lfk-password-reset": "{{copyright_owner}} - Passwort zurücksetzen",
"password-reset": "Passwort zurücksetzen",
"privacy": "Datenschutz",
"reset-password": "Passwort zurücksetzen",
"test-mail": "Test mail",
"thank-you-for-requesting-a-new-link-to-your-event_name-runner-profile": "Danke, dass du einen neuen Profillink für das {{event_name}} Läuferstem beantragt hast.",
"thanks-for-registering-and-welcome-to-the-event_name": "Vielen Dank für die Registrierung beim {{event_name}}",
"the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you": "Bitte bringe deinen Registrierungscode zum Lauf am 21.04.2023 mit.",
"this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend": "Das ist eine Testmail, die von einem Admin im LfK! Backend erzeugt wurde.",
"this-mail-was-sent-to-recipient_mail-because-someone-request-a-mail-test-for-this-mail-address": "Du bekommst diese Mail, weil jemand eine Testmail für deine Mail-Adresse angefragt hat.",
"this-mail-was-sent-to-you-because-someone-request-a-password-reset-for-a-account-linked-to-the-mail-address": "Du bekommst diese E-Mail, weil jemand einen Passwort-Reset für deinen Account beantragt hat.",
"this-mail-was-sent-to-you-because-someone-requested-a-new-link-to-access-your-profile-for-the-event_name": "Diese E-Mail wurde an dich gesendet, weil jemand einen neuen Link angefordert hat, um auf dein {{event_name}} Profil zuzugreifen.",
"this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name": "Du bekommst diese Mail, weil jemand deine E-Mail-Adresse verwendet hat, um sich beim {{event_name}} zu registrieren.",
"to-prevent-spam-you-can-only-request-a-new-link-every-24hrs": "Um Spam zu vermeiden, kannst du nur alle 24 Stunden einen neuen Link anfordern.",
"view-my-data": "Meine Daten",
"we-successfully-processed-your-registration": "Wir haben deine Registrierung erfolgreich verarbeitet.",
"welcome": "Willkommen",
"you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice": "Deinen Registrierungscode, Rundenzeiten und vieles mehr kannst du im Selfservice einsehen:",
"your-event_name-profile": "Dein {{event_name}} Profil",
"your-password-wont-be-changed-until-you-click-the-reset-link-below-and-set-a-new-one": "Dein Passwort wird erst zurückgesetzt, wenn du den Reset-Link öffnest und ein neues Passwort setzt.",
"your-profile": "Dein Profil"
}

View File

@@ -1,31 +0,0 @@
{
"a-password-reset-for-your-account-got-requested": "A password reset for your account got requested.",
"all-rights-reserved": "All rights reserved.",
"event_name-registration": "{{event_name}} Registration",
"if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems": "If you didn't register yourself you should contact us to get your data removed from our systems:",
"if-you-didnt-request-the-reset-please-ignore-this-mail": "If you didn't request the reset please ignore this mail.",
"if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website": "If you ever loose the link you can request a new one by visiting our website:",
"imprint": "Imprint",
"lfk-mail-test": "{{copyright_owner}} - Mail test",
"lfk-password-reset": "{{copyright_owner}} - Password reset",
"password-reset": "Password reset",
"privacy": "Privacy",
"reset-password": "Reset password",
"test-mail": "Test mail",
"thank-you-for-requesting-a-new-link-to-your-event_name-runner-profile": "Thank you for requesting a new link to your {{event_name}} runner profile.",
"thanks-for-registering-and-welcome-to-the-event_name": "Thanks for registering and welcome to the {{event_name}}!",
"the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you": "The only thing you have to do now is to bring your registration code with you.",
"this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend": "This is a test mail triggered by an admin in the LfK! backend.",
"this-mail-was-sent-to-recipient_mail-because-someone-request-a-mail-test-for-this-mail-address": "This mail was sent to you because someone request a mail test for this mail address.",
"this-mail-was-sent-to-you-because-someone-request-a-password-reset-for-a-account-linked-to-the-mail-address": "This mail was sent to you because someone request a password reset for a account linked to the mail address.",
"this-mail-was-sent-to-you-because-someone-requested-a-new-link-to-access-your-profile-for-the-event_name": "This mail was sent to you, because someone requested a new link to access your profile for the {{event_name}}.",
"this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name": "This mail was sent to you, because someone used your mail address to register themselves for the {{event_name}}",
"to-prevent-spam-you-can-only-request-a-new-link-every-24hrs": "To prevent spam you can only request a new link every 24hrs.",
"view-my-data": "View my data",
"we-successfully-processed-your-registration": "We successfully processed your registration.",
"welcome": "Welcome",
"you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice": "You can view your registration code, lap times and much more by visiting our selfservice:",
"your-event_name-profile": "Your {{event_name}} profile",
"your-password-wont-be-changed-until-you-click-the-reset-link-below-and-set-a-new-one": "Your password won't be changed until you click the reset link below and set a new one.",
"your-profile": "Your profile"
}

View File

@@ -1,14 +0,0 @@
import { Action } from "routing-controllers";
import { config } from '../config';
/**
* Handles authentication via jwt's (Bearer authorization header) for all api endpoints using the @Authorized decorator.
* @param action Routing-Controllers action object that provides request and response objects among other stuff.
* @param permissions The permissions that the endpoint using @Authorized requires.
*/
const AuthChecker = async (action: Action) => {
const provided_token = action.request.query.key;
return provided_token == config.api_key;
}
export default AuthChecker

View File

@@ -1,14 +0,0 @@
import { ExpressErrorMiddlewareInterface, Middleware } from "routing-controllers";
/**
* Our Error handling middlware that returns our custom httperrors to the user.
*/
@Middleware({ type: "after" })
export class ErrorHandler implements ExpressErrorMiddlewareInterface {
public error(error: any, request: any, response: any, next: (err: any) => any) {
if (response.headersSent) {
return;
}
response.json(error);
}
}

View File

@@ -1,4 +0,0 @@
export enum locales {
de = "de",
en = "en"
}

View File

@@ -1,6 +0,0 @@
export enum MailTypes {
PASSWORD_RESET = "PASSWORD_RESET",
RUNNER_FORGOT = "RUNNER_FORGOT",
RUNNER_WELCOME = "RUNNER_WELCOME",
TEST = "TEST"
}

View File

@@ -1,16 +0,0 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
/**
* Simple reset mail request class for validation and easier handling.
*/
export class ResetMail {
@IsString()
@IsEmail()
@IsNotEmpty()
address: string;
@IsString()
@IsNotEmpty()
resetKey: string;
}

View File

@@ -1,26 +0,0 @@
import { IsBoolean, IsString } from 'class-validator';
import { locales } from './LocaleEnum';
import { MailTypes } from './MailTypeEnum';
/**
* Simple success response class to make everyone happy :)
*/
export class SuccessResponse {
@IsBoolean()
success: boolean = true;
@IsString()
message: string = "Sent!"
@IsString()
locale: locales;
@IsString()
type: MailTypes;
constructor(type: MailTypes, locale?: locales) {
this.type = type;
this.locale = locale || locales.en;
}
}

View File

@@ -1,16 +0,0 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
/**
* Simple welcome mail request class for validation and easier handling.
*/
export class WelcomeMail {
@IsString()
@IsEmail()
@IsNotEmpty()
address: string;
@IsString()
@IsNotEmpty()
selfserviceToken: string;
}

78
src/queues/email.queue.ts Normal file
View File

@@ -0,0 +1,78 @@
import { createTransport } from "nodemailer";
import { Queue, Worker, QueueEvents } from "bullmq";
import { config } from "../config/env";
import Redis from "ioredis";
interface EmailJob {
to: string;
subject: string;
html: string;
text: string;
}
export const QUEUE_NAME = "{lfkmailqueue}";
const connection = new Redis(config.redis.url, {
maxRetriesPerRequest: null,
});
export const emailQueue = new Queue<EmailJob>(QUEUE_NAME, {
connection,
defaultJobOptions: {
attempts: 3,
backoff: {
type: "exponential",
delay: 1000,
},
},
});
const transporter = createTransport({
host: config.smtp.host,
port: config.smtp.port,
auth: {
user: config.smtp.user,
pass: config.smtp.pass,
},
});
const worker = new Worker<EmailJob>(
QUEUE_NAME,
async (job) => {
await transporter.sendMail({
from: config.email.from,
replyTo: config.email.replyTo,
to: job.data.to,
subject: job.data.subject,
text: job.data.text,
html: job.data.html,
});
},
{
connection,
concurrency: 5,
limiter: {
max: 250,
duration: 1000 * 60, // 250 emails per minute
},
}
);
const queueEvents = new QueueEvents(QUEUE_NAME, { connection });
worker.on("completed", (job) => {
console.log(`Email job ${job.id} completed`);
});
worker.on("failed", (job, error) => {
console.error(`Email job ${job?.id} failed:`, error);
});
queueEvents.on("waiting", ({ jobId }) => {
console.log(`Job ${jobId} is waiting`);
});
process.on("SIGTERM", async () => {
await worker.close();
await connection.quit();
});

82
src/routes/email.ts Normal file
View File

@@ -0,0 +1,82 @@
import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { EmailService } from '../services/email'
import { getEmailTemplate } from '../templates'
import { Language } from '../types'
import { toBuffer } from 'bwip-js/node'
const emailRouter = new Hono()
const emailService = new EmailService()
const sendEmailSchema = z.object({
to: z.string().email(),
templateName: z.string(),
language: z.enum(['en', 'de']),
data: z.record(z.any())
})
async function generateBarcodeDataURL(data) {
const buffer = await toBuffer({
bcid: 'code128',
text: data,
scale: 3,
height: 10,
includetext: true,
textxalign: 'center',
});
const base64Data = buffer.toString('base64');
const dataURL = `data:image/png;base64,${base64Data}`;
return dataURL;
}
emailRouter.post('/', bearerAuth({ token: process.env.AUTHKEY }), zValidator('json', sendEmailSchema), async (c) => {
let { to, templateName, language, data } = c.req.valid('json')
try {
const template = getEmailTemplate(templateName, language as Language)
if (templateName === "welcome") {
if (data.name && data.link && data.barcode_content) {
const dataURL = await generateBarcodeDataURL(data.barcode_content);
data.barcode_url = dataURL;
} else {
return c.json({ success: false, error: "required params 'data.name', 'data.link', 'data.barcode_content' not provided" }, 406)
}
}
if (templateName === "password-reset") {
if (data.token) {
console.log(data);
data.reset_link = `${process.env.FRONTEND_URL}/reset/${(Buffer.from(data.token)).toString("base64")}`
} else {
return c.json({ success: false, error: "'data.token' not provided" }, 406)
}
}
data.event_date = process.env.EVENT_DATE
data.event_name = process.env.EVENT_NAME
await emailService.sendEmail({
to,
subject: template.subject(data),
html: template.html(data),
text: template.text(data)
})
return c.json({ success: true })
} catch (error) {
return c.json({ success: false, error: (error as Error).message }, 400)
}
})
// Add queue status endpoint
emailRouter.get('/status', bearerAuth({ token: process.env.AUTHKEY }), async (c) => {
try {
const status = await emailService.getQueueStatus()
return c.json(status)
} catch (error) {
return c.json({ error: (error as Error).message }, 500)
}
})
export { emailRouter }

40
src/services/email.ts Normal file
View File

@@ -0,0 +1,40 @@
import { emailQueue } from '../queues/email.queue'
import { config } from '../config/env'
interface EmailOptions {
to: string
subject: string
html: string
text: string
}
export class EmailService {
async sendEmail(options: EmailOptions): Promise<void> {
const email = {
from: config.email.from,
...options
}
// Add to queue instead of sending directly
await emailQueue.add('send-email', email, {
removeOnComplete: true,
// removeOnFail: 1000
})
}
async getQueueStatus() {
const [waiting, active, completed, failed] = await Promise.all([
emailQueue.getWaitingCount(),
emailQueue.getActiveCount(),
emailQueue.getCompletedCount(),
emailQueue.getFailedCount()
])
return {
waiting,
active,
completed,
failed
}
}
}

View File

@@ -1,156 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Docs</title>
<style>
:root {
--bg-color: #fff;
--bg-secondary-color: #f3f3f6;
--color-primary: #14854f;
--color-lightGrey: #d2d6dd;
--color-grey: #747681;
--color-darkGrey: #3f4144;
--color-error: #d43939;
--color-success: #28bd14;
--grid-maxWidth: 120rem;
--grid-gutter: 2rem;
--font-size: 1.6rem;
--font-color: #333;
--font-family-sans: -apple-system, BlinkMacSystemFont, Avenir, "Avenir Next", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
--font-family-mono: monaco, "Consolas", "Lucida Console", monospace
}
html {
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-size: 62.5%;
line-height: 1.15;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%
}
*,
:after,
:before {
-webkit-box-sizing: inherit;
box-sizing: inherit
}
body {
background-color: var(--bg-color);
line-height: 1.6;
font-size: var(--font-size);
color: var(--font-color);
font-family: Segoe UI, Helvetica Neue, sans-serif;
font-family: var(--font-family-sans);
margin: 0;
padding: 0
}
h3 {
font-weight: 500;
margin: .35em 0 .7em
}
h3 {
font-size: 1.5em
}
a {
color: var(--color-primary);
text-decoration: none
}
a:hover:not(.button) {
opacity: .75
}
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]):not(:disabled):hover {
border-color: var(--color-grey)
}
::-webkit-input-placeholder {
color: #bdbfc4
}
::-moz-placeholder {
color: #bdbfc4
}
:-ms-input-placeholder {
color: #bdbfc4
}
::-ms-input-placeholder {
color: #bdbfc4
}
.tabs {
display: -webkit-box;
display: -ms-flexbox;
display: flex
}
.tabs a {
text-decoration: none
}
.tabs>a {
padding: 1rem 2rem;
-webkit-box-flex: 0;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
color: var(--color-darkGrey);
border-bottom: 2px solid var(--color-lightGrey);
text-align: center
}
.tabs>a:hover {
opacity: 1;
border-bottom: 2px solid var(--color-darkGrey)
}
.is-vertical-align {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
.is-center {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center
}
.is-center {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center
}
</style>
</head>
<body>
<div class="hero">
<div class="logo is-center is-vertical-align">
<h3>API Docs</h3>
</div>
<nav class="tabs is-center">
<a href="./redoc">ReDoc</a>
<a href="./swaggerui">SwaggerUI</a>
<a href="./rapidoc">RapiDoc</a>
<a href="./openapi.json">Raw Spec (json)</a>
</nav>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="./rapidoc-min.js"></script>
</head>
<body>
<rapi-doc
spec-url="/docs/openapi.json"
> </rapi-doc>
</body>
</html>

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='/docs/openapi.json'></redoc>
<script src="./redoc.standalone.js"> </script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +0,0 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "/docs/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

118
src/swagger.ts Normal file
View File

@@ -0,0 +1,118 @@
import { swaggerUI } from '@hono/swagger-ui'
const routes = {
'/api/v1/email': {
post: {
summary: 'Send an email',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['to', 'templateName', 'language', 'data'],
properties: {
to: {
type: 'string',
format: 'email'
},
templateName: {
type: 'string'
},
language: {
type: 'string',
enum: ['en', 'de']
},
data: {
type: 'object'
}
}
}
}
}
},
responses: {
'200': {
description: 'Email queued successfully',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean'
}
}
}
}
}
},
'400': {
description: 'Bad request or email queueing failed',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: {
type: 'boolean'
},
error: {
type: 'string'
}
}
}
}
}
}
}
},
get: {
path: '/status',
summary: 'Get email queue status',
responses: {
'200': {
description: 'Queue status retrieved successfully',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
waiting: {
type: 'number'
},
active: {
type: 'number'
},
completed: {
type: 'number'
},
failed: {
type: 'number'
}
}
}
}
}
}
}
}
}
}
const apiDoc = {
openapi: '3.0.0',
info: {
title: process.env.npm_package_name,
version: process.env.npm_package_version,
description: 'API for sending templated emails in multiple languages'
},
paths: routes
}
export const createSwaggerUI = () => {
return swaggerUI({
url: '/api/docs',
spec: apiDoc
})
}

View File

46
src/templates/index.ts Normal file
View File

@@ -0,0 +1,46 @@
import { Language } from '../types'
import { readFileSync } from 'fs'
import { join } from 'path'
import { compile } from 'handlebars'
interface TemplateCache {
[key: string]: {
html: HandlebarsTemplateDelegate
subject: HandlebarsTemplateDelegate,
text: HandlebarsTemplateDelegate
}
}
const templateCache: TemplateCache = {}
function loadTemplate(name: string, language: Language) {
const cacheKey = `${name}:${language}`
if (!templateCache[cacheKey]) {
const htmlPath = join(process.cwd(), 'src', 'templates', name, `${language}.html`)
const textPath = join(process.cwd(), 'src', 'templates', name, `${language}.txt`)
const subjectPath = join(process.cwd(), 'src', 'templates', name, `${language}.subject.txt`)
const htmlTemplate = readFileSync(htmlPath, 'utf-8')
const textTemplate = readFileSync(textPath, 'utf-8')
const subjectTemplate = readFileSync(subjectPath, 'utf-8')
templateCache[cacheKey] = {
html: compile(htmlTemplate),
subject: compile(subjectTemplate),
text: compile(textTemplate)
}
}
return templateCache[cacheKey]
}
export function getEmailTemplate(name: string, language: Language) {
const template = loadTemplate(name, language)
return {
subject: (data: any) => template.subject(data),
html: (data: any) => template.html(data),
text: (data: any) => template.text(data)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
Lauf für Kaya! - Passwort Reset

View File

@@ -0,0 +1,17 @@
Hallo 👋
Wir haben eine Anfrage zum Zurücksetzen deines Passworts erhalten.
Verwende den folgenden Link, um ein neues Passwort zu erstellen:
{{reset_link}}
Falls du diese Anfrage nicht gestellt hast, ignoriere bitte diese E-Mail.
Sportliche Grüße 🏃‍♂️
Dein Team Lauf für Kaya!
-
Lauf für Kaya!
Powered by ODIT.Services (https://odit.services)
Impressum: https://lauf-fuer-kaya.de/impressum/
Datenschutzerklärung: https://lauf-fuer-kaya.de/datenschutz/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
Lauf für Kaya! - Password Reset

View File

@@ -0,0 +1,17 @@
Hello 👋
We received a request to reset your password.
Use the following link to create a new password:
{{reset_link}}
If you didn't request this password reset, please ignore this email.
Sporty Greetings 🏃‍♂️
Your Team Lauf für Kaya!
-
Lauf für Kaya!
Powered by ODIT.Services (https://odit.services)
Imprint: https://lauf-fuer-kaya.de/impressum/
Privacy Policy: https://lauf-fuer-kaya.de/datenschutz/

View File

@@ -1,384 +0,0 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<title>{{__ "lfk-password-reset"}}</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no"> <!-- Tell iOS not to automatically link certain text strings. -->
<!-- CSS Reset : BEGIN -->
<style>
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin:0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border: 0;
border-spacing: 0;
border-collapse: collapse
}
/* What it does: Forces Samsung Android mail clients to use the entire viewport. */
#MessageViewBody,
#MessageWebViewDiv{
width: 100% !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode:bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
/* What it does: A work-around for email clients automatically linking certain text strings. */
/* iOS */
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
u + #body a, /* Gmail */
#MessageViewBody a /* Samsung Mail */
{
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display:none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u ~ div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u ~ div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u ~ div .email-container {
min-width: 414px !important;
}
}
</style>
<!-- What it does: Helps DPI scaling in Outlook 2007-2013 -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- CSS Reset : END -->
<!-- Progressive Enhancements : BEGIN -->
<style>
/* What it does: Hover styles for buttons and tags */
.s-btn__primary:hover {
background: #0077CC !important;
border-color: #0077CC !important;
}
.s-btn__white:hover {
background: #EFF0F1 !important;
border-color: #EFF0F1 !important;
}
.s-btn__outlined:hover {
background: rgba(0,119,204,.05) !important;
color: #005999 !important;
}
.s-tag:hover,
.post-tag:hover {
border-color: #cee0ed !important;
background: #cee0ed !important;
}
/* What it does: Styles markdown links that we can't write inline CSS for. */
.has-markdown a,
.has-markdown a:visited {
color: #0077CC !important;
text-decoration: none !important;
}
/* What it does: Styles markdown code blocks that we can't write inline CSS for. */
code {
padding: 1px 5px;
background-color: #EFF0F1;
color: #242729;
font-size: 13px;
line-height: inherit;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
pre {
margin: 0 0 15px;
line-height: 17px;
background-color: #EFF0F1;
padding: 4px 8px;
border-radius: 3px;
overflow-x: auto;
}
pre code {
margin: 0 0 15px;
padding: 0;
line-height: 17px;
background-color: none;
}
/* What it does: Styles markdown blockquotes that we can't write inline CSS for. */
blockquote {
margin: 0 0 15px;
padding: 4px 10px;
background-color: #FFF8DC;
border-left: 2px solid #ffeb8e;
}
blockquote p {
padding: 4px 0;
margin: 0;
overflow-wrap: break-word;
}
/* What it does: Rounds corners in email clients that support it */
.bar {
border-radius: 5px;
}
.btr {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.bbr {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
@media screen and (max-width: 680px) {
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* Hides things in small viewports. */
.hide-on-mobile {
display: none !important;
max-height: 0 !important;
overflow: hidden !important;
visibility: hidden !important;
}
/* What it does: Utility classes to reduce spacing for smaller viewports. */
.sm-p-none {padding: 0 !important;}
.sm-pt-none {padding-top: 0 !important;}
.sm-pb-none {padding-bottom: 0 !important;}
.sm-pr-none {padding-right: 0 !important;}
.sm-pl-none {padding-left: 0 !important;}
.sm-px-none {padding-left: 0 !important; padding-right: 0 !important;}
.sm-py-none {padding-top: 0 !important; padding-bottom: 0 !important;}
.sm-p {padding: 20px !important;}
.sm-pt {padding-top: 20px !important;}
.sm-pb {padding-bottom: 20px !important;}
.sm-pr {padding-right: 20px !important;}
.sm-pl {padding-left: 20px !important;}
.sm-px {padding-left: 20px !important; padding-right: 20px !important;}
.sm-py {padding-top: 20px !important; padding-bottom: 20px !important;}
.sm-mb {margin-bottom: 20px !important;}
/* What it does: Utility classes to kill border radius for smaller viewports. Used mainly on the email's main container(s). */
.bar,
.btr,
.bbr {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
</style>
<!-- Progressive Enhancements : END -->
</head>
<!--
The email background color is defined in three places, just below. If you change one, remember to change the others.
1. body tag: for most email clients
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
3. mso conditional: For Windows 10 Mail
-->
<body width="100%" style="margin: 0; padding: 0 !important; background: #f3f3f5; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #f3f3f5;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f3f3f5;">
<tr>
<td>
<![endif]-->
<!-- Visually Hidden Preview Text : BEGIN -->
<div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
{{__ "lfk-password-reset"}}
</div>
<!-- Visually Hidden Preview Text : END -->
<div class="email-container" style="max-width: 680px; margin: 0 auto;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="max-width: 680px; width:100%">
<tr>
<td style="padding: 30px; background-color: #ffffff;" class="sm-p bar">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1 style="font-weight: bold; font-size: 27px; line-height: 27px; color: #0C0D0E; margin: 0 0 15px 0;">LfK!</h1>
</td>
</tr>
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1 style="font-weight: bold; font-size: 21px; line-height: 21px; color: #0C0D0E; margin: 0 0 15px 0;">{{__ "password-reset"}}</h1>
<p style="margin: 0 0 15px;" class="has-markdown">{{__ "a-password-reset-for-your-account-got-requested"}}<br><b>{{__ "if-you-didnt-request-the-reset-please-ignore-this-mail"}}</b><br>{{__ "your-password-wont-be-changed-until-you-click-the-reset-link-below-and-set-a-new-one"}}</p>
</td>
</tr>
<!-- Button Row : BEGIN -->
<tr>
<td>
<!-- Button : BEGIN -->
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="s-btn s-btn__primary" style="border-radius: 4px; background: #0095ff;">
<a class="s-btn s-btn__primary" href="{{reset_link}}" target="_parent" style="background: #0095FF; border: 1px solid #0077cc; box-shadow: inset 0 1px 0 0 rgba(102,191,255,.75); font-family: arial, sans-serif; font-size: 17px; line-height: 17px; color: #ffffff; text-align: center; text-decoration: none; padding: 13px 17px; display: block; border-radius: 4px; white-space: nowrap;">{{__ "reset-password"}}</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
<!-- Button Row : END -->
</table>
</td>
</tr>
<!-----------------------------
EMAIL BODY : END
------------------------------>
<!-- Footer : BEGIN -->
<tr>
<td style="padding: 30px;" class="sm-p">
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<!-- Subscription Info : BEGIN -->
<tr>
<td style="padding-bottom: 10px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}
</td>
</tr>
<tr>
<td style="font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
<a href="{{link_imprint}}"
style="color: #9199A1; text-decoration: underline;">{{__ "imprint"}}</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="{{link_privacy}}" style="color: #9199A1; text-decoration: underline;">{{__ "privacy"}}</a>
</td>
</tr>
<!-- Subscription Info : BEGIN -->
<!-- HR line : BEGIN -->
<tr>
<td style="padding: 30px 0;" width="100%" class="sm-py">
<table aria-hidden="true" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%">
<tr>
<td height="1" width="100%" style="font-size: 0; line-height: 0; border-top: 1px solid #D6D8DB;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- HR line : END -->
<tr>
<td style="padding-bottom: 5px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">{{__ "this-mail-was-sent-to-you-because-someone-request-a-password-reset-for-a-account-linked-to-the-mail-address"}}</td>
</tr>
<!-- Sender Info : END -->
</table>
</td>
</tr>
<!-- Footer : END -->
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@@ -1,12 +0,0 @@
{{__ "lfk-password-reset"}}
{{__ "a-password-reset-for-your-account-got-requested"}}
{{__ "if-you-didnt-request-the-reset-please-ignore-this-mail"}}
{{__ "your-password-wont-be-changed-until-you-click-the-reset-link-below-and-set-a-new-one"}}
{{__ "reset-password"}}: {{reset_link}}
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}.
{{__ "imprint"}}: {{link_imprint}} | {{__ "privacy"}}: {{link_privacy}}
{{__ "this-mail-was-sent-to-you-because-someone-request-a-password-reset-for-a-account-linked-to-the-mail-address"}}

View File

@@ -1,396 +0,0 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<title>{{__ "your-event_name-profile"}}</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no"> <!-- Tell iOS not to automatically link certain text strings. -->
<!-- CSS Reset : BEGIN -->
<style>
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin:0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border: 0;
border-spacing: 0;
border-collapse: collapse
}
/* What it does: Forces Samsung Android mail clients to use the entire viewport. */
#MessageViewBody,
#MessageWebViewDiv{
width: 100% !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode:bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
/* What it does: A work-around for email clients automatically linking certain text strings. */
/* iOS */
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
u + #body a, /* Gmail */
#MessageViewBody a /* Samsung Mail */
{
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display:none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u ~ div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u ~ div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u ~ div .email-container {
min-width: 414px !important;
}
}
</style>
<!-- What it does: Helps DPI scaling in Outlook 2007-2013 -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- CSS Reset : END -->
<!-- Progressive Enhancements : BEGIN -->
<style>
/* What it does: Hover styles for buttons and tags */
.s-btn__primary:hover {
background: #0077CC !important;
border-color: #0077CC !important;
}
.s-btn__white:hover {
background: #EFF0F1 !important;
border-color: #EFF0F1 !important;
}
.s-btn__outlined:hover {
background: rgba(0,119,204,.05) !important;
color: #005999 !important;
}
.s-tag:hover,
.post-tag:hover {
border-color: #cee0ed !important;
background: #cee0ed !important;
}
/* What it does: Styles markdown links that we can't write inline CSS for. */
.has-markdown a,
.has-markdown a:visited {
color: #0077CC !important;
text-decoration: none !important;
}
/* What it does: Styles markdown code blocks that we can't write inline CSS for. */
code {
padding: 1px 5px;
background-color: #EFF0F1;
color: #242729;
font-size: 13px;
line-height: inherit;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
pre {
margin: 0 0 15px;
line-height: 17px;
background-color: #EFF0F1;
padding: 4px 8px;
border-radius: 3px;
overflow-x: auto;
}
pre code {
margin: 0 0 15px;
padding: 0;
line-height: 17px;
background-color: none;
}
/* What it does: Styles markdown blockquotes that we can't write inline CSS for. */
blockquote {
margin: 0 0 15px;
padding: 4px 10px;
background-color: #FFF8DC;
border-left: 2px solid #ffeb8e;
}
blockquote p {
padding: 4px 0;
margin: 0;
overflow-wrap: break-word;
}
/* What it does: Rounds corners in email clients that support it */
.bar {
border-radius: 5px;
}
.btr {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.bbr {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
@media screen and (max-width: 680px) {
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* Hides things in small viewports. */
.hide-on-mobile {
display: none !important;
max-height: 0 !important;
overflow: hidden !important;
visibility: hidden !important;
}
/* What it does: Utility classes to reduce spacing for smaller viewports. */
.sm-p-none {padding: 0 !important;}
.sm-pt-none {padding-top: 0 !important;}
.sm-pb-none {padding-bottom: 0 !important;}
.sm-pr-none {padding-right: 0 !important;}
.sm-pl-none {padding-left: 0 !important;}
.sm-px-none {padding-left: 0 !important; padding-right: 0 !important;}
.sm-py-none {padding-top: 0 !important; padding-bottom: 0 !important;}
.sm-p {padding: 20px !important;}
.sm-pt {padding-top: 20px !important;}
.sm-pb {padding-bottom: 20px !important;}
.sm-pr {padding-right: 20px !important;}
.sm-pl {padding-left: 20px !important;}
.sm-px {padding-left: 20px !important; padding-right: 20px !important;}
.sm-py {padding-top: 20px !important; padding-bottom: 20px !important;}
.sm-mb {margin-bottom: 20px !important;}
/* What it does: Utility classes to kill border radius for smaller viewports. Used mainly on the email's main container(s). */
.bar,
.btr,
.bbr {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
</style>
<!-- Progressive Enhancements : END -->
</head>
<!--
The email background color is defined in three places, just below. If you change one, remember to change the others.
1. body tag: for most email clients
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
3. mso conditional: For Windows 10 Mail
-->
<body width="100%" style="margin: 0; padding: 0 !important; background: #f3f3f5; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #f3f3f5;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f3f3f5;">
<tr>
<td>
<![endif]-->
<!-- Visually Hidden Preview Text : BEGIN -->
<div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
{{__ "your-event_name-profile"}}
</div>
<!-- Visually Hidden Preview Text : END -->
<div class="email-container" style="max-width: 680px; margin: 0 auto;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="max-width: 680px; width:100%">
<tr>
<td style="padding: 30px; background-color: #ffffff;" class="sm-p bar">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1 style="font-weight: bold; font-size: 27px; line-height: 27px; color: #0C0D0E; margin: 0 0 15px 0;">{{event_name}}</h1>
</td>
</tr>
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1 style="font-weight: bold; font-size: 21px; line-height: 21px; color: #0C0D0E; margin: 0 0 15px 0;">{{__ "your-profile"}}</h1>
<p style="margin: 0 0 15px;" class="has-markdown">
{{__ "thank-you-for-requesting-a-new-link-to-your-event_name-runner-profile"}}<br>
{{__ "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice"}}
</p>
</td>
</tr>
<!-- Button Row : BEGIN -->
<tr>
<td>
<!-- Button : BEGIN -->
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="s-btn s-btn__primary" style="border-radius: 4px; background: #0095ff;">
<a class="s-btn s-btn__primary" href="{{selfservice_link}}" target="_parent" style="background: #0095FF; border: 1px solid #0077cc; box-shadow: inset 0 1px 0 0 rgba(102,191,255,.75); font-family: arial, sans-serif; font-size: 17px; line-height: 17px; color: #ffffff; text-align: center; text-decoration: none; padding: 13px 17px; display: block; border-radius: 4px; white-space: nowrap;">{{__ "view-my-data"}}</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<p>Link: <a href="{{selfservice_link}}">{{selfservice_link}}</a><br>
{{__ "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website"}} <a href="{{forgot_link}}">{{forgot_link}}</a></p></td>
</tr>
<!-- Button Row : END -->
</table>
</td>
</tr>
<!-----------------------------
EMAIL BODY : END
------------------------------>
<!-- Footer : BEGIN -->
<tr>
<td style="padding: 30px;" class="sm-p">
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<!-- Subscription Info : BEGIN -->
<tr>
<td style="padding-bottom: 10px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}
</td>
</tr>
<tr>
<td style="font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
<a href="{{link_imprint}}"
style="color: #9199A1; text-decoration: underline;">{{__ "imprint"}}</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="{{link_privacy}}" style="color: #9199A1; text-decoration: underline;">{{__ "privacy"}}</a>
</td>
</tr>
<!-- Subscription Info : BEGIN -->
<!-- HR line : BEGIN -->
<tr>
<td style="padding: 30px 0;" width="100%" class="sm-py">
<table aria-hidden="true" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%">
<tr>
<td height="1" width="100%" style="font-size: 0; line-height: 0; border-top: 1px solid #D6D8DB;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- HR line : END -->
<tr>
<td style="padding-bottom: 5px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
{{__ "this-mail-was-sent-to-you-because-someone-requested-a-new-link-to-access-your-profile-for-the-event_name"}}<br>
{{__ "to-prevent-spam-you-can-only-request-a-new-link-every-24hrs"}}<br>
{{__ "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems"}} <a href="mailto:{{contact_mail}}">{{contact_mail}}</a>
</td>
</tr>
<!-- Sender Info : END -->
</table>
</td>
</tr>
<!-- Footer : END -->
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@@ -1,15 +0,0 @@
{{__ "your-event_name-profile"}}
{{__ "thank-you-for-requesting-a-new-link-to-your-event_name-runner-profile"}}
{{__ "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice"}}
{{selfservice_link}}
{{__ "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website"}} {{forgot_link}}
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}.
{{__ "imprint"}}: {{link_imprint}} | {{__ "privacy"}}: {{link_privacy}}
{{__ "this-mail-was-sent-to-you-because-someone-requested-a-new-link-to-access-your-profile-for-the-event_name"}}
{{__ "to-prevent-spam-you-can-only-request-a-new-link-every-24hrs"}}
{{__ "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems"}} {{contact_mail}}

View File

@@ -1,450 +0,0 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<title>{{__ "lfk-mail-test"}}</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<!-- Tell iOS not to automatically link certain text strings. -->
<!-- CSS Reset : BEGIN -->
<style>
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border: 0;
border-spacing: 0;
border-collapse: collapse
}
/* What it does: Forces Samsung Android mail clients to use the entire viewport. */
#MessageViewBody,
#MessageWebViewDiv {
width: 100% !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
/* What it does: A work-around for email clients automatically linking certain text strings. */
/* iOS */
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
u+#body a,
/* Gmail */
#MessageViewBody a
/* Samsung Mail */
{
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img+div {
display: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!-- What it does: Helps DPI scaling in Outlook 2007-2013 -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- CSS Reset : END -->
<!-- Progressive Enhancements : BEGIN -->
<style>
/* What it does: Hover styles for buttons and tags */
.s-btn__primary:hover {
background: #0077CC !important;
border-color: #0077CC !important;
}
.s-btn__white:hover {
background: #EFF0F1 !important;
border-color: #EFF0F1 !important;
}
.s-btn__outlined:hover {
background: rgba(0, 119, 204, .05) !important;
color: #005999 !important;
}
.s-tag:hover,
.post-tag:hover {
border-color: #cee0ed !important;
background: #cee0ed !important;
}
/* What it does: Styles markdown links that we can't write inline CSS for. */
.has-markdown a,
.has-markdown a:visited {
color: #0077CC !important;
text-decoration: none !important;
}
/* What it does: Styles markdown code blocks that we can't write inline CSS for. */
code {
padding: 1px 5px;
background-color: #EFF0F1;
color: #242729;
font-size: 13px;
line-height: inherit;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
pre {
margin: 0 0 15px;
line-height: 17px;
background-color: #EFF0F1;
padding: 4px 8px;
border-radius: 3px;
overflow-x: auto;
}
pre code {
margin: 0 0 15px;
padding: 0;
line-height: 17px;
background-color: none;
}
/* What it does: Styles markdown blockquotes that we can't write inline CSS for. */
blockquote {
margin: 0 0 15px;
padding: 4px 10px;
background-color: #FFF8DC;
border-left: 2px solid #ffeb8e;
}
blockquote p {
padding: 4px 0;
margin: 0;
overflow-wrap: break-word;
}
/* What it does: Rounds corners in email clients that support it */
.bar {
border-radius: 5px;
}
.btr {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.bbr {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
@media screen and (max-width: 680px) {
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* Hides things in small viewports. */
.hide-on-mobile {
display: none !important;
max-height: 0 !important;
overflow: hidden !important;
visibility: hidden !important;
}
/* What it does: Utility classes to reduce spacing for smaller viewports. */
.sm-p-none {
padding: 0 !important;
}
.sm-pt-none {
padding-top: 0 !important;
}
.sm-pb-none {
padding-bottom: 0 !important;
}
.sm-pr-none {
padding-right: 0 !important;
}
.sm-pl-none {
padding-left: 0 !important;
}
.sm-px-none {
padding-left: 0 !important;
padding-right: 0 !important;
}
.sm-py-none {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.sm-p {
padding: 20px !important;
}
.sm-pt {
padding-top: 20px !important;
}
.sm-pb {
padding-bottom: 20px !important;
}
.sm-pr {
padding-right: 20px !important;
}
.sm-pl {
padding-left: 20px !important;
}
.sm-px {
padding-left: 20px !important;
padding-right: 20px !important;
}
.sm-py {
padding-top: 20px !important;
padding-bottom: 20px !important;
}
.sm-mb {
margin-bottom: 20px !important;
}
/* What it does: Utility classes to kill border radius for smaller viewports. Used mainly on the email's main container(s). */
.bar,
.btr,
.bbr {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
</style>
<!-- Progressive Enhancements : END -->
</head>
<!--
The email background color is defined in three places, just below. If you change one, remember to change the others.
1. body tag: for most email clients
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
3. mso conditional: For Windows 10 Mail
-->
<body width="100%" style="margin: 0; padding: 0 !important; background: #f3f3f5; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #f3f3f5;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f3f3f5;">
<tr>
<td>
<![endif]-->
<!-- Visually Hidden Preview Text : BEGIN -->
<div
style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
{{__ "lfk-mail-test"}}
</div>
<!-- Visually Hidden Preview Text : END -->
<div class="email-container" style="max-width: 680px; margin: 0 auto;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="max-width: 680px; width:100%">
<tr>
<td style="padding: 30px; background-color: #ffffff;" class="sm-p bar">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tr>
<td
style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1
style="font-weight: bold; font-size: 27px; line-height: 27px; color: #0C0D0E; margin: 0 0 15px 0;">
LfK!</h1>
</td>
</tr>
<tr>
<td
style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1
style="font-weight: bold; font-size: 21px; line-height: 21px; color: #0C0D0E; margin: 0 0 15px 0;">
{{__ "test-mail"}}</h1>
<p style="margin: 0 0 15px;" class="has-markdown">{{__ "this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend"}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-----------------------------
EMAIL BODY : END
------------------------------>
<!-- Footer : BEGIN -->
<tr>
<td style="padding: 30px;" class="sm-p">
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<!-- Subscription Info : BEGIN -->
<tr>
<td
style="padding-bottom: 10px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}
</td>
</tr>
<tr>
<td
style="font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
<a href="{{link_imprint}}" style="color: #9199A1; text-decoration: underline;">{{__ "imprint"}}</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="{{link_privacy}}" style="color: #9199A1; text-decoration: underline;">{{__ "privacy"}}</a>
</td>
</tr>
<!-- Subscription Info : BEGIN -->
<!-- HR line : BEGIN -->
<tr>
<td style="padding: 30px 0;" width="100%" class="sm-py">
<table aria-hidden="true" border="0" cellpadding="0" cellspacing="0"
role="presentation" style="width:100%">
<tr>
<td height="1" width="100%"
style="font-size: 0; line-height: 0; border-top: 1px solid #D6D8DB;">
&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- HR line : END -->
<tr>
<td
style="padding-bottom: 5px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
{{__ "this-mail-was-sent-to-recipient_mail-because-someone-request-a-mail-test-for-this-mail-address"}}
</tr>
<!-- Sender Info : END -->
</table>
</td>
</tr>
<!-- Footer : END -->
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@@ -1,7 +0,0 @@
{{__ "lfk-mail-test"}}.
{{__ "this-is-a-test-mail-triggered-by-an-admin-in-the-lfk-backend"}}
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}.
{{__ "imprint"}}: {{link_imprint}} | {{__ "privacy"}}: {{link_privacy}}
{{__ "this-mail-was-sent-to-recipient_mail-because-someone-request-a-mail-test-for-this-mail-address"}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
Willkommen beim {{event_name}}

View File

@@ -0,0 +1,20 @@
Hallo {{name}} 👋
vielen Dank für deine Registrierung beim {{event_name}}
Am Lauftag ({{event_date}}) musst du nur noch deinen Barcode vorzeigen, damit erhältst du deine Läuferkarte.
Deinen Registrierungs-Code, Rundenzeiten und weitere Infos kannst du jederzeit im Lauf für Kaya! Selfservice unter {{link}} einsehen.
Wir freuen uns schon auf dich und einen erfolgreichen Lauf für Kaya!
Bei Fragen antworte gerne jederzeit auf diese E-Mail oder schreibe an info@lauf-fuer-kaya.de
Sportliche Grüße 🏃‍♂️
Dein Team Lauf für Kaya!
-
Lauf für Kaya!
Powered by ODIT.Services (https://odit.services)
Impressum: https://lauf-fuer-kaya.de/impressum/
Datenschutzerklärung: https://lauf-fuer-kaya.de/datenschutz/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
Welcome to {{event_name}}

View File

@@ -0,0 +1,20 @@
Hello {{name}} 👋
Thank you for registering for the {{event_name}}
On the day of the run ({{event_date}}) you only have to show your barcode to receive your runner's card.
You can view your registration code, lap times and further information at any time from the Lauf für Kaya! Selfservice at {{link}}.
We look forward to seeing you and to a successful Lauf für Kaya!
If you have any questions, please reply to this e-mail at any time or write to info@lauf-fuer-kaya.de
Sporty Greetings 🏃‍♂️
Your Team Lauf für Kaya!
-
Lauf für Kaya!
Powered by ODIT.Services (https://odit.services)
Imprint: https://lauf-fuer-kaya.de/impressum/
Privacy Policy: https://lauf-fuer-kaya.de/datenschutz/

View File

@@ -1,397 +0,0 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<title>{{__ "event_name-registration"}}</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no"> <!-- Tell iOS not to automatically link certain text strings. -->
<!-- CSS Reset : BEGIN -->
<style>
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin:0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border: 0;
border-spacing: 0;
border-collapse: collapse
}
/* What it does: Forces Samsung Android mail clients to use the entire viewport. */
#MessageViewBody,
#MessageWebViewDiv{
width: 100% !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode:bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
/* What it does: A work-around for email clients automatically linking certain text strings. */
/* iOS */
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
u + #body a, /* Gmail */
#MessageViewBody a /* Samsung Mail */
{
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* What it does: Prevents Gmail from displaying an download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img + div {
display:none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u ~ div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u ~ div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u ~ div .email-container {
min-width: 414px !important;
}
}
</style>
<!-- What it does: Helps DPI scaling in Outlook 2007-2013 -->
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- CSS Reset : END -->
<!-- Progressive Enhancements : BEGIN -->
<style>
/* What it does: Hover styles for buttons and tags */
.s-btn__primary:hover {
background: #0077CC !important;
border-color: #0077CC !important;
}
.s-btn__white:hover {
background: #EFF0F1 !important;
border-color: #EFF0F1 !important;
}
.s-btn__outlined:hover {
background: rgba(0,119,204,.05) !important;
color: #005999 !important;
}
.s-tag:hover,
.post-tag:hover {
border-color: #cee0ed !important;
background: #cee0ed !important;
}
/* What it does: Styles markdown links that we can't write inline CSS for. */
.has-markdown a,
.has-markdown a:visited {
color: #0077CC !important;
text-decoration: none !important;
}
/* What it does: Styles markdown code blocks that we can't write inline CSS for. */
code {
padding: 1px 5px;
background-color: #EFF0F1;
color: #242729;
font-size: 13px;
line-height: inherit;
font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
}
pre {
margin: 0 0 15px;
line-height: 17px;
background-color: #EFF0F1;
padding: 4px 8px;
border-radius: 3px;
overflow-x: auto;
}
pre code {
margin: 0 0 15px;
padding: 0;
line-height: 17px;
background-color: none;
}
/* What it does: Styles markdown blockquotes that we can't write inline CSS for. */
blockquote {
margin: 0 0 15px;
padding: 4px 10px;
background-color: #FFF8DC;
border-left: 2px solid #ffeb8e;
}
blockquote p {
padding: 4px 0;
margin: 0;
overflow-wrap: break-word;
}
/* What it does: Rounds corners in email clients that support it */
.bar {
border-radius: 5px;
}
.btr {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.bbr {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
@media screen and (max-width: 680px) {
/* What it does: Forces table cells into full-width rows. */
.stack-column,
.stack-column-center {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
}
/* And center justify these ones. */
.stack-column-center {
text-align: center !important;
}
/* Hides things in small viewports. */
.hide-on-mobile {
display: none !important;
max-height: 0 !important;
overflow: hidden !important;
visibility: hidden !important;
}
/* What it does: Utility classes to reduce spacing for smaller viewports. */
.sm-p-none {padding: 0 !important;}
.sm-pt-none {padding-top: 0 !important;}
.sm-pb-none {padding-bottom: 0 !important;}
.sm-pr-none {padding-right: 0 !important;}
.sm-pl-none {padding-left: 0 !important;}
.sm-px-none {padding-left: 0 !important; padding-right: 0 !important;}
.sm-py-none {padding-top: 0 !important; padding-bottom: 0 !important;}
.sm-p {padding: 20px !important;}
.sm-pt {padding-top: 20px !important;}
.sm-pb {padding-bottom: 20px !important;}
.sm-pr {padding-right: 20px !important;}
.sm-pl {padding-left: 20px !important;}
.sm-px {padding-left: 20px !important; padding-right: 20px !important;}
.sm-py {padding-top: 20px !important; padding-bottom: 20px !important;}
.sm-mb {margin-bottom: 20px !important;}
/* What it does: Utility classes to kill border radius for smaller viewports. Used mainly on the email's main container(s). */
.bar,
.btr,
.bbr {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
</style>
<!-- Progressive Enhancements : END -->
</head>
<!--
The email background color is defined in three places, just below. If you change one, remember to change the others.
1. body tag: for most email clients
2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
3. mso conditional: For Windows 10 Mail
-->
<body width="100%" style="margin: 0; padding: 0 !important; background: #f3f3f5; mso-line-height-rule: exactly;">
<center style="width: 100%; background: #f3f3f5;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f3f3f5;">
<tr>
<td>
<![endif]-->
<!-- Visually Hidden Preview Text : BEGIN -->
<div style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
{{__ "event_name-registration"}}
</div>
<!-- Visually Hidden Preview Text : END -->
<div class="email-container" style="max-width: 680px; margin: 0 auto;">
<!--[if mso]>
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="680" align="center">
<tr>
<td>
<![endif]-->
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="max-width: 680px; width:100%">
<tr>
<td style="padding: 30px; background-color: #ffffff;" class="sm-p bar">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1 style="font-weight: bold; font-size: 27px; line-height: 27px; color: #0C0D0E; margin: 0 0 15px 0;">{{event_name}}</h1>
</td>
</tr>
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<h1 style="font-weight: bold; font-size: 21px; line-height: 21px; color: #0C0D0E; margin: 0 0 15px 0;">{{__ "welcome"}}</h1>
<p style="margin: 0 0 15px;" class="has-markdown">
{{__ "thanks-for-registering-and-welcome-to-the-event_name"}} <br>
{{__ "we-successfully-processed-your-registration"}}<br><br>
{{__ "the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you"}}<br>
{{__ "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice"}}
</p>
</td>
</tr>
<!-- Button Row : BEGIN -->
<tr>
<td>
<!-- Button : BEGIN -->
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="s-btn s-btn__primary" style="border-radius: 4px; background: #0095ff;">
<a class="s-btn s-btn__primary" href="{{selfservice_link}}" target="_parent" style="background: #0095FF; border: 1px solid #0077cc; box-shadow: inset 0 1px 0 0 rgba(102,191,255,.75); font-family: arial, sans-serif; font-size: 17px; line-height: 17px; color: #ffffff; text-align: center; text-decoration: none; padding: 13px 17px; display: block; border-radius: 4px; white-space: nowrap;">{{__ "view-my-data"}}</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
<tr>
<td style="padding-bottom: 15px; font-family: arial, sans-serif; font-size: 15px; line-height: 21px; color: #3C3F44; text-align: left;">
<p>Link: <a href="{{selfservice_link}}">{{selfservice_link}}</a><br>
{{__ "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website"}} <a href="{{forgot_link}}">{{forgot_link}}</a></p></td>
</tr>
<!-- Button Row : END -->
</table>
</td>
</tr>
<!-----------------------------
EMAIL BODY : END
------------------------------>
<!-- Footer : BEGIN -->
<tr>
<td style="padding: 30px;" class="sm-p">
<table align="left" border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<!-- Subscription Info : BEGIN -->
<tr>
<td style="padding-bottom: 10px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}
</td>
</tr>
<tr>
<td style="font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
<a href="{{link_imprint}}"
style="color: #9199A1; text-decoration: underline;">{{__ "imprint"}}</a>&nbsp;&nbsp;&nbsp;&nbsp;
<a href="{{link_privacy}}" style="color: #9199A1; text-decoration: underline;">{{__ "privacy"}}</a>
</td>
</tr>
<!-- Subscription Info : BEGIN -->
<!-- HR line : BEGIN -->
<tr>
<td style="padding: 30px 0;" width="100%" class="sm-py">
<table aria-hidden="true" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%">
<tr>
<td height="1" width="100%" style="font-size: 0; line-height: 0; border-top: 1px solid #D6D8DB;">&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- HR line : END -->
<tr>
<td style="padding-bottom: 5px; font-size: 12px; line-height: 15px; font-family: arial, sans-serif; color: #9199A1; text-align: left;">
{{__ "this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name"}}.<br>
{{__ "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems"}} <a href="mailto:{{contact_mail}}">{{contact_mail}}</a>
</td>
</tr>
<!-- Sender Info : END -->
</table>
</td>
</tr>
<!-- Footer : END -->
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@@ -1,17 +0,0 @@
{{__ "event_name-registration"}}
{{__ "thanks-for-registering-and-welcome-to-the-event_name"}}
{{__ "we-successfully-processed-your-registration"}}
{{__ "the-only-thing-you-have-to-do-now-is-to-bring-your-registration-code-with-you"}}
{{__ "you-can-view-your-registration-code-lap-times-and-much-more-by-visiting-our-selfservice"}}
{{selfservice_link}}
{{__ "if-you-ever-loose-the-link-you-can-request-a-new-one-by-visiting-our-website"}} {{forgot_link}}
Copyright © {{copyright_owner}}. {{__ "all-rights-reserved"}}.
{{__ "imprint"}}: {{link_imprint}} | {{__ "privacy"}}: {{link_privacy}}
{{__ "this-mail-was-sent-to-you-because-someone-used-your-mail-address-to-register-themselfes-for-the-event_name"}}
{{__ "if-you-didnt-register-yourself-you-should-contact-us-to-get-your-data-removed-from-our-systems"}} {{contact_mail}}

View File

@@ -1,34 +0,0 @@
import axios from 'axios';
import { config } from '../config';
const base = "http://localhost:" + config.internal_port
describe('GET /docs/openapi.json', () => {
it('OpenAPI Spec is availdable 200', async () => {
const res = await axios.get(base + '/docs/openapi.json');
expect(res.status).toEqual(200);
});
});
describe('GET /docs/swagger.json', () => {
it('OpenAPI Spec is availdable 200', async () => {
const res = await axios.get(base + '/docs/swagger.json');
expect(res.status).toEqual(200);
});
});
describe('GET /docs/swaggerui', () => {
it('swaggerui is availdable 200', async () => {
const res = await axios.get(base + '/docs/swaggerui');
expect(res.status).toEqual(200);
});
});
describe('GET /docs/redoc', () => {
it('redoc is availdable 200', async () => {
const res = await axios.get(base + '/docs/redoc');
expect(res.status).toEqual(200);
});
});
describe('GET /docs/rapidoc', () => {
it('rapidoc is availdable 200', async () => {
const res = await axios.get(base + '/docs/rapidoc');
expect(res.status).toEqual(200);
});
});

View File

@@ -1,75 +0,0 @@
import axios from 'axios';
import { config } from '../config';
const base = "http://localhost:" + config.internal_port
const axios_config = {
validateStatus: undefined
};
describe('POST /reset without auth', () => {
it('Post without auth should return 401', async () => {
const res = await axios.post(base + '/reset', null, axios_config);
expect(res.status).toEqual(401);
});
});
describe('POST /reset with auth but wrong body', () => {
it('Post with auth but no body should return 400', async () => {
const res = await axios.post(base + '/reset?key=' + config.api_key, null, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but no mail should return 400', async () => {
const res = await axios.post(base + '/reset?key=' + config.api_key, { resetKey: "test" }, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but no reset key should return 400', async () => {
const res = await axios.post(base + '/reset?key=' + config.api_key, { address: "test@dev.lauf-fuer-kaya.de" }, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but invalid mail should return 400', async () => {
const res = await axios.post(base + '/reset?key=' + config.api_key, { resetKey: "test", address: "testdev.l.de" }, axios_config);
expect(res.status).toEqual(400);
});
});
describe('POST /reset with auth and vaild body', () => {
it('Post with auth, body and no locale should return 200', async () => {
const res = await axios.post(base + '/reset?key=' + config.api_key, {
resetKey: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "PASSWORD_RESET"
})
});
it('Post with auth, body and locale=en should return 200', async () => {
const res = await axios.post(base + '/reset?locale=en&key=' + config.api_key, {
resetKey: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "PASSWORD_RESET"
})
});
it('Post with auth, body and locale=de should return 200', async () => {
const res = await axios.post(base + '/reset?locale=de&key=' + config.api_key, {
resetKey: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "de",
type: "PASSWORD_RESET"
})
});
});

View File

@@ -1,75 +0,0 @@
import axios from 'axios';
import { config } from '../config';
const base = "http://localhost:" + config.internal_port
const axios_config = {
validateStatus: undefined
};
describe('POST /registration_forgot without auth', () => {
it('Post without auth should return 401', async () => {
const res = await axios.post(base + '/registration_forgot', null, axios_config);
expect(res.status).toEqual(401);
});
});
describe('POST /registration_forgot with auth but wrong body', () => {
it('Post with auth but no body should return 400', async () => {
const res = await axios.post(base + '/registration_forgot?key=' + config.api_key, null, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but no mail should return 400', async () => {
const res = await axios.post(base + '/registration_forgot?key=' + config.api_key, { selfserviceToken: "test" }, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but no reset key should return 400', async () => {
const res = await axios.post(base + '/registration_forgot?key=' + config.api_key, { address: "test@dev.lauf-fuer-kaya.de" }, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but invalid mail should return 400', async () => {
const res = await axios.post(base + '/registration_forgot?key=' + config.api_key, { selfserviceToken: "test", address: "testdev.l.de" }, axios_config);
expect(res.status).toEqual(400);
});
});
describe('POST /reset with auth and vaild body', () => {
it('Post with auth, body and no locale should return 200', async () => {
const res = await axios.post(base + '/registration_forgot?key=' + config.api_key, {
selfserviceToken: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "RUNNER_FORGOT"
})
});
it('Post with auth, body and locale=en should return 200', async () => {
const res = await axios.post(base + '/registration_forgot?locale=en&key=' + config.api_key, {
selfserviceToken: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "RUNNER_FORGOT"
})
});
it('Post with auth, body and locale=de should return 200', async () => {
const res = await axios.post(base + '/registration_forgot?locale=de&key=' + config.api_key, {
selfserviceToken: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "de",
type: "RUNNER_FORGOT"
})
});
});

View File

@@ -1,75 +0,0 @@
import axios from 'axios';
import { config } from '../config';
const base = "http://localhost:" + config.internal_port
const axios_config = {
validateStatus: undefined
};
describe('POST /registration without auth', () => {
it('Post without auth should return 401', async () => {
const res = await axios.post(base + '/registration', null, axios_config);
expect(res.status).toEqual(401);
});
});
describe('POST /registration with auth but wrong body', () => {
it('Post with auth but no body should return 400', async () => {
const res = await axios.post(base + '/registration?key=' + config.api_key, null, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but no mail should return 400', async () => {
const res = await axios.post(base + '/registration?key=' + config.api_key, { selfserviceToken: "test" }, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but no reset key should return 400', async () => {
const res = await axios.post(base + '/registration?key=' + config.api_key, { address: "test@dev.lauf-fuer-kaya.de" }, axios_config);
expect(res.status).toEqual(400);
});
it('Post with auth but invalid mail should return 400', async () => {
const res = await axios.post(base + '/registration?key=' + config.api_key, { selfserviceToken: "test", address: "testdev.l.de" }, axios_config);
expect(res.status).toEqual(400);
});
});
describe('POST /reset with auth and vaild body', () => {
it('Post with auth, body and no locale should return 200', async () => {
const res = await axios.post(base + '/registration?key=' + config.api_key, {
selfserviceToken: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "RUNNER_WELCOME"
})
});
it('Post with auth, body and locale=en should return 200', async () => {
const res = await axios.post(base + '/registration?locale=en&key=' + config.api_key, {
selfserviceToken: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "RUNNER_WELCOME"
})
});
it('Post with auth, body and locale=de should return 200', async () => {
const res = await axios.post(base + '/registration?locale=de&key=' + config.api_key, {
selfserviceToken: "test",
address: "test@dev.lauf-fuer-kaya.de"
}, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "de",
type: "RUNNER_WELCOME"
})
});
});

View File

@@ -1,47 +0,0 @@
import axios from 'axios';
import { config } from '../config';
const base = "http://localhost:" + config.internal_port
const axios_config = {
validateStatus: undefined
};
describe('POST /test without auth', () => {
it('Post without auth should return 401', async () => {
const res = await axios.post(base + '/test', null, axios_config);
expect(res.status).toEqual(401);
});
});
describe('POST /test with auth', () => {
it('Post with auth and no locale should return 200', async () => {
const res = await axios.post(base + '/test?key=' + config.api_key, null, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "TEST"
})
});
it('Post with auth and locale=en should return 200', async () => {
const res = await axios.post(base + '/test?locale=en&key=' + config.api_key, null, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "en",
type: "TEST"
})
});
it('Post with auth and locale=de should return 200', async () => {
const res = await axios.post(base + '/test?locale=de&key=' + config.api_key, null, axios_config);
expect(res.status).toEqual(200);
expect(res.data).toEqual({
success: true,
message: "Sent!",
locale: "de",
type: "TEST"
})
});
});

6
src/types.ts Normal file
View File

@@ -0,0 +1,6 @@
export type Language = 'en' | 'de'
export interface EmailTemplate {
html: (data: any) => string
text: (data: any) => string
}