Add email integration to be able to broadcast emails to all users.
Resolves #388
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Hutten"
|
||||
"Hutten",
|
||||
"sendmail"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,3 +17,14 @@ CLOUDINARY_API_KEY=476712943135152
|
||||
# See: https://redwoodjs.com/docs/logger for level options:
|
||||
# trace | info | debug | warn | error | silent
|
||||
# LOG_LEVEL=debug
|
||||
|
||||
|
||||
# EMAIL_PASSWORD=abc123
|
||||
|
||||
|
||||
CAD_LAMBDA_BASE_URL="http://localhost:8080"
|
||||
|
||||
# sentry
|
||||
GITHUB_ASSIST_APP_ID=23342
|
||||
GITHUB_ASSIST_SECRET=abc
|
||||
GITHUB_ASSIST_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nabcdefg\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
"dependencies": {
|
||||
"@redwoodjs/api": "^0.34.1",
|
||||
"@sentry/node": "^6.5.1",
|
||||
"cloudinary": "^1.23.0"
|
||||
"cloudinary": "^1.23.0",
|
||||
"nodemailer": "^6.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "^6.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createUserInsecure } from 'src/services/users/users.js'
|
||||
import { createUserInsecure } from 'src/services/users/users'
|
||||
import { db } from 'src/lib/db'
|
||||
import { sentryWrapper } from 'src/lib/sentry'
|
||||
import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers'
|
||||
|
||||
22
app/api/src/graphql/email.sdl.ts
Normal file
22
app/api/src/graphql/email.sdl.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const schema = gql`
|
||||
type Envelope {
|
||||
from: String
|
||||
to: [String!]!
|
||||
}
|
||||
|
||||
type EmailResponse {
|
||||
accepted: [String!]!
|
||||
rejected: [String!]!
|
||||
messageId: String!
|
||||
envelope: Envelope
|
||||
}
|
||||
|
||||
input Email {
|
||||
subject: String!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
sendAllUsersEmail(input: Email!): EmailResponse!
|
||||
}
|
||||
`
|
||||
@@ -121,7 +121,8 @@ export const getCurrentUser = async (decoded, { _token, _type }) => {
|
||||
* requireAuth({ role: ['editor', 'author'] })
|
||||
* requireAuth({ role: ['publisher'] })
|
||||
*/
|
||||
export const requireAuth = ({ role } = {}) => {
|
||||
export const requireAuth = ({ role }: {role?: string | string[]} = {}) => {
|
||||
console.log(context.currentUser)
|
||||
if (!context.currentUser) {
|
||||
throw new AuthenticationError("You don't have permission to do that.")
|
||||
}
|
||||
56
app/api/src/lib/sendmail.ts
Normal file
56
app/api/src/lib/sendmail.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import nodemailer, {SendMailOptions} from 'nodemailer'
|
||||
|
||||
interface Args {
|
||||
to: SendMailOptions['to']
|
||||
from: SendMailOptions['from']
|
||||
subject: string
|
||||
text: string
|
||||
}
|
||||
|
||||
interface SuccessResult {
|
||||
accepted: string[]
|
||||
rejected: string[]
|
||||
envelopeTime: number
|
||||
messageTime: number
|
||||
messageSize: number
|
||||
response: string
|
||||
envelope: {
|
||||
from: string | false,
|
||||
to: string[]
|
||||
},
|
||||
messageId: string
|
||||
}
|
||||
|
||||
export function sendMail({to, from, subject, text}: Args): Promise<SuccessResult> {
|
||||
let transporter = nodemailer.createTransport({
|
||||
host: 'smtp.mailgun.org',
|
||||
port: 587,
|
||||
secure: false,
|
||||
tls: {
|
||||
ciphers:'SSLv3'
|
||||
},
|
||||
auth: {
|
||||
user: 'postmaster@mail.cadhub.xyz',
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
console.log({to, from, subject, text});
|
||||
|
||||
const emailPromise = new Promise((resolve, reject) => {
|
||||
transporter.sendMail({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
}, (error, info) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(info);
|
||||
}
|
||||
});
|
||||
}) as any as Promise<SuccessResult>
|
||||
return emailPromise
|
||||
}
|
||||
|
||||
18
app/api/src/services/email/email.ts
Normal file
18
app/api/src/services/email/email.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { requireAuth } from 'src/lib/auth'
|
||||
import {sendMail} from 'src/lib/sendmail'
|
||||
import {users} from 'src/services/users/users'
|
||||
|
||||
export const sendAllUsersEmail = async ({input: {body, subject}}) => {
|
||||
requireAuth({ role: 'admin' })
|
||||
const recipients = (await users()).map(({email}) => email)
|
||||
const from = {
|
||||
address:'news@mail.cadhub.xyz',
|
||||
name: 'CadHub',
|
||||
}
|
||||
return sendMail({
|
||||
to: recipients,
|
||||
from,
|
||||
subject,
|
||||
text: body,
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,8 @@
|
||||
'SENTRY_DSN',
|
||||
'SENTRY_AUTH_TOKEN',
|
||||
'SENTRY_ORG',
|
||||
'SENTRY_PROJECT'
|
||||
'SENTRY_PROJECT',
|
||||
'EMAIL_PASSWORD'
|
||||
]
|
||||
# experimentalFastRefresh = true # this seems to break cascadeStudio
|
||||
[api]
|
||||
|
||||
@@ -62,6 +62,7 @@ const Routes = () => {
|
||||
<Route path="/admin/subject-access-requests/{id}/edit" page={EditSubjectAccessRequestPage} name="editSubjectAccessRequest" />
|
||||
<Route path="/admin/subject-access-requests/{id}" page={SubjectAccessRequestPage} name="subjectAccessRequest" />
|
||||
<Route path="/admin/subject-access-requests" page={SubjectAccessRequestsPage} name="subjectAccessRequests" />
|
||||
<Route path="/admin/email" page={AdminEmailPage} name="adminEmail" />
|
||||
</Private>
|
||||
</Router>
|
||||
)
|
||||
|
||||
41
app/web/src/pages/AdminEmailPage/AdminEmailPage.tsx
Normal file
41
app/web/src/pages/AdminEmailPage/AdminEmailPage.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import {useState} from 'react'
|
||||
import { useMutation } from '@redwoodjs/web'
|
||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
const SEND_EMAIL_MUTATION = gql`
|
||||
mutation sendEmailMutation($email: Email!) {
|
||||
sendAllUsersEmail(input: $email) {
|
||||
accepted
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const AdminEmailPage = () => {
|
||||
const [subject, setSubject] = useState('')
|
||||
const [body, setBody] = useState('')
|
||||
const [sendEmailMutation] = useMutation(SEND_EMAIL_MUTATION, {
|
||||
onCompleted: ({sendAllUsersEmail}) => {
|
||||
toast.success(`Emails sent, ${sendAllUsersEmail?.accepted.join(', ')}`)
|
||||
setSubject('')
|
||||
setBody('')
|
||||
},
|
||||
})
|
||||
|
||||
const sendEmail = () => sendEmailMutation({ variables: { email: { subject, body } } })
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<div className="max-w-7xl pt-8">
|
||||
<h2 className="" style={{width: '46rem'}}>Email all users</h2>
|
||||
<label htmlFor="subject">Subject</label>
|
||||
<input name="subject" className="rounded border border-gray-400 px-2 w-full" value={subject} onChange={({target}) => setSubject(target.value)}/>
|
||||
<label htmlFor="body">Body</label>
|
||||
<textarea className="w-full rounded border border-gray-400 p-2" name="text" value={body} onChange={({target}) => setBody(target.value)}></textarea>
|
||||
<button className="rounded px-2 p-1 mt-4 bg-ch-purple-400 text-indigo-200" onClick={sendEmail}>Send</button>
|
||||
</div>
|
||||
<Toaster timeout={1500} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminEmailPage
|
||||
@@ -3963,6 +3963,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
|
||||
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
|
||||
|
||||
"@types/nodemailer@^6.4.2":
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.2.tgz#d8ee254c969e6ad83fb9a0a0df3a817406a3fa3b"
|
||||
integrity sha512-yhsqg5Xbr8aWdwjFS3QjkniW5/tLpWXtOYQcJdo9qE3DolBxsKzgRCQrteaMY0hos8MklJNSEsMqDpZynGzMNg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
@@ -13070,6 +13077,11 @@ node-releases@^1.1.29, node-releases@^1.1.61, node-releases@^1.1.71:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
|
||||
integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
|
||||
|
||||
nodemailer@^6.6.2:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.2.tgz#e184c9ed5bee245a3e0bcabc7255866385757114"
|
||||
integrity sha512-YSzu7TLbI+bsjCis/TZlAXBoM4y93HhlIgo0P5oiA2ua9Z4k+E2Fod//ybIzdJxOlXGRcHIh/WaeCBehvxZb/Q==
|
||||
|
||||
normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
|
||||
|
||||
Reference in New Issue
Block a user