update
This commit is contained in:
parent
0f50ac2947
commit
666943d4f1
15 changed files with 3330 additions and 158 deletions
|
|
@ -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
21
src/MediaService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
14
src/app.ts
14
src/app.ts
|
|
@ -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}`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue