Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
21caa93461 | ||
![]() |
ad77e4cf3d | ||
![]() |
62ccb927b2 | ||
![]() |
42df8debb0 | ||
![]() |
5e56a91bd3 | ||
![]() |
c8926d99be | ||
![]() |
4b1ad8511d | ||
![]() |
3551e5084c | ||
![]() |
6948958cf3 | ||
![]() |
f494a89f5c |
@ -13,13 +13,18 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-ses": "^3.577.0",
|
||||||
"@fastify/cors": "^8.2.0",
|
"@fastify/cors": "^8.2.0",
|
||||||
"@fastify/formbody": "^7.4.0",
|
"@fastify/formbody": "^7.4.0",
|
||||||
"@fastify/helmet": "^10.1.0",
|
"@fastify/helmet": "^10.1.0",
|
||||||
"@fastify/jwt": "^6.7.0",
|
"@fastify/jwt": "^6.7.0",
|
||||||
|
"@wecom/crypto": "^1.0.1",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"fastify": "^4.14.1",
|
"fastify": "^4.14.1",
|
||||||
"fastify-plugin": "^4.5.0",
|
"fastify-plugin": "^4.5.0",
|
||||||
|
"fastify-xml-body-parser": "^2.2.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"mailgun.js": "^10.2.1",
|
||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.9.1",
|
||||||
"tracer": "^1.1.6"
|
"tracer": "^1.1.6"
|
||||||
},
|
},
|
||||||
|
@ -37,6 +37,7 @@ export class ApiServer {
|
|||||||
|
|
||||||
private registerPlugins() {
|
private registerPlugins() {
|
||||||
this.server.register(require('@fastify/formbody'))
|
this.server.register(require('@fastify/formbody'))
|
||||||
|
this.server.register(require('fastify-xml-body-parser'))
|
||||||
this.server.register(zReqParserPlugin)
|
this.server.register(zReqParserPlugin)
|
||||||
this.server.register(helmet, { hidePoweredBy: false })
|
this.server.register(helmet, { hidePoweredBy: false })
|
||||||
this.server.register(zTokenParserPlugin)
|
this.server.register(zTokenParserPlugin)
|
||||||
|
@ -12,6 +12,6 @@ class MailController extends BaseController {
|
|||||||
throw new ZError(10, 'params mismatch')
|
throw new ZError(10, 'params mismatch')
|
||||||
}
|
}
|
||||||
const result = await new MailQueue().addTaskToQueue(message)
|
const result = await new MailQueue().addTaskToQueue(message)
|
||||||
return { msgId: result.messageId }
|
return { msgId: result.messageId, messageId: result.messageId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
src/controllers/workflow.controller.ts
Normal file
36
src/controllers/workflow.controller.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import BaseController from 'common/base.controller'
|
||||||
|
import { ZError } from 'common/ZError'
|
||||||
|
import { role, router } from 'decorators/router'
|
||||||
|
import { getSignature, decrypt } from '@wecom/crypto'
|
||||||
|
|
||||||
|
class WorkFlowController extends BaseController {
|
||||||
|
@role('anon')
|
||||||
|
@router('get /workflow/notify')
|
||||||
|
async wxNotifyCheck(req, res) {
|
||||||
|
const token = process.env.WX_TOKEN
|
||||||
|
const aesKey = process.env.WX_AES_KEY
|
||||||
|
let { msg_signature, timestamp, nonce, echostr } = req.params
|
||||||
|
const signature = getSignature(token, timestamp, nonce, echostr)
|
||||||
|
if (msg_signature !== signature) {
|
||||||
|
throw new ZError(10, 'sign check failed')
|
||||||
|
}
|
||||||
|
const { message, id } = decrypt(aesKey, echostr)
|
||||||
|
res.send(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@role('anon')
|
||||||
|
@router('post /workflow/notify')
|
||||||
|
async flowNotify(req, res) {
|
||||||
|
let { msg_signature, timestamp, nonce, xml } = req.params
|
||||||
|
const token = process.env.WX_TOKEN
|
||||||
|
const aesKey = process.env.WX_AES_KEY
|
||||||
|
const signature = getSignature(token, timestamp, nonce, xml.Encrypt)
|
||||||
|
if (msg_signature !== signature) {
|
||||||
|
throw new ZError(10, 'sign check failed')
|
||||||
|
}
|
||||||
|
const { message, id } = decrypt(aesKey, xml.Encrypt)
|
||||||
|
console.log(id)
|
||||||
|
console.log(message)
|
||||||
|
res.send('success')
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,61 @@
|
|||||||
import { singleton } from 'decorators/singleton'
|
import { singleton } from 'decorators/singleton'
|
||||||
import { createTransport, Transporter } from 'nodemailer'
|
import { createTransport, Transporter } from 'nodemailer'
|
||||||
import Mail from 'nodemailer/lib/mailer'
|
import Mail from 'nodemailer/lib/mailer'
|
||||||
|
import FormData from 'form-data'
|
||||||
|
import Mailgun, { InputFormData } from 'mailgun.js'
|
||||||
|
import { ZError } from 'common/ZError'
|
||||||
|
import * as aws from '@aws-sdk/client-ses'
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
export class MailService {
|
export class MailService {
|
||||||
private transporter: Transporter
|
private transporter: Transporter
|
||||||
|
private mailClient: any
|
||||||
|
private awsClient: any
|
||||||
constructor() {
|
constructor() {
|
||||||
const options = {
|
// const options = {
|
||||||
host: process.env.MAIL_SMTP_HOST,
|
// host: process.env.MAIL_SMTP_HOST,
|
||||||
secure: true,
|
// secure: true,
|
||||||
auth: {
|
// auth: {
|
||||||
user: process.env.MAIL_SMTP_USER,
|
// user: process.env.MAIL_SMTP_USER,
|
||||||
pass: process.env.MAIL_SMTP_PASS,
|
// pass: process.env.MAIL_SMTP_PASS,
|
||||||
|
// },
|
||||||
|
// logger: true,
|
||||||
|
// debug: false,
|
||||||
|
// }
|
||||||
|
// // @ts-ignore
|
||||||
|
// this.transporter = createTransport(options, {})
|
||||||
|
|
||||||
|
const mailgun = new Mailgun(FormData)
|
||||||
|
this.mailClient = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY })
|
||||||
|
const ses = new aws.SES({
|
||||||
|
region: 'ap-southeast-1', // Your region will need to be updated
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_KEY,
|
||||||
},
|
},
|
||||||
logger: true,
|
})
|
||||||
debug: false,
|
this.transporter = createTransport({
|
||||||
}
|
SES: { ses, aws },
|
||||||
// @ts-ignore
|
})
|
||||||
this.transporter = createTransport(options, {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async send(message: Mail.Options) {
|
public async send(message: Mail.Options) {
|
||||||
await this.transporter.verify()
|
await this.transporter.verify()
|
||||||
return this.transporter.sendMail(message)
|
return this.transporter.sendMail(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async sendMailgun(message: Mail.Options) {
|
||||||
|
const domain = 'counterfire.games'
|
||||||
|
const sendResult = await this.mailClient.messages.create(domain, {
|
||||||
|
from: message.from,
|
||||||
|
to: message.to,
|
||||||
|
subject: message.subject,
|
||||||
|
html: message.html,
|
||||||
|
text: message.text,
|
||||||
|
})
|
||||||
|
if (sendResult.status !== 200) {
|
||||||
|
throw new ZError(20, sendResult.message)
|
||||||
|
}
|
||||||
|
return { messageId: sendResult.id }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
53
src/services/WechatWorkService.ts
Normal file
53
src/services/WechatWorkService.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { singleton } from 'decorators/singleton'
|
||||||
|
|
||||||
|
const WX_API_HOST = 'https://qyapi.weixin.qq.com'
|
||||||
|
@singleton
|
||||||
|
export class WechatWorkService {
|
||||||
|
private accessToken: string
|
||||||
|
private tokenExpire: number
|
||||||
|
private wxToken: string
|
||||||
|
private wxAesKey: string
|
||||||
|
private wxCorpId: string
|
||||||
|
private wxCorpSecret: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.wxToken = process.env.WX_TOKEN
|
||||||
|
this.wxAesKey = process.env.WX_AES_KEY
|
||||||
|
this.wxCorpId = process.env.WX_CORP_ID
|
||||||
|
this.wxCorpSecret = process.env.WX_CORP_SECRET
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信企业号的access_token
|
||||||
|
* https://developer.work.weixin.qq.com/resource/devtool
|
||||||
|
*/
|
||||||
|
public async refreshAccessToken() {
|
||||||
|
const url = `${WX_API_HOST}/cgi-bin/gettoken`
|
||||||
|
// use axios get url
|
||||||
|
let config = {
|
||||||
|
method: 'get',
|
||||||
|
maxBodyLength: Infinity,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
let response = await axios.request(config).then(response => {
|
||||||
|
return response.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取审批申请详情
|
||||||
|
* https://developer.work.weixin.qq.com/devtool/interface/alone?id=18615
|
||||||
|
* @param spNo 审批单号
|
||||||
|
*/
|
||||||
|
public async fetchApprovalDetail(spNo: string) {
|
||||||
|
const url = `${WX_API_HOST}/cgi-bin/oa/getapprovaldetail`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据media_id获取文件
|
||||||
|
* https://developer.work.weixin.qq.com/devtool/interface/alone?id=18615
|
||||||
|
* @param mediaId
|
||||||
|
*/
|
||||||
|
public async fetchFile(mediaId: string) {
|
||||||
|
const url = `${WX_API_HOST}/cgi-bin/media/get`
|
||||||
|
}
|
||||||
|
}
|
59
src/utils/security.util.ts
Normal file
59
src/utils/security.util.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
const ENCODER = 'base64'
|
||||||
|
const REG_KEY = /^[0-9a-fA-F]{63,64}$/
|
||||||
|
|
||||||
|
export function isEncrypt(msg: string) {
|
||||||
|
return !REG_KEY.test(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesEncrypt(text: string, password: string, iv: string) {
|
||||||
|
var md5 = crypto.createHash('md5')
|
||||||
|
const key = md5.update(password).digest('hex')
|
||||||
|
let cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
|
||||||
|
let encrypted = cipher.update(text, 'utf8', ENCODER)
|
||||||
|
encrypted += cipher.final(ENCODER)
|
||||||
|
return encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aesDecrypt(encryptedText: string, password: string, iv: string) {
|
||||||
|
var md5 = crypto.createHash('md5')
|
||||||
|
const key = md5.update(password).digest('hex')
|
||||||
|
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
|
||||||
|
let decrypted = decipher.update(encryptedText, ENCODER, 'utf8')
|
||||||
|
return decrypted + decipher.final('utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha512(password: string, salt: string) {
|
||||||
|
let hash = crypto.createHmac('sha512', salt)
|
||||||
|
hash.update(password)
|
||||||
|
let value = hash.digest('hex')
|
||||||
|
return {
|
||||||
|
salt: salt,
|
||||||
|
passwordHash: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function genRandomString(length: number) {
|
||||||
|
return crypto
|
||||||
|
.randomBytes(Math.ceil(length / 2))
|
||||||
|
.toString('hex')
|
||||||
|
.slice(0, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uuid() {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function md5(content: string) {
|
||||||
|
var md5 = crypto.createHash('md5')
|
||||||
|
return md5.update(content).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sha1(content: string) {
|
||||||
|
var md5 = crypto.createHash('sha1')
|
||||||
|
return md5.update(content).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64Decode(str: string) {
|
||||||
|
return Buffer.from(str, ENCODER).toString()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user