diff --git a/package.json b/package.json index f465ed3..8d7d63d 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "maxmind": "^4.3.11", "morgan": "^1.10.0", "nanoid": "^3.3.6", + "nodemailer": "^6.9.5", "openbadge": "^1.0.4", "passport": "^0.6.0", "passport-jwt": "^4.0.1", @@ -68,6 +69,7 @@ "@types/lodash-es": "^4.17.9", "@types/morgan": "^1.9.5", "@types/node": "^18.17.12", + "@types/nodemailer": "^6.4.11", "@types/passport": "^1.0.12", "@types/passport-jwt": "^3.0.9", "@types/react": "^18.2.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58f2d73..a29022f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ dependencies: nanoid: specifier: ^3.3.6 version: 3.3.6 + nodemailer: + specifier: ^6.9.5 + version: 6.9.5 openbadge: specifier: ^1.0.4 version: 1.0.4 @@ -162,6 +165,9 @@ devDependencies: '@types/node': specifier: ^18.17.12 version: 18.17.12 + '@types/nodemailer': + specifier: ^6.4.11 + version: 6.4.11 '@types/passport': specifier: ^1.0.12 version: 1.0.12 @@ -2029,6 +2035,12 @@ packages: /@types/node@18.17.12: resolution: {integrity: sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==} + /@types/nodemailer@6.4.11: + resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==} + dependencies: + '@types/node': 18.17.12 + dev: true + /@types/passport-jwt@3.0.9: resolution: {integrity: sha512-5XJt+79emfgpuBvBQusUPylFIVtW1QVAAkTRwCbRJAmxUjmLtIqUU6V1ovpnHPu6Qut3mR5Juc+s7kd06roNTg==} dependencies: @@ -4159,6 +4171,11 @@ packages: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true + /nodemailer@6.9.5: + resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==} + engines: {node: '>=6.0.0'} + dev: false + /nodemon@2.0.22: resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==} engines: {node: '>=8.10.0'} diff --git a/src/client/api/trpc.ts b/src/client/api/trpc.ts index 683202d..c78f0c4 100644 --- a/src/client/api/trpc.ts +++ b/src/client/api/trpc.ts @@ -1,7 +1,8 @@ import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../../server/trpc'; -import { httpBatchLink } from '@trpc/client'; +import { httpBatchLink, TRPCClientErrorLike } from '@trpc/client'; import { getJWT } from './auth'; +import { message } from 'antd'; export const trpc = createTRPCReact(); @@ -17,3 +18,23 @@ export const trpcClient = trpc.createClient({ }), ], }); + +/** + * @usage + * trpc..useMutation({ + * onSuccess: defaultSuccessHandler, + * }); + */ +export function defaultSuccessHandler(data: any) { + message.success('Operate Success'); +} + +/** + * @usage + * trpc..useMutation({ + * onError: defaultErrorHandler, + * }); + */ +export function defaultErrorHandler(error: TRPCClientErrorLike) { + message.error(error.message); +} diff --git a/src/client/components/modals/NotificationInfo/index.tsx b/src/client/components/modals/NotificationInfo/index.tsx index 3417014..a8fd4a3 100644 --- a/src/client/components/modals/NotificationInfo/index.tsx +++ b/src/client/components/modals/NotificationInfo/index.tsx @@ -3,12 +3,19 @@ import { Form, FormProps, Input, + message, Modal, ModalProps, Select, } from 'antd'; import React, { useMemo } from 'react'; +import { + defaultErrorHandler, + defaultSuccessHandler, + trpc, +} from '../../../api/trpc'; import { useEvent } from '../../../hooks/useEvent'; +import { useCurrentWorkspaceId } from '../../../store/user'; import { notificationStrategies } from './strategies'; export interface NotificationFormValues { @@ -32,6 +39,12 @@ export const NotificationInfoModal: React.FC = React.memo((props) => { const [form] = Form.useForm(); const typeValue = Form.useWatch('type', form); + const currentWorkspaceId = useCurrentWorkspaceId()!; + + const testMutation = trpc.notification.test.useMutation({ + onSuccess: defaultSuccessHandler, + onError: defaultErrorHandler, + }); const formEl = useMemo(() => { const strategy = notificationStrategies.find((s) => s.name === typeValue); @@ -63,7 +76,12 @@ export const NotificationInfoModal: React.FC = const values = form.getFieldsValue(); const { name, type, payload } = values; - console.log('TODO', { name, type, payload }); + await testMutation.mutateAsync({ + workspaceId: currentWorkspaceId, + name, + type, + payload, + }); }); return ( @@ -76,7 +94,9 @@ export const NotificationInfoModal: React.FC = onCancel={props.onCancel} footer={
- + diff --git a/src/server/model/notification.ts b/src/server/model/notification.ts deleted file mode 100644 index acd67e1..0000000 --- a/src/server/model/notification.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { prisma } from './_client'; - -export function getWorkspaceNotifications(workspaceId: string) { - return prisma.notification.findMany({ - where: { - workspaceId, - }, - }); -} - -export async function createWorkspaceNotification( - workspaceId: string, - name: string, - type: string, - payload: Record -) { - const notification = await prisma.notification.create({ - data: { - workspaceId, - name, - type, - payload, - }, - }); - - return notification; -} diff --git a/src/server/model/notification/index.ts b/src/server/model/notification/index.ts new file mode 100644 index 0000000..cd8fcda --- /dev/null +++ b/src/server/model/notification/index.ts @@ -0,0 +1,15 @@ +import { Notification } from '@prisma/client'; +import { notificationProviders } from './provider'; + +export async function sendNotification( + notification: Pick, + message: string +) { + const type = notification.type; + + if (!notificationProviders[type]) { + throw new Error('Not match type:' + type); + } + + await notificationProviders[type].send(notification, message); +} diff --git a/src/server/model/notification/provider/index.ts b/src/server/model/notification/provider/index.ts new file mode 100644 index 0000000..7f3b1d5 --- /dev/null +++ b/src/server/model/notification/provider/index.ts @@ -0,0 +1,6 @@ +import { smtp } from './smtp'; +import type { NotificationProvider } from './type'; + +export const notificationProviders: Record = { + smtp, +}; diff --git a/src/server/model/notification/provider/smtp.ts b/src/server/model/notification/provider/smtp.ts new file mode 100644 index 0000000..33f055f --- /dev/null +++ b/src/server/model/notification/provider/smtp.ts @@ -0,0 +1,50 @@ +import { NotificationProvider } from './type'; +import nodemailer from 'nodemailer'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +interface SMTPPayload { + hostname: string; + port: number; + security: boolean; + ignoreTLS: boolean; + username: string; + password: string; + from: string; + to: string; + cc?: string; + bcc?: string; +} + +// Fork from https://github.com/louislam/uptime-kuma/blob/HEAD/server/notification-providers/smtp.js +export const smtp: NotificationProvider = { + send: async (notification, message) => { + const payload = notification.payload as unknown as SMTPPayload; + + const config: SMTPTransport.Options = { + host: payload.hostname, + port: payload.port, + secure: payload.security, + ignoreTLS: payload.ignoreTLS, + }; + + if (payload.username || payload.password) { + config.auth = { + user: payload.username, + pass: payload.password, + }; + } + + const subject = message; + const bodyTextContent = message; + + const transporter = nodemailer.createTransport(config); + await transporter.sendMail({ + from: payload.from, + to: payload.to, + cc: payload.cc, + bcc: payload.bcc, + subject: subject, + text: bodyTextContent, + }); + }, +}; diff --git a/src/server/model/notification/provider/type.ts b/src/server/model/notification/provider/type.ts new file mode 100644 index 0000000..65556b4 --- /dev/null +++ b/src/server/model/notification/provider/type.ts @@ -0,0 +1,8 @@ +import { Notification } from '@prisma/client'; + +export interface NotificationProvider { + send: ( + notification: Pick, + message: string + ) => Promise; +} diff --git a/src/server/router/notification.ts b/src/server/router/notification.ts deleted file mode 100644 index 2ce70bc..0000000 --- a/src/server/router/notification.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Router } from 'express'; -import { auth } from '../middleware/auth'; -import { body, param, validate } from '../middleware/validate'; -import { workspacePermission } from '../middleware/workspace'; -import { - createWorkspaceNotification, - getWorkspaceNotifications, -} from '../model/notification'; -import { ROLES } from '../utils/const'; - -export const notificationRouter = Router(); - -notificationRouter.get( - '/list', - validate(param('workspaceId').isUUID()), - auth(), - workspacePermission(), - async (req, res) => { - const list = await getWorkspaceNotifications(req.params.workspaceId); - - res.json({ list }); - } -); - -notificationRouter.post( - '/create', - validate( - param('workspaceId').isUUID(), - body('name').isString(), - body('type').isString(), - body('payload').isObject() - ), - auth(), - workspacePermission([ROLES.owner]), - async (req, res) => { - const workspaceId = req.params.workspaceId; - const { name, type, payload } = req.body; - const notification = await createWorkspaceNotification( - workspaceId, - name, - type, - payload - ); - - res.json({ notification }); - } -); diff --git a/src/server/trpc/routers/notification.ts b/src/server/trpc/routers/notification.ts index 46c6ae7..0c4126f 100644 --- a/src/server/trpc/routers/notification.ts +++ b/src/server/trpc/routers/notification.ts @@ -1,6 +1,7 @@ import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc'; import { z } from 'zod'; import { prisma } from '../../model/_client'; +import { sendNotification } from '../../model/notification'; export const notificationRouter = router({ getAll: workspaceProcedure.query(({ input }) => { @@ -12,6 +13,18 @@ export const notificationRouter = router({ }, }); }), + test: workspaceProcedure + .input( + z.object({ + id: z.string().optional(), + name: z.string(), + type: z.string(), + payload: z.object({}).passthrough(), + }) + ) + .mutation(async ({ input }) => { + await sendNotification(input, `${input.name} + Notification Testing`); + }), upsert: workspaceOwnerProcedure .input( z.object({