feat(auth): migrate Application model from MongoDB to MySQL

- Add Sequelize migration creating the 'application' table
- Add Application Sequelize model with slugify, toJSONFor, toAuthJSON
- Rewrite passport-headerapikey strategy to use DB.Application
- Rewrite applications.routes.js to use Sequelize (findAll, count,
  build/save, destroy) instead of Mongoose
- Fix pre-existing bug: passport was querying { encryptedKey } but the
  stored field is named 'apikey'
- Add Application to MySQL models index and relationships
- Remove Application from Mongoose models index and delete the schema
This commit is contained in:
2026-04-26 18:26:58 +02:00
parent 097e11be1b
commit b3a31e4725
8 changed files with 228 additions and 148 deletions
+4 -6
View File
@@ -2,9 +2,7 @@ var secret = require('.').secret,
bcrypt = require('bcrypt'); bcrypt = require('bcrypt');
const passport = require('passport'); const passport = require('passport');
var HeaderAPIKeyStrategy = require('passport-headerapikey').HeaderAPIKeyStrategy; var HeaderAPIKeyStrategy = require('passport-headerapikey').HeaderAPIKeyStrategy;
var mongoose = require('mongoose'); const DB = require('../database/mysql');
var Application = mongoose.model('Application');
passport.use('headerapikey', new HeaderAPIKeyStrategy( passport.use('headerapikey', new HeaderAPIKeyStrategy(
{ header: 'Authorization', prefix: `${process.env.AUTHORIZATION_PREFIX} ` }, { header: 'Authorization', prefix: `${process.env.AUTHORIZATION_PREFIX} ` },
@@ -12,13 +10,13 @@ passport.use('headerapikey', new HeaderAPIKeyStrategy(
function (apikey, done) { function (apikey, done) {
const salt = secret; const salt = secret;
Promise.resolve(bcrypt.hash(apikey, salt)).then(function (encryptedKey) { Promise.resolve(bcrypt.hash(apikey, salt)).then(function (encryptedKey) {
Application.findOne({ encryptedKey: encryptedKey }) DB.Application.findOne({ where: { apikey: encryptedKey } })
.then(function (application) { .then(function (application) {
if (!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); return done(null, application);
}); });
}).catch(done); }).catch(done);
} }
)); ));
@@ -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');
}
};
-55
View File
@@ -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);
-1
View File
@@ -1,4 +1,3 @@
exports.Application = require('./Application');
exports.Aeronef = require('./Aeronef'); exports.Aeronef = require('./Aeronef');
exports.Canopy = require('./Canopy'); exports.Canopy = require('./Canopy');
exports.Dropzone = require('./Dropzone'); exports.Dropzone = require('./Dropzone');
+103
View File
@@ -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;
+1
View File
@@ -1,3 +1,4 @@
exports.Application = require('./Application');
exports.Article = require('./Article'); exports.Article = require('./Article');
//exports.ArticleTag = require('./ArticleTag'); //exports.ArticleTag = require('./ArticleTag');
exports.Brand = require('./Brand'); exports.Brand = require('./Brand');
+3
View File
@@ -1,6 +1,9 @@
const DB = require('../mysql'); const DB = require('../mysql');
const associate = () => { 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.User.hasOne(DB.SkydiverProfile, { as: 'skydiverProfile', foreignKey: 'userId', onDelete: 'CASCADE' });
DB.SkydiverProfile.belongsTo(DB.User, { as: 'user', foreignKey: 'userId' }); DB.SkydiverProfile.belongsTo(DB.User, { as: 'user', foreignKey: 'userId' });
+54 -86
View File
@@ -1,15 +1,15 @@
var router = require('express').Router(), var router = require('express').Router();
mongoose = require('mongoose'), const { Op } = require('sequelize');
Application = mongoose.model('Application'); const DB = require('../../../database/mysql');
const auth = require('../../../middlewares/auth'), const auth = require('../../../middlewares/auth');
mailer = require('@sendgrid/mail'); const mailer = require('@sendgrid/mail');
const { UserService } = require('../../../services'); const { UserService } = require('../../../services');
mailer.setApiKey(process.env.SENDGRID_API_KEY); mailer.setApiKey(process.env.SENDGRID_API_KEY);
// Preload application objects on routes with ':application' // Preload application objects on routes with ':application'
router.param('application', function (req, res, next, slug) { router.param('application', function (req, res, next, slug) {
Application.findOne({ slug: slug }) DB.Application.findOne({ where: { slug } })
.then(function (application) { .then(function (application) {
if (!application) { if (!application) {
return res.sendStatus(404); return res.sendStatus(404);
@@ -23,47 +23,26 @@ router.param('application', function (req, res, next, slug) {
* return all applications * return all applications
*/ */
router.get('/', auth.required, function (req, res, next) { router.get('/', auth.required, function (req, res, next) {
let query = {}; const limit = parseInt(req.query.limit) || 25;
let limit = 25; const offset = parseInt(req.query.offset) || 0;
let offset = 0; const where = {};
if (typeof req.query.limit !== 'undefined') { if (req.query.apikey) where.apikey = req.query.apikey;
limit = req.query.limit; if (req.query.maskedkey) where.maskedkey = req.query.maskedkey;
}
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;
}
Promise.all([ Promise.all([
req.query.author ? UserService.getUserByUsername(req.query.author) : null req.query.author ? UserService.getUserByUsername(req.query.author) : null,
]).then(function (author) { req.payload ? UserService.getUserById(req.payload.id) : null
if (author[0]) { ]).then(function ([author, user]) {
query.author = author[0].id; if (author) where.authorId = author.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];
return Promise.all([
DB.Application.findAll({ where, limit, offset, order: [['createdAt', 'DESC']] }),
DB.Application.count({ where })
]).then(function ([applications, applicationsCount]) {
return res.json({ return res.json({
applications: applications.map(function (application) { applications: applications.map((a) => a.toJSONFor(user)),
return application.toJSONFor(user); applicationsCount
}),
applicationsCount: applicationsCount
}); });
}); });
}).catch(next); }).catch(next);
@@ -76,18 +55,18 @@ router.post('/', auth.required, function (req, res, next) {
Promise.all([ Promise.all([
UserService.getUserById(req.payload.id), UserService.getUserById(req.payload.id),
auth.generateAPIKey() auth.generateAPIKey()
]).then(function (results) { ]).then(function ([user, keys]) {
let user = results[0];
let keys = results[1];
if (!user) { 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.apikey = keys.encrypted;
application.maskedkey = keys.masked; application.maskedkey = keys.masked;
return application.save().then(function () { return application.save().then(function () {
let msg = { const msg = {
to: user.email, to: user.email,
from: process.env.SENDGRID_FROM_MAIL, from: process.env.SENDGRID_FROM_MAIL,
subject: `Api-Key ${application.title}`, subject: `Api-Key ${application.title}`,
@@ -100,11 +79,10 @@ router.post('/', auth.required, function (req, res, next) {
<hr /> <hr />
<p>${application.description}</p>` <p>${application.description}</p>`
}; };
mailer.send(msg).then(() => { return mailer.send(msg).then(() => {
return res.json({ application: application.toJSONFor(user) }); return res.json({ application: application.toJSONFor(user) });
}) }).catch(err => {
.catch(err => { return res.status(500).json({ message: 'A SendGrid error occured while sending an email', err });
res.sendStatus(500).json({message: "A SendGrid error occured while sending an email", err: err});
}); });
}); });
}).catch(next); }).catch(next);
@@ -114,12 +92,10 @@ router.post('/', auth.required, function (req, res, next) {
* return an application * return an application
*/ */
router.get('/:application', auth.required, function (req, res, next) { router.get('/:application', auth.required, function (req, res, next) {
Promise.all([ Promise.resolve(req.payload ? UserService.getUserById(req.payload.id) : null)
req.payload ? UserService.getUserById(req.payload.id) : null .then(function (user) {
]).then(function (results) { return res.json({ application: req.application.toJSONFor(user) });
let user = results[0]; }).catch(next);
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) { router.put('/:application', auth.required, function (req, res, next) {
UserService.getUserById(req.payload.id).then(function (user) { UserService.getUserById(req.payload.id).then(function (user) {
const authorId = req.application.author?.id ?? req.application.author?.toString(); if (req.application.authorId !== req.payload.id) {
if (authorId === req.payload.id) { return res.status(403).json({ errors: { Forbidden: 'Vous ne disposez pas des autorisations suffisantes' } });
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" } });
} }
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) { router.delete('/:application', auth.required, function (req, res, next) {
UserService.getUserById(req.payload.id).then(function (user) { UserService.getUserById(req.payload.id).then(function (user) {
if (!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' && req.application.authorId !== req.payload.id) {
if (user.role == 'Admin' || authorId === req.payload.id) { return res.status(403).json({ errors: { Forbidden: 'Vous ne disposez pas des autorisations suffisantes' } });
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" } });
} }
return req.application.destroy().then(function () {
return res.json({ deleted: true });
});
}).catch(next); }).catch(next);
}); });