This commit is contained in:
root 2020-07-14 21:58:55 +00:00
parent 0f50ac2947
commit 666943d4f1
15 changed files with 3330 additions and 158 deletions

View file

@ -21,6 +21,23 @@ export default class EmailService {
return nodemailer.createTransport(config)
}
// static getMailgunClient(): Mailgun {
// return new Mailgun({
// apiKey: '4e64a894f18cc2fe10e4e4829b0048f3-7caa9475-241eee2c'
// })
// }
static nativeSend(params: any): Promise<any> {
// return this.getMailgunClient().sendMessage('sandboxeb2300b9439e4fe7bd2cca8f52ac7bd6.mailgun.org', {
// from: params.from,
// to: params.to,
// subject: params.subject,
// text: params.text,
// html: params.html
// })
return this.getTransporter().sendMail(params)
}
static send(to: string, subject: string, templateName: string, templateParams: any): Promise<any> {
const viewPath: string = __dirname + '/../views/emails/'
const data: Buffer = fs.readFileSync(viewPath + templateName + '.twig')
@ -30,6 +47,7 @@ export default class EmailService {
// for now replace every email by a predefined one
console.info(to + ' replaced')
to = "spamfree@matthieubessat.fr"
subject += " - Forum des associations 2020"
return new Promise((resolve, reject) => {
const config: any = {
@ -37,12 +55,11 @@ export default class EmailService {
to, subject, text, html
}
console.log(config.html)
this.getTransporter().sendMail(config).then(info => {
console.log(info)
this.nativeSend(config).then(res => {
console.log(res)
resolve()
}).catch(err => {
console.log(err)
reject()
console.error(err)
})
})
}

21
src/MediaService.ts Normal file
View file

@ -0,0 +1,21 @@
import aws from 'aws-sdk'
export default class MediaService {
static getS3(): aws.S3 {
aws.config.update({
secretAccessKey: process.env.S3_SECRET_KEY,
accessKeyId: process.env.S3_ACCESS_KEY,
region: 'fr-par',
httpOptions: { timeout: 4 * 60 * 1000 }
})
return new aws.S3({
endpoint: 's3.fr-par.scw.cloud'
})
}
static getBucket(): string {
return process.env.S3_BUCKET === undefined ? '___BUCKET_NOT_FOUND_ENV_VAR_ISSUE___' : process.env.S3_BUCKET
}
}

View file

@ -75,8 +75,10 @@ let main = async () => {
app.use('/delegate', express.Router()
.use(DelegateAuthMiddleware.handle)
.get('/', DelegateController.get)
.post('/test', DelegateController.test)
.put('/', DelegateController.update)
.put('/submit', DelegateController.submit)
.post('/submit', DelegateController.submit)
.post('/thumbnail', DelegateController.uploadThumbnail())
.delete('/', DelegateController.destroy)
)
@ -88,10 +90,12 @@ let main = async () => {
.get('/', AdminOrganizationController.getMany)
)
*/
app.post('/api/upload', MediaController.uploadRoute())
app.post('/api/media', MediaController.uploadRoute())
app.delete('/api/media/:key', MediaController.delete)
app.use(express.static('/apps/forum_server/static'))
app.listen(port, host, () => {
console.log(`API listening on ${host}:${port}`)
})

View file

@ -2,10 +2,8 @@ import * as express from 'express'
import Organization from '../models/Organization'
import cryptoRandomString from 'crypto-random-string'
import EmailService from '../EmailService'
import fs from 'fs'
import Mustache from 'mustache'
import slugify from 'slugify'
import Twig from 'twig'
import { Document } from 'mongoose'
export default class AdminOrganizationController {
static getMany(req: express.Request, res: express.Response) {
@ -17,21 +15,38 @@ export default class AdminOrganizationController {
static getOne(req: express.Request, res: express.Response) {
Organization.findById(req.params.id).then(data => {
res.json({ success: true, data })
})
}).catch(err => res.status(400).json({ success: false, errors: err }))
}
static store(req: express.Request, res: express.Response) {
Organization.create({
token: cryptoRandomString({ length: 10, type: 'distinguishable' }),
let body: any = {
token: AdminOrganizationController.generateToken(),
createdAt: new Date(),
slug: req.body.name === undefined ? undefined : slugify(req.body.name),
slug: req.body.adminName === undefined ? undefined : slugify(req.body.adminName).toLowerCase(),
...req.body
}).then(data => {
// ...{
// proposedVersion: {
// name: '',
// descriptionShort: '',
// descriptionLong: '',
// contacts: [],
// schedule: [],
// pricing: [],
// tag: null,
// cover: null,
// gallery: [],
// thumbnail: null
// }
// }
}
Organization.create(body).then(data => {
AdminOrganizationController.sendEmailTokenUniversal(data)
res.json({
success: true,
data
})
}).catch(err => {
console.log(err)
res.status(400).json({
success: false,
errors: err.errors
@ -53,12 +68,10 @@ export default class AdminOrganizationController {
success: true,
data
})
}).catch(err => {
res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
})
})
}).catch(err => res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
}))
}
static destroy(req: express.Request, res: express.Response) {
@ -68,12 +81,10 @@ export default class AdminOrganizationController {
success: isSuccess,
data
})
}).catch(err => {
res.status(400).json({
success: false,
errors: err.errors
})
})
}).catch(err => res.status(400).json({
success: false,
errors: err.errors
}))
}
static sendEmailToken(req: express.Request, res: express.Response) {
@ -81,34 +92,24 @@ export default class AdminOrganizationController {
if (data === null) {
return res.status(404).json({ success: false, errors: [ { code: 'not-found', message: 'Organization not found' } ] })
}
EmailService.send(
data.get('email'),
"Votre lien secret pour modifier votre association - Forum des associations 2020",
"token",
{ organizationName: data.get('adminName'), token: data.get('token') }
).then(() => {
console.log('email sent')
})
AdminOrganizationController.sendEmailTokenUniversal(data)
res.json({ success: true })
})
}).catch(err => res.status(400).json({ success: false, errors: err }))
}
static resetToken(req: express.Request, res: express.Response) {
Organization.updateOne({ _id: req.params.id }, {
token: cryptoRandomString({ length: 10, type: 'distinguishable' }),
token: this.generateToken(),
updatedAt: new Date()
}).then(data => {
res.json({
success: true,
data
})
}).catch(err => {
res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
})
})
}).catch(err => res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
}))
}
static approve(req: express.Request, res: express.Response) {
@ -116,6 +117,48 @@ export default class AdminOrganizationController {
}
static reject(req: express.Request, res: express.Response) {
Organization.findById(req.params.id).then(data => {
Organization.updateOne({ _id: req.params.id }, {
updatedAt: new Date()
}).then(data => {
EmailService.send(
data.get('email'),
"Votre lien secret pour modifier votre association",
"token",
{ adminName: data.get('adminName'), rejectionDescription: data.get('rejectionDescription') }
).then(() => {
console.log('> A token email was sent')
}).catch(() => {
console.error('> Token email failed to be sent')
})
res.json({
success: true,
data
})
}).catch(err => {
res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
})
})
}).catch(err => res.status(400).json({ success: false, errors: err }))
}
static generateToken(): string {
return cryptoRandomString({ length: 10, type: 'distinguishable' })
}
static sendEmailTokenUniversal(data: Document) {
EmailService.send(
data.get('email'),
"Votre lien secret pour modifier votre association",
"token",
{ adminName: data.get('adminName'), token: data.get('token') }
).then(() => {
console.log('> A token email was sent')
}).catch(() => {
console.error('> Token email failed to be sent')
})
}
}

View file

@ -80,4 +80,44 @@ export default class DefaultController {
// let path: string = __dirname + '/../../templates/organization.html'
// return res.send(Mustache.render(fs.readFileSync(path).toString(), {}))
// }
static getContactIcons(req: express.Request, res: express.Response) {
let icons: IconInterface[]
let contactTypes: any = {
twitter: {
name: 'Profil twitter',
iconId: 'fab fa-twitter'
},
facebook: {
name: 'Page ou profil facebook',
iconId: 'fab fa-facebook'
},
instagram: {
name: 'Profil instagram',
iconId: 'fab fa-instagram'
},
email: {
name: 'Email',
iconId: 'fasfa-at'
},
phone: {
name: 'Téléphone',
iconId: 'fas fa-phone'
},
person: {
name: "Personne responsable de l'association",
iconId: 'fas fa-user'
},
website: {
name: 'Site web',
iconId: 'fas fa-globe'
},
address: {
name: 'Adresse physique',
iconId: 'fas fa-map-marker-alt'
}
}
let icon: IconInterface|null = IconService.getIcon('fas fa')
res.json({ success: true, data: icon })
}
}

View file

@ -1,5 +1,10 @@
import Organization from '../models/Organization'
import * as express from 'express'
import EmailService from '../EmailService'
import MediaService from '../MediaService'
import multer from 'multer'
import multerS3 from 'multer-s3'
import slugify from 'slugify'
export default class DelegateController {
@ -7,12 +12,131 @@ export default class DelegateController {
res.json({ success: true, data: res.locals.organization })
}
static test(req: express.Request, res: express.Response) {
res.json({ success: true, body: req.body, organization: res.locals.organization })
}
static update(req: express.Request, res: express.Response) {
res.json({ success: true })
if (res.locals.organization.validationState === 'pending') {
return res.json({
success: false,
errors: [{ code: 'update-forbidden', message: 'Organization cannot be updated while the validationState is set to "pending"' }]
})
}
// only update proposedVersion
Organization.updateOne({ _id: res.locals.organization._id }, {
proposedVersion: req.body,
updatedAt: new Date()
}).then(data => {
res.json({
success: true,
data,
body: req.body
})
}).catch(err => {
res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
})
})
}
static submit(req: express.Request, res: express.Response) {
res.json({ success: true })
if (res.locals.organization.validationState === 'pending') {
return res.json({
success: false,
errors: [{ code: 'submit-forbidden', message: 'Organization cannot be submit twice' }]
})
}
Organization.updateOne({ _id: res.locals.organization._id }, {
validationState: 'pending',
updatedAt: new Date()
}).then(data => {
// send email to the manager
const organization: any = res.locals.organization
EmailService.send(
organization.email,
"L'association \"" + organization.adminName + "\" demande à être vérifé",
"approval",
{ adminName: organization.adminName, email: organization.email }
).then(() => {
console.log('> A "approval asked" email was sent')
}).catch(() => {
console.error('> "approval asked" email failed to be sent')
})
res.json({
success: true,
data
})
}).catch(err => {
console.log(err)
res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
})
})
}
static uploadThumbnail(): express.RequestHandler[] {
// we upload the thumbnail
return [
multer({
storage: multerS3({
s3: MediaService.getS3(),
bucket: 'development-bucket',
acl: 'public-read',
contentType: multerS3.AUTO_CONTENT_TYPE,
key: (_: any, file: any, cb: any) => {
console.log(file)
cb(null, Date.now().toString() + '_' + slugify(file.originalname))
}
})
}).single('file'),
(req: express.Request, res: express.Response) => {
// if the current thumbnail is defined AND the published thumnbnail is defined AND the current thumbnail is different from the published one
// THEN we delete the current thumbnail
const proposedVersion: any = res.locals.organization.proposedVersion
const publishedVersion: any = res.locals.organization.publishedVersion
if (
proposedVersion !== undefined && proposedVersion !== null &&
proposedVersion.thumbnail !== undefined && proposedVersion.thumbnail !== null &&
publishedVersion !== undefined && publishedVersion !== null &&
publishedVersion.thumbnail !== undefined && publishedVersion.thumbnail !== null &&
publishedVersion.thumbnail.location !== proposedVersion.thumbnail.location
) {
console.log(' we must delete ', proposedVersion.thumbnail)
MediaService.getS3().deleteObject({
Bucket: MediaService.getBucket(),
Key: proposedVersion.thumbnail.key
}, (err, _) => {
if (err !== null) {
console.error('> Cannot delete a old thumbnail')
console.log(err, err.stack)
}
})
}
const thumbnail: any = {
// @ts-ignore
key: req.file.key,
// @ts-ignore
contentType: req.file.contentType,
// @ts-ignore
location: req.file.location,
type: 'thumbnail'
}
Organization.updateOne({ _id: res.locals.organization._id }, {
proposedVersion: { thumbnail },
updatedAt: new Date()
}).then(data => {
res.json({ success: true, data, thumbnail })
}).catch(err => {
res.status(400).json({
success: false,
errors: err.errors !== undefined ? err.errors : err
})
})
}
]
}
static destroy(req: express.Request, res: express.Response) {

View file

@ -1,35 +1,22 @@
import Organization from '../models/Organization'
import * as express from 'express'
import aws from 'aws-sdk'
import multer from 'multer'
import multerS3 from 'multer-s3'
import MediaService from '../MediaService'
import slugify from 'slugify'
export default class MediaController {
static uploadRoute(): express.RequestHandler[] {
//
aws.config.update({
secretAccessKey: process.env.S3_SECRET_KEY,
accessKeyId: process.env.S3_ACCESS_KEY,
region: 'fr-par',
httpOptions: {
timeout: 4 * 60 * 1000,
}
})
const s3 = new aws.S3({
endpoint: 's3.fr-par.scw.cloud'
})
return [
multer({
storage: multerS3({
s3: s3,
bucket: 'development-bucket',
s3: MediaService.getS3(),
bucket: MediaService.getBucket(),
acl: 'public-read',
contentType: multerS3.AUTO_CONTENT_TYPE,
key: (req: any, file: any, cb: any) => {
key: (_: any, file: any, cb: any) => {
console.log(file)
cb(null, Date.now().toString() + '_' + file.originalname) //use Date.now() for unique file keys
cb(null, Date.now().toString() + '_' + slugify(file.originalname)) //use Date.now() for unique file keys
}
})
}).single('file'),
@ -46,4 +33,22 @@ export default class MediaController {
data: { file: req.file }
})
}
static delete(req: express.Request, res: express.Response) {
MediaService.getS3().deleteObject({
Bucket: MediaService.getBucket(),
Key: req.params.key
}, (err, _) => {
if (err !== null) {
console.log(err, err.stack)
res.status(400).json({ success: false, errors: err })
}
else {
res.json({
success: true
})
}
})
}
}

View file

@ -29,40 +29,48 @@ class AllowedString extends mongoose.SchemaType {
// @ts-ignore
mongoose.Schema.Types['AllowedString'] = AllowedString
const ScheduleIntervalBoundary = new Schema({
const ScheduleIntervalBoundary = {
hour: { type: Number },
minute: { type: Number }
})
}
const Media = new Schema({
location: { type: String, required: true },
bucket: { type: String },
key: { type: String },
const Media = {
location: { type: String },
type: {
type: AllowedString,
name: 'MediaType',
allowedValues: ['cover', 'thumbnail', 'video', 'image']
}
})
},
key: { type: String },
contentType: { type: String }
}
const OrganizationVersion = new Schema({
name: { type: String, required: true },
descriptionShort: { type: String, required: true },
descriptionLong: { type: String, required: true },
thumbnail: { type: Media, required: true },
gallery: [{ type: Media }],
cover: { type: Media },
contacts: [{
name: { type: String, required: true },
type: { type: String, required: true }, // (facebook,twitter,instagram,website,address,person,email,phone)
content: { type: String, required: true },
}],
const OrganizationVersion = {
name: { type: String },
descriptionShort: { type: String },
descriptionLong: { type: String },
thumbnail: Media,
gallery: [Media],
cover: Media,
// (facebook,twitter,instagram,website,address,person,email,phone)
contacts: {
facebook: { type: String },
twitter: { type: String },
instagram: { type: String },
website: { type: String },
address: { type: String },
person: { type: String },
email: { type: String },
phone: { type: String }
},
schedule: [{
name: { type: String, required: true },
description: { type: String },
day: { type: String }, // (Mon,Tue,Wed,Thu,Fri,Sat,Sun)
from: { type: ScheduleIntervalBoundary, required: true },
to: { type: ScheduleIntervalBoundary, required: true }
when: [{
day: { type: String }, // (Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche)
from: ScheduleIntervalBoundary,
to: ScheduleIntervalBoundary
}]
}],
pricing: [{
name: { type: String, required: true },
@ -70,12 +78,13 @@ const OrganizationVersion = new Schema({
description: { type: String }
}],
tag: { type: Tag }
})
}
const Organization = new Schema({
adminName: { type: String, required: true },
email: { type: String, required: true },
token: { type: String, required: true },
slug: { type: String, required: true },
validationState: {
type: AllowedString,
required: true,
@ -84,8 +93,8 @@ const Organization = new Schema({
allowedValues: ['none', 'pending', 'rejected', 'published']
},
rejectionDescription: { type: String },
proposedVersion: { type: OrganizationVersion },
publishedVersion: { type: OrganizationVersion },
proposedVersion: OrganizationVersion,
publishedVersion: OrganizationVersion,
createdAt: { type: Date },
updatedAt: { type: Date },
publishedAt: { type: Date },