diff --git a/.vscode/settings.json b/.vscode/settings.json index 2532d6e..34de47f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ - "Hutten" + "Hutten", + "sendmail" ] } diff --git a/app/.env.defaults b/app/.env.defaults index 2568da0..b8293a4 100644 --- a/app/.env.defaults +++ b/app/.env.defaults @@ -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-----" diff --git a/app/api/package.json b/app/api/package.json index 726461b..83c4250 100644 --- a/app/api/package.json +++ b/app/api/package.json @@ -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" } } diff --git a/app/api/src/functions/identity-signup.js b/app/api/src/functions/identity-signup.js index 4d128cc..61426fe 100644 --- a/app/api/src/functions/identity-signup.js +++ b/app/api/src/functions/identity-signup.js @@ -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' diff --git a/app/api/src/graphql/email.sdl.ts b/app/api/src/graphql/email.sdl.ts new file mode 100644 index 0000000..81ae8fa --- /dev/null +++ b/app/api/src/graphql/email.sdl.ts @@ -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! + } +` diff --git a/app/api/src/lib/auth.js b/app/api/src/lib/auth.ts similarity index 97% rename from app/api/src/lib/auth.js rename to app/api/src/lib/auth.ts index bab25b9..539b0e9 100644 --- a/app/api/src/lib/auth.js +++ b/app/api/src/lib/auth.ts @@ -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.") } diff --git a/app/api/src/lib/sendmail.ts b/app/api/src/lib/sendmail.ts new file mode 100644 index 0000000..c4d216e --- /dev/null +++ b/app/api/src/lib/sendmail.ts @@ -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 { + 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 + return emailPromise +} + diff --git a/app/api/src/services/email/email.ts b/app/api/src/services/email/email.ts new file mode 100644 index 0000000..a45a178 --- /dev/null +++ b/app/api/src/services/email/email.ts @@ -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, + }) +} diff --git a/app/api/src/services/users/users.js b/app/api/src/services/users/users.ts similarity index 100% rename from app/api/src/services/users/users.js rename to app/api/src/services/users/users.ts diff --git a/app/redwood.toml b/app/redwood.toml index f5c2b01..762f83b 100644 --- a/app/redwood.toml +++ b/app/redwood.toml @@ -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] diff --git a/app/web/src/Routes.js b/app/web/src/Routes.js index e7ec58d..5d4dfc8 100644 --- a/app/web/src/Routes.js +++ b/app/web/src/Routes.js @@ -62,6 +62,7 @@ const Routes = () => { + ) diff --git a/app/web/src/pages/AdminEmailPage/AdminEmailPage.tsx b/app/web/src/pages/AdminEmailPage/AdminEmailPage.tsx new file mode 100644 index 0000000..c36d18e --- /dev/null +++ b/app/web/src/pages/AdminEmailPage/AdminEmailPage.tsx @@ -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 ( +
+
+

Email all users

+ + setSubject(target.value)}/> + + + +
+ +
+ ) +} + +export default AdminEmailPage diff --git a/app/yarn.lock b/app/yarn.lock index c4b5ddd..d66da50 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -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"