feat(backend): Added flows endpoint for translator invites
This commit is contained in:
parent
064b058fd6
commit
1bc256fb05
12 changed files with 249 additions and 0 deletions
75
sites/backend/openapi/flows.mjs
Normal file
75
sites/backend/openapi/flows.mjs
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { jwt, key, fields, parameters, response, errorExamples, jsonResponse } from './lib.mjs'
|
||||
|
||||
const common = {
|
||||
tags: ['Workflows'],
|
||||
security: [jwt, key],
|
||||
}
|
||||
|
||||
const local = {
|
||||
params: {
|
||||
id: {
|
||||
in: 'path',
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: "The Set's unique ID",
|
||||
schema: {
|
||||
example: 666,
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Paths
|
||||
export const paths = {}
|
||||
|
||||
// Create set
|
||||
paths['/flows/translator-invite/{auth}'] = {
|
||||
post: {
|
||||
...common,
|
||||
summary: 'Sends out an invite to join a FreeSewing translation team',
|
||||
description:
|
||||
'Will trigger an invite to be sent out to the authenticated user that allows them to join a FreeSewing translation team',
|
||||
parameters: [parameters.auth],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
language: {
|
||||
description: `Language code of the translation team the user wants to join`,
|
||||
type: 'string',
|
||||
example: 'es',
|
||||
enum: ['es', 'de', 'fr', 'nl', 'uk'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
...response.status['200'],
|
||||
...jsonResponse({
|
||||
result: { ...fields.result, example: 'sent' },
|
||||
}),
|
||||
},
|
||||
400: {
|
||||
...response.status['400'],
|
||||
description:
|
||||
response.status['400'].description +
|
||||
errorExamples(['languageMissing', 'languageInvalid']),
|
||||
},
|
||||
401: response.status['401'],
|
||||
403: {
|
||||
...response.status['403'],
|
||||
description:
|
||||
response.status['403'].description +
|
||||
errorExamples(['accountStatusLacking', 'insufficientAccessLevel']),
|
||||
},
|
||||
500: response.status['500'],
|
||||
},
|
||||
},
|
||||
}
|
|
@ -6,6 +6,7 @@ import { paths as patternPaths } from './patterns.mjs'
|
|||
import { paths as setPaths } from './sets.mjs'
|
||||
import { paths as curatedSetPaths } from './curated-sets.mjs'
|
||||
import { paths as userPaths } from './users.mjs'
|
||||
import { paths as flowPaths } from './flows.mjs'
|
||||
|
||||
const description = `
|
||||
## What am I looking at? 🤔
|
||||
|
@ -53,5 +54,6 @@ export const openapi = {
|
|||
...setPaths,
|
||||
...curatedSetPaths,
|
||||
...userPaths,
|
||||
...flowPaths,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export const errors = {
|
|||
keyLevelExceedsRoleLevel:
|
||||
'The `level` field in the request body is higher than the `level` of the user creating the key. This is not allowed.',
|
||||
languageMissing: 'The `langauge` field was missing from the request body',
|
||||
languageInvalid: 'The `langauge` field holds a value that is invalid',
|
||||
levelMissing: 'The `level` field was missing from the request body',
|
||||
levelNotNumeric: 'The `level` field in the request body was a number',
|
||||
mfaActive: 'MFA is already activated on the account',
|
||||
|
|
|
@ -26,6 +26,9 @@ const envToBool = (input = 'no') => {
|
|||
return false
|
||||
}
|
||||
|
||||
// Save ourselves some typing
|
||||
const crowdinProject = 'https://translate.freesewing.org/project/freesewing/'
|
||||
|
||||
// Construct config object
|
||||
const baseConfig = {
|
||||
// Environment
|
||||
|
@ -65,6 +68,15 @@ const baseConfig = {
|
|||
github: {
|
||||
token: process.env.BACKEND_GITHUB_TOKEN,
|
||||
},
|
||||
crowdin: {
|
||||
invites: {
|
||||
nl: crowdinProject + 'invite?h=' + process.env.BACKEND_CROWDIN_INVITE_NL,
|
||||
fr: crowdinProject + 'invite?h=' + process.env.BACKEND_CROWDIN_INVITE_FR,
|
||||
de: crowdinProject + 'invite?h=' + process.env.BACKEND_CROWDIN_INVITE_DE,
|
||||
es: crowdinProject + 'invite?h=' + process.env.BACKEND_CROWDIN_INVITE_ES,
|
||||
uk: crowdinProject + 'invite?h=' + process.env.BACKEND_CROWDIN_INVITE_UK,
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
secretOrKey: encryptionKey,
|
||||
issuer: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
|
||||
|
@ -72,6 +84,7 @@ const baseConfig = {
|
|||
expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d',
|
||||
},
|
||||
languages: ['en', 'de', 'es', 'fr', 'nl'],
|
||||
translations: ['de', 'es', 'fr', 'nl', 'uk'],
|
||||
measies: measurements,
|
||||
mfa: {
|
||||
service: process.env.BACKEND_MFA_SERVICE || 'FreeSewing',
|
||||
|
|
14
sites/backend/src/controllers/flows.mjs
Normal file
14
sites/backend/src/controllers/flows.mjs
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { FlowModel } from '../models/flow.mjs'
|
||||
|
||||
export function FlowsController() {}
|
||||
|
||||
/*
|
||||
* Send out an invite for a translator who wants to join the team
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
FlowsController.prototype.sendTranslatorInvite = async (req, res, tools) => {
|
||||
const Flow = new FlowModel(tools)
|
||||
await Flow.sendTranslatorInvite(req)
|
||||
|
||||
return Flow.sendResponse(res)
|
||||
}
|
67
sites/backend/src/models/flow.mjs
Normal file
67
sites/backend/src/models/flow.mjs
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { log } from '../utils/log.mjs'
|
||||
import { UserModel } from './user.mjs'
|
||||
import { i18nUrl } from '../utils/index.mjs'
|
||||
|
||||
export function FlowModel(tools) {
|
||||
this.config = tools.config
|
||||
this.mailer = tools.email
|
||||
this.rbac = tools.rbac
|
||||
this.User = new UserModel(tools)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
FlowModel.prototype.sendTranslatorInvite = async function ({ body, user }) {
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
if (Object.keys(body).length < 1) return this.setResponse(400, 'postBodyMissing')
|
||||
if (!body.language) return this.setResponse(400, 'languageMissing')
|
||||
if (!this.config.translations.includes(body.language))
|
||||
return this.setResponse(400, 'languageInvalid')
|
||||
|
||||
// Load user making the call
|
||||
await this.User.revealAuthenticatedUser(user)
|
||||
|
||||
// Send the invite email
|
||||
await this.mailer.send({
|
||||
template: 'transinvite',
|
||||
language: body.language,
|
||||
to: this.User.clear.email,
|
||||
replacements: {
|
||||
actionUrl: this.config.crowdin.invites[body.language],
|
||||
whyUrl: i18nUrl(body.language, `/docs/faq/email/why-transinvite`),
|
||||
supportUrl: i18nUrl(body.language, `/patrons/join`),
|
||||
},
|
||||
})
|
||||
|
||||
return this.setResponse(200, 'sent', {})
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to set the response code, result, and body
|
||||
*
|
||||
* Will be used by this.sendResponse()
|
||||
*/
|
||||
FlowModel.prototype.setResponse = function (status = 200, error = false, data = {}) {
|
||||
this.response = {
|
||||
status,
|
||||
body: {
|
||||
result: 'success',
|
||||
...data,
|
||||
},
|
||||
}
|
||||
if (status > 201) {
|
||||
this.response.body.error = error
|
||||
this.response.body.result = 'error'
|
||||
this.error = true
|
||||
} else this.error = false
|
||||
if (status === 404) this.response.body = null
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to send response
|
||||
*/
|
||||
FlowModel.prototype.sendResponse = async function (res) {
|
||||
return res.status(this.response.status).send(this.response.body)
|
||||
}
|
|
@ -125,6 +125,23 @@ UserModel.prototype.loadAuthenticatedUser = async function (user) {
|
|||
return this
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads & reveals the user that is making the API request
|
||||
e
|
||||
* Stores result in this.record
|
||||
*/
|
||||
UserModel.prototype.revealAuthenticatedUser = async function (user) {
|
||||
if (!user) return this
|
||||
this.record = await this.prisma.user.findUnique({
|
||||
where: { id: user.uid },
|
||||
include: {
|
||||
apikeys: true,
|
||||
},
|
||||
})
|
||||
|
||||
return this.reveal()
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks this.record and sets a boolean to indicate whether
|
||||
* the user exists or not
|
||||
|
|
17
sites/backend/src/routes/flows.mjs
Normal file
17
sites/backend/src/routes/flows.mjs
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { FlowsController } from '../controllers/flows.mjs'
|
||||
|
||||
const Flow = new FlowsController()
|
||||
const jwt = ['jwt', { session: false }]
|
||||
const bsc = ['basic', { session: false }]
|
||||
|
||||
export function flowsRoutes(tools) {
|
||||
const { app, passport } = tools
|
||||
|
||||
// Send a translator invite
|
||||
app.post('/flows/translator-invite/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Flow.sendTranslatorInvite(req, res, tools)
|
||||
)
|
||||
app.post('/flows/translator-invite/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Flow.sendTranslatorInvite(req, res, tools)
|
||||
)
|
||||
}
|
|
@ -5,6 +5,7 @@ import { patternsRoutes } from './patterns.mjs'
|
|||
import { confirmationsRoutes } from './confirmations.mjs'
|
||||
import { curatedSetsRoutes } from './curated-sets.mjs'
|
||||
import { issuesRoutes } from './issues.mjs'
|
||||
import { flowsRoutes } from './flows.mjs'
|
||||
|
||||
export const routes = {
|
||||
apikeysRoutes,
|
||||
|
@ -14,4 +15,5 @@ export const routes = {
|
|||
confirmationsRoutes,
|
||||
curatedSetsRoutes,
|
||||
issuesRoutes,
|
||||
flowsRoutes,
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { passwordreset, translations as passwordresetTranslations } from './pass
|
|||
import { signup, translations as signupTranslations } from './signup/index.mjs'
|
||||
import { signupaea, translations as signupaeaTranslations } from './signup-aea/index.mjs'
|
||||
import { signupaed, translations as signupaedTranslations } from './signup-aed/index.mjs'
|
||||
import { transinvite, translations as transinviteTranslations } from './transinvite/index.mjs'
|
||||
// Shared translations
|
||||
import en from '../../../public/locales/en/shared.json' assert { type: 'json' }
|
||||
import de from '../../../public/locales/de/shared.json' assert { type: 'json' }
|
||||
|
@ -25,6 +26,7 @@ export const templates = {
|
|||
signup,
|
||||
'signup-aea': signupaea,
|
||||
'signup-aed': signupaed,
|
||||
transinvite,
|
||||
}
|
||||
|
||||
export const translations = {
|
||||
|
@ -36,5 +38,6 @@ export const translations = {
|
|||
signup: signupTranslations,
|
||||
'signup-aea': signupaeaTranslations,
|
||||
'signup-aed': signupaedTranslations,
|
||||
transinvite: transinviteTranslations,
|
||||
shared: { en, de, es, fr, nl },
|
||||
}
|
||||
|
|
32
sites/backend/src/templates/email/transinvite/index.mjs
Normal file
32
sites/backend/src/templates/email/transinvite/index.mjs
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { buttonRow, closingRow, headingRow, lead1Row, wrap } from '../shared/blocks.mjs'
|
||||
// Translations
|
||||
import en from '../../../../public/locales/en/transinvite.json' assert { type: 'json' }
|
||||
import de from '../../../../public/locales/de/transinvite.json' assert { type: 'json' }
|
||||
import es from '../../../../public/locales/es/transinvite.json' assert { type: 'json' }
|
||||
import fr from '../../../../public/locales/fr/transinvite.json' assert { type: 'json' }
|
||||
import nl from '../../../../public/locales/nl/transinvite.json' assert { type: 'json' }
|
||||
|
||||
export const transinvite = {
|
||||
html: wrap.html(`
|
||||
${headingRow.html}
|
||||
${lead1Row.html}
|
||||
${buttonRow.html}
|
||||
${closingRow.html}
|
||||
`),
|
||||
text: wrap.text(`
|
||||
{{{ heading }}}
|
||||
|
||||
{{{ textLead }}}
|
||||
|
||||
{{{ actionUrl }}}
|
||||
|
||||
{{{ closing }}}
|
||||
|
||||
{{{ greeting }}},
|
||||
joost
|
||||
|
||||
PS: {{{ text-ps }}} : {{{ supportUrl }}}
|
||||
`),
|
||||
}
|
||||
|
||||
export const translations = { en, de, es, fr, nl }
|
|
@ -0,0 +1,6 @@
|
|||
subject: "[FreeSewing] Your invitation to join FreeSewing's the translation team"
|
||||
heading: Join a FreeSewing translation team
|
||||
lead: 'To join the FreeSewing translation team, click the big black rectangle below:'
|
||||
text-lead: 'To join the FreeSewing translation team, click the link below:'
|
||||
button: Join the translation team
|
||||
closing: "This will take you to Crowdin, the translation platform we use."
|
Loading…
Add table
Add a link
Reference in a new issue