diff --git a/src/config/passport-headerapikey.js b/src/config/passport-headerapikey.js index 2b1f46a..1e2da7a 100644 --- a/src/config/passport-headerapikey.js +++ b/src/config/passport-headerapikey.js @@ -2,9 +2,7 @@ var secret = require('.').secret, bcrypt = require('bcrypt'); const passport = require('passport'); var HeaderAPIKeyStrategy = require('passport-headerapikey').HeaderAPIKeyStrategy; -var mongoose = require('mongoose'); -var Application = mongoose.model('Application'); - +const DB = require('../database/mysql'); passport.use('headerapikey', new HeaderAPIKeyStrategy( { header: 'Authorization', prefix: `${process.env.AUTHORIZATION_PREFIX} ` }, @@ -12,13 +10,13 @@ passport.use('headerapikey', new HeaderAPIKeyStrategy( function (apikey, done) { const salt = secret; Promise.resolve(bcrypt.hash(apikey, salt)).then(function (encryptedKey) { - Application.findOne({ encryptedKey: encryptedKey }) + DB.Application.findOne({ where: { apikey: encryptedKey } }) .then(function (application) { if (!application) { - return done({message: "Application can't be found.", status: 404}, false, false); + return done({ message: "Application can't be found.", status: 404 }, false, false); } return done(null, application); - }); + }); }).catch(done); } )); diff --git a/src/database/migrations/20260426000000-add-application.js b/src/database/migrations/20260426000000-add-application.js new file mode 100644 index 0000000..906f71e --- /dev/null +++ b/src/database/migrations/20260426000000-add-application.js @@ -0,0 +1,63 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable('application', { + id: { + type: Sequelize.STRING(36), + allowNull: false, + primaryKey: true + }, + apikey: { + type: Sequelize.STRING(255), + allowNull: false + }, + maskedkey: { + type: Sequelize.STRING(20), + allowNull: true + }, + slug: { + type: Sequelize.STRING(255), + allowNull: false, + unique: true + }, + title: { + type: Sequelize.STRING(255), + allowNull: false + }, + description: { + type: Sequelize.TEXT, + allowNull: true + }, + authorId: { + type: Sequelize.STRING(36), + allowNull: true, + references: { + model: 'user', + key: 'id' + }, + onDelete: 'SET NULL' + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false + } + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + + async down(queryInterface) { + await queryInterface.dropTable('application'); + } +}; diff --git a/src/database/models/mongo/Application.js b/src/database/models/mongo/Application.js deleted file mode 100644 index 53d1453..0000000 --- a/src/database/models/mongo/Application.js +++ /dev/null @@ -1,55 +0,0 @@ -var mongoose = require('mongoose'); -var uniqueValidator = require('mongoose-unique-validator'); -var slug = require('slug'); - -var ApplicationSchema = new mongoose.Schema({ - apikey: String, - maskedkey: String, - slug: { type: String, lowercase: true, unique: true }, - title: String, - description: String, - author: { type: String } -}, { timestamps: true, toJSON: { virtuals: true } }); - -ApplicationSchema.plugin(uniqueValidator, { message: 'is already taken' }); - -ApplicationSchema.pre('validate', function (next) { - if (!this.slug) { - this.slugify(); - } - next(); -}); - -ApplicationSchema.methods.slugify = function () { - this.slug = slug(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36); -}; - -ApplicationSchema.methods.toJSONFor = function (user) { - return { - maskedkey: this.maskedkey, - slug: this.slug, - title: this.title, - description: this.description, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - author: this.author && typeof this.author.toProfileJSONFor === 'function' - ? this.author.toProfileJSONFor(user) - : (user && typeof user.toProfileJSONFor === 'function' ? user.toProfileJSONFor() : this.author ?? null) - }; -}; - -ApplicationSchema.methods.toAuthJSON = function () { - return { - maskedkey: this.maskedkey, - slug: this.slug, - title: this.title, - description: this.description, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - author: this.author && typeof this.author.toJSONFor === 'function' - ? this.author.toJSONFor() - : this.author ?? null - }; -}; - -mongoose.model('Application', ApplicationSchema); \ No newline at end of file diff --git a/src/database/models/mongo/index.js b/src/database/models/mongo/index.js index ba858b4..eb07c0f 100644 --- a/src/database/models/mongo/index.js +++ b/src/database/models/mongo/index.js @@ -1,4 +1,3 @@ -exports.Application = require('./Application'); exports.Aeronef = require('./Aeronef'); exports.Canopy = require('./Canopy'); exports.Dropzone = require('./Dropzone'); diff --git a/src/database/models/mysql/Application.js b/src/database/models/mysql/Application.js new file mode 100644 index 0000000..61ceb94 --- /dev/null +++ b/src/database/models/mysql/Application.js @@ -0,0 +1,103 @@ +const { DataTypes, Model } = require('sequelize'); +const { v4: uuidv4 } = require('uuid'); +const slug = require('slug'); + +const sequelize = require('../../config/sequelize'); + +class Application extends Model {} + +const ApplicationModel = () => { + Application.init( + { + id: { + type: DataTypes.STRING(36), + allowNull: false, + primaryKey: true + }, + apikey: { + type: DataTypes.STRING(255), + allowNull: false + }, + maskedkey: { + type: DataTypes.STRING(20), + allowNull: true + }, + slug: { + type: DataTypes.STRING(255), + allowNull: false, + unique: 'slug' + }, + title: { + type: DataTypes.STRING(255), + allowNull: false + }, + description: { + type: DataTypes.TEXT, + allowNull: true + }, + authorId: { + type: DataTypes.STRING(36), + allowNull: true + } + }, + { + sequelize, + tableName: 'application', + timestamps: true, + indexes: [ + { + name: 'PRIMARY', + unique: true, + using: 'BTREE', + fields: [{ name: 'id' }] + }, + { + name: 'slug', + unique: true, + using: 'BTREE', + fields: [{ name: 'slug' }] + } + ] + } + ); + + Application.beforeCreate((application) => { + application.id = uuidv4(); + if (!application.slug) { + application.slugify(); + } + application.slug = application.slug.toLowerCase(); + }); + + Application.prototype.slugify = function () { + this.slug = slug(this.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36); + }; + + Application.prototype.toJSONFor = function (user) { + return { + maskedkey: this.maskedkey, + slug: this.slug, + title: this.title, + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + author: user ? user.toProfileJSONFor() : this.authorId ?? null + }; + }; + + Application.prototype.toAuthJSON = function () { + return { + maskedkey: this.maskedkey, + slug: this.slug, + title: this.title, + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + author: this.authorId ?? null + }; + }; + + return Application; +}; + +module.exports = ApplicationModel; diff --git a/src/database/models/mysql/index.js b/src/database/models/mysql/index.js index 5e4afa7..2f36852 100644 --- a/src/database/models/mysql/index.js +++ b/src/database/models/mysql/index.js @@ -1,3 +1,4 @@ +exports.Application = require('./Application'); exports.Article = require('./Article'); //exports.ArticleTag = require('./ArticleTag'); exports.Brand = require('./Brand'); diff --git a/src/database/relationships/index.js b/src/database/relationships/index.js index 190cf50..549fe7d 100644 --- a/src/database/relationships/index.js +++ b/src/database/relationships/index.js @@ -1,6 +1,9 @@ const DB = require('../mysql'); const associate = () => { + DB.Application.belongsTo(DB.User, { as: 'author', foreignKey: 'authorId' }); + DB.User.hasMany(DB.Application, { as: 'applications', foreignKey: 'authorId', onDelete: 'SET NULL' }); + DB.User.hasOne(DB.SkydiverProfile, { as: 'skydiverProfile', foreignKey: 'userId', onDelete: 'CASCADE' }); DB.SkydiverProfile.belongsTo(DB.User, { as: 'user', foreignKey: 'userId' }); diff --git a/src/routes/api/skydive/applications.routes.js b/src/routes/api/skydive/applications.routes.js index 29aa211..4d86543 100644 --- a/src/routes/api/skydive/applications.routes.js +++ b/src/routes/api/skydive/applications.routes.js @@ -1,15 +1,15 @@ -var router = require('express').Router(), - mongoose = require('mongoose'), - Application = mongoose.model('Application'); -const auth = require('../../../middlewares/auth'), - mailer = require('@sendgrid/mail'); +var router = require('express').Router(); +const { Op } = require('sequelize'); +const DB = require('../../../database/mysql'); +const auth = require('../../../middlewares/auth'); +const mailer = require('@sendgrid/mail'); const { UserService } = require('../../../services'); mailer.setApiKey(process.env.SENDGRID_API_KEY); // Preload application objects on routes with ':application' router.param('application', function (req, res, next, slug) { - Application.findOne({ slug: slug }) + DB.Application.findOne({ where: { slug } }) .then(function (application) { if (!application) { return res.sendStatus(404); @@ -23,47 +23,26 @@ router.param('application', function (req, res, next, slug) { * return all applications */ router.get('/', auth.required, function (req, res, next) { - let query = {}; - let limit = 25; - let offset = 0; + const limit = parseInt(req.query.limit) || 25; + const offset = parseInt(req.query.offset) || 0; + const where = {}; - if (typeof req.query.limit !== 'undefined') { - limit = req.query.limit; - } - if (typeof req.query.offset !== 'undefined') { - offset = req.query.offset; - } - if (typeof req.query.apikey !== 'undefined') { - query.apikey = req.query.apikey; - } - if (typeof req.query.maskedkey !== 'undefined') { - query.maskedkey = req.query.maskedkey; - } + if (req.query.apikey) where.apikey = req.query.apikey; + if (req.query.maskedkey) where.maskedkey = req.query.maskedkey; Promise.all([ - req.query.author ? UserService.getUserByUsername(req.query.author) : null - ]).then(function (author) { - if (author[0]) { - query.author = author[0].id; - } - return Promise.all([ - Application.find(query) - .skip(Number(offset)) - .limit(Number(limit)) - .sort({ createdAt: 'desc' }) - .exec(), - Application.countDocuments(query).exec(), - req.payload ? UserService.getUserById(req.payload.id) : null - ]).then(function (results) { - let applications = results[0]; - let applicationsCount = results[1]; - let user = results[2]; + req.query.author ? UserService.getUserByUsername(req.query.author) : null, + req.payload ? UserService.getUserById(req.payload.id) : null + ]).then(function ([author, user]) { + if (author) where.authorId = author.id; + return Promise.all([ + DB.Application.findAll({ where, limit, offset, order: [['createdAt', 'DESC']] }), + DB.Application.count({ where }) + ]).then(function ([applications, applicationsCount]) { return res.json({ - applications: applications.map(function (application) { - return application.toJSONFor(user); - }), - applicationsCount: applicationsCount + applications: applications.map((a) => a.toJSONFor(user)), + applicationsCount }); }); }).catch(next); @@ -76,18 +55,18 @@ router.post('/', auth.required, function (req, res, next) { Promise.all([ UserService.getUserById(req.payload.id), auth.generateAPIKey() - ]).then(function (results) { - let user = results[0]; - let keys = results[1]; + ]).then(function ([user, keys]) { if (!user) { - return res.sendStatus(401).json({message: "Unauthorized - You are not allowed to access this resource."}); + return res.status(401).json({ message: 'Unauthorized - You are not allowed to access this resource.' }); } - let application = new Application(req.body.application); - application.author = user.id; + + const application = DB.Application.build(req.body.application); + application.authorId = user.id; application.apikey = keys.encrypted; application.maskedkey = keys.masked; + return application.save().then(function () { - let msg = { + const msg = { to: user.email, from: process.env.SENDGRID_FROM_MAIL, subject: `Api-Key ${application.title}`, @@ -100,11 +79,10 @@ router.post('/', auth.required, function (req, res, next) {
${application.description}
` }; - mailer.send(msg).then(() => { + return mailer.send(msg).then(() => { return res.json({ application: application.toJSONFor(user) }); - }) - .catch(err => { - res.sendStatus(500).json({message: "A SendGrid error occured while sending an email", err: err}); + }).catch(err => { + return res.status(500).json({ message: 'A SendGrid error occured while sending an email', err }); }); }); }).catch(next); @@ -114,12 +92,10 @@ router.post('/', auth.required, function (req, res, next) { * return an application */ router.get('/:application', auth.required, function (req, res, next) { - Promise.all([ - req.payload ? UserService.getUserById(req.payload.id) : null - ]).then(function (results) { - let user = results[0]; - return res.json({ application: req.application.toJSONFor(user) }); - }).catch(next); + Promise.resolve(req.payload ? UserService.getUserById(req.payload.id) : null) + .then(function (user) { + return res.json({ application: req.application.toJSONFor(user) }); + }).catch(next); }); /** @@ -127,26 +103,20 @@ router.get('/:application', auth.required, function (req, res, next) { */ router.put('/:application', auth.required, function (req, res, next) { UserService.getUserById(req.payload.id).then(function (user) { - const authorId = req.application.author?.id ?? req.application.author?.toString(); - if (authorId === req.payload.id) { - if (typeof req.body.application.title !== 'undefined') { - req.application.title = req.body.application.title; - } - if (typeof req.body.application.description !== 'undefined') { - req.application.description = req.body.application.description; - } - if (typeof req.body.application.apikey !== 'undefined') { - req.application.apikey = req.body.application.apikey; - } - if (typeof req.body.application.maskedkey !== 'undefined') { - req.application.maskedkey = req.body.application.maskedkey; - } - req.application.save().then(function (application) { - return res.json({ application: application.toJSONFor(user) }); - }).catch(next); - } else { - return res.status(403).json({ errors: { "Forbidden": "Vous ne disposez pas des autorisations suffisantes" } }); + if (req.application.authorId !== req.payload.id) { + return res.status(403).json({ errors: { Forbidden: 'Vous ne disposez pas des autorisations suffisantes' } }); } + + const fields = ['title', 'description', 'apikey', 'maskedkey']; + fields.forEach((f) => { + if (typeof req.body.application[f] !== 'undefined') { + req.application[f] = req.body.application[f]; + } + }); + + return req.application.save().then(function (application) { + return res.json({ application: application.toJSONFor(user) }); + }).catch(next); }); }); @@ -156,16 +126,14 @@ router.put('/:application', auth.required, function (req, res, next) { router.delete('/:application', auth.required, function (req, res, next) { UserService.getUserById(req.payload.id).then(function (user) { if (!user) { - return res.status(401).json({ errors: { "Unauthorized": "autentification requise" } }); + return res.status(401).json({ errors: { Unauthorized: 'authentification requise' } }); } - const authorId = req.application.author?.id ?? req.application.author?.toString(); - if (user.role == 'Admin' || authorId === req.payload.id) { - return req.application.remove().then(function () { - return res.json({ deleted: true }); - }); - } else { - return res.status(403).json({ errors: { "Forbidden": "Vous ne disposez pas des autorisations suffisantes" } }); + if (user.role !== 'Admin' && req.application.authorId !== req.payload.id) { + return res.status(403).json({ errors: { Forbidden: 'Vous ne disposez pas des autorisations suffisantes' } }); } + return req.application.destroy().then(function () { + return res.json({ deleted: true }); + }); }).catch(next); });