refactor: move to bun
This commit is contained in:
198
src/Mailer.ts
198
src/Mailer.ts
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
31
src/app.ts
31
src/app.ts
@@ -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
|
||||
}
|
||||
@@ -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
15
src/config/env.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
26
src/index.ts
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum locales {
|
||||
de = "de",
|
||||
en = "en"
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export enum MailTypes {
|
||||
PASSWORD_RESET = "PASSWORD_RESET",
|
||||
RUNNER_FORGOT = "RUNNER_FORGOT",
|
||||
RUNNER_WELCOME = "RUNNER_WELCOME",
|
||||
TEST = "TEST"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
78
src/queues/email.queue.ts
Normal 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
82
src/routes/email.ts
Normal 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
40
src/services/email.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
220
src/static/docs/rapidoc-min.js
vendored
220
src/static/docs/rapidoc-min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
@@ -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
@@ -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
118
src/swagger.ts
Normal 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
|
||||
})
|
||||
}
|
||||
46
src/templates/index.ts
Normal file
46
src/templates/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
298
src/templates/password-reset/de.html
Normal file
298
src/templates/password-reset/de.html
Normal file
File diff suppressed because one or more lines are too long
1
src/templates/password-reset/de.subject.txt
Normal file
1
src/templates/password-reset/de.subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
Lauf für Kaya! - Passwort Reset
|
||||
17
src/templates/password-reset/de.txt
Normal file
17
src/templates/password-reset/de.txt
Normal 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/
|
||||
297
src/templates/password-reset/en.html
Normal file
297
src/templates/password-reset/en.html
Normal file
File diff suppressed because one or more lines are too long
1
src/templates/password-reset/en.subject.txt
Normal file
1
src/templates/password-reset/en.subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
Lauf für Kaya! - Password Reset
|
||||
17
src/templates/password-reset/en.txt
Normal file
17
src/templates/password-reset/en.txt
Normal 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/
|
||||
@@ -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>
|
||||
<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;"> </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>
|
||||
@@ -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"}}
|
||||
@@ -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>
|
||||
<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;"> </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>
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
<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;">
|
||||
</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>
|
||||
@@ -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"}}
|
||||
377
src/templates/welcome/de.html
Normal file
377
src/templates/welcome/de.html
Normal file
File diff suppressed because one or more lines are too long
1
src/templates/welcome/de.subject.txt
Normal file
1
src/templates/welcome/de.subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
Willkommen beim {{event_name}}
|
||||
20
src/templates/welcome/de.txt
Normal file
20
src/templates/welcome/de.txt
Normal 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/
|
||||
372
src/templates/welcome/en.html
Normal file
372
src/templates/welcome/en.html
Normal file
File diff suppressed because one or more lines are too long
1
src/templates/welcome/en.subject.txt
Normal file
1
src/templates/welcome/en.subject.txt
Normal file
@@ -0,0 +1 @@
|
||||
Welcome to {{event_name}}
|
||||
20
src/templates/welcome/en.txt
Normal file
20
src/templates/welcome/en.txt
Normal 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/
|
||||
@@ -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>
|
||||
<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;"> </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>
|
||||
@@ -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}}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -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
6
src/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Language = 'en' | 'de'
|
||||
|
||||
export interface EmailTemplate {
|
||||
html: (data: any) => string
|
||||
text: (data: any) => string
|
||||
}
|
||||
Reference in New Issue
Block a user