Initial commit

This commit is contained in:
Damien Broqua 2024-01-07 12:32:39 +01:00
parent eace557491
commit d007dfd9e6
23 changed files with 10773 additions and 1 deletions

12
.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"esmodules": true
}
}
]
]
}

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
/node_modules/**
/build

24
.eslintrc.json Normal file
View File

@ -0,0 +1,24 @@
{
"env": {
"es6": true,
"node": true,
"jest": true
},
"extends": [
"google",
"plugin:prettier/recommended"
],
"plugins": [
"prettier"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"prettier/prettier": "error",
"dot-notation": [2, {
"allowKeywords": true
}]
}
}

1
.gitignore vendored
View File

@ -130,3 +130,4 @@ dist
.yarn/install-state.gz
.pnp.*
reports

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}

View File

@ -1,2 +1,17 @@
# express
# Express template
Ce repo est un simple template pour un projet Express écrit en ES6.
## Les scripts
Pour démarrer le projet en mode développement il faut utiliser la commande `npm run watch`. Cette commande permet de relancer automatiquement le server dès qu'un fichier est modifié.
En mode production il faut utiliser `npm run build` puis `npm start`. La première commande va transpiler le code en une version optimisée et la déployer dans le dossier `dist/`. La seconde va simplement lancer un serveur Node sur ce dossier.
Pour linter le code et corriger la plupart des problèmes automatiquement il y a la commande `npm run lint:fix`.
Il y a ensuite 2 commandes pour tester le code :
- `npm run test:lint` qui permet de vérifier que le code est correctement écris (tabulation, ;, const/let, etc)
- `npm run test:jest` qui permet de lancer les tests unitaires et d'intégration
Ces 2 commandes sont automatiquement lancé avec `npm test` manuellement ou lors d'un commit via Huksy.

24
jest.config.js Normal file
View File

@ -0,0 +1,24 @@
/** @type {import('jest').Config} */
const config = {
verbose: false,
testTimeout: 5000,
reporters: [
'default',
[
'jest-junit',
{ outputDirectory: 'reports', outputName: 'report.xml' },
],
],
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!**/node_modules/**',
'!**/build/**',
],
coverageReporters: ['clover', 'lcov', ['text', { skipFull: true }]],
coverageDirectory: 'reports',
showSeed: true,
slowTestThreshold: 5,
};
module.exports = config;

10243
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

55
package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "express-template",
"version": "0.0.0.",
"private": false,
"scripts": {
"start": "node ./build/bin/www",
"build": "babel ./src --out-dir ./build --copy-files",
"watch": "nodemon --exec babel-node src/bin/www",
"lint:fix": "eslint . --fix",
"test": "npm run test:lint && npm run test:jest",
"test:lint": "eslint .",
"test:jest": "jest --testTimeout=10000 --collectCoverage=true --detectOpenHandles --forceExit ./test",
"prepare": "npx husky install"
},
"lint-staged": {
"*.js": "eslint --cache --fix"
},
"engines": {
"node": "18.x"
},
"nodemonConfig": {
"ignore": [
"test/*",
"dist/*"
]
},
"dependencies": {
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"joi": "^17.11.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"rand-token": "^1.0.1"
},
"devDependencies": {
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.7",
"@babel/node": "^7.22.19",
"@babel/preset-env": "^7.23.7",
"eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"nodemon": "^3.0.2",
"supertest": "^6.3.3"
}
}

67
src/app.js Normal file
View File

@ -0,0 +1,67 @@
import createHttpError from 'http-errors';
import express from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
import passport from 'passport';
import session from 'express-session';
import { nodeEnv, secretSession } from './config';
import passportConfig from './libs/passport';
import authRouter from './routes/v1/auth';
import meRouter from './routes/v1/me';
import usersRouter from './routes/v1/users';
const app = express();
passportConfig(passport);
app.use(passport.initialize());
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'ejs');
app.set('trust proxy', 1);
app.use(
session({
secret: secretSession,
resave: false,
saveUninitialized: true,
cookie: { secure: true },
})
);
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(passport.session());
app.use('/v1/auth', authRouter);
app.use('/v1/me', meRouter);
app.use('/v1/users', usersRouter);
// catch 404 and forward to error handler
app.use((req, res, next) => {
next(createHttpError(404));
});
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = nodeEnv === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
if (req.xhr) {
res.json(err.message);
} else {
res.render('error');
}
});
module.exports = app;

43
src/bin/www Normal file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const debug = require('debug')('express:server');
const http = require('http');
const app = require('../app');
const { port } = require('../config');
app.set('port', port);
const server = http.createServer(app);
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
switch (error.code) {
case 'EACCES':
console.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
console.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
}
function onListening() {
const addr = server.address();
const bind =
typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
debug(`Listening on ${bind}`);
}
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

7
src/config/index.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3000', 10),
jwtKey: process.env.JWT_KEY || 'aekiFahshai5ooveeNuumaivoh2zohch',
secretSession:
process.env.SECRET_SESSION || 'Shie0kooz0uem9bu0aihoi3Woxoo9uxi',
};

13
src/helpers/object.js Normal file
View File

@ -0,0 +1,13 @@
/**
* Check if object is empty
* @param {Object} obj
*
* @return {Boolean}
*/
export const isEmpty = (obj) => {
if (!obj) {
return true;
}
return Object.keys(obj).length === 0;
};

40
src/libs/passport.js Normal file
View File

@ -0,0 +1,40 @@
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
// import createHttpError from 'http-errors';
import { jwtKey } from '../config';
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtKey,
passReqToCallback: true,
};
export default (passport) => {
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
passport.use(
'jwt',
new JwtStrategy(jwtOptions, (req, jwtPayload, next) => {
// INFO: C'est ici que l'on vérifie que le contenu du payload est valide
/**
* Exemple de jwtPayload:
* {
* userId: X,
* }
*
* On doit aller faire une requête sur la table Users pour vérifier
* qu'il y a un bien un user avec comme id X.
* Si ok :
* next(null, user);
* Sinon :
* throw createHttpError(401);
*/
})
);
};

124
src/middleware/Users.js Normal file
View File

@ -0,0 +1,124 @@
import Joi from 'joi';
import createHttpError from 'http-errors';
// import jwt from 'jsonwebtoken';
// import { uid } from 'rand-token';
// import { jwtKey } from '../config';
import { isEmpty } from '../helpers/object';
const schema = Joi.object({
username: Joi.string().min(3).max(30).required(),
password: Joi.string().required(),
}).with('username', 'password');
/**
* Create new user
* @param {Object} req
*
* @return {Object}
*/
export const createUser = async (req) => {
// INFO: On commence par vérifier que l'on reçoit bien un objet dans le body
if (isEmpty(req.body)) {
throw createHttpError(406);
}
// INFO: On vérifie ensuite que l'objet reçu correspond au schéma autorisé
const { value, error } = schema.validate(req.body, { abortEarly: false });
// INFO: Ce n'est pas le cas, on retourne une erreur
if (error) {
throw createHttpError(406, error);
}
console.log('Valeurs reçues:', value);
// INFO: Sinon on créé l'utilisateur
// const user = await models.Users.create(value);
// INFO: Puis on le retourne
// return user;
};
/**
* Auth user
* @param {Object} req
*
* @return {Object}
*/
export const authUser = async (req) => {
if (isEmpty(req.body)) {
throw createHttpError(406);
}
const { value, error } = schema.validate(req.body, { abortEarly: false });
if (error) {
throw createHttpError(406, error);
}
const { username, password } = value;
console.log('Valeurs reçues:', username, password);
// const user = await models.Users.findOne({
// where: {
// username,
// },
// });
// if (!user) {
// throw createHttpError(404);
// }
// if (!(await user.validPassword(password, user.password))) {
// throw createHttpError(401);
// }
// const payload = {
// id: user.id,
// token: uid(32),
// };
// const bearer = jwt.sign(payload, jwtKey, {
// expiresIn: '30d',
// });
// const date = new Date();
// date.setDate(date.getDate() + 30);
// return {
// bearer,
// expireAt: date,
// };
};
/**
* Update user profile (password)
* @param {Object} req
*
* @return {Object}
*/
export const updateMyProfile = async (req) => {
if (isEmpty(req.body)) {
throw createHttpError(406);
}
const schemaMyProfile = Joi.object({
password: Joi.string().required(),
});
const { value, error } = schemaMyProfile.validate(req.body, {
abortEarly: false,
});
if (error) {
throw createHttpError(406, error);
}
console.log('Valeurs reçues:', value);
// await req.user.update(value);
return {};
};

17
src/routes/v1/auth.js Normal file
View File

@ -0,0 +1,17 @@
import express from 'express';
import { authUser } from '../../middleware/Users';
// eslint-disable-next-line new-cap
const router = express.Router();
router.route('/').post(async (req, res, next) => {
try {
const data = await authUser(req);
res.status(201).json(data);
} catch (err) {
next(err);
}
});
export default router;

23
src/routes/v1/me.js Normal file
View File

@ -0,0 +1,23 @@
import express from 'express';
import passport from 'passport';
import { updateMyProfile } from '../../middleware/Users';
// eslint-disable-next-line new-cap
const router = express.Router();
router
.route('/')
.get(passport.authenticate('jwt'), (req, res) => {
res.status(200).json(req.user);
})
.patch(passport.authenticate('jwt'), async (req, res, next) => {
try {
const data = await updateMyProfile(req);
res.status(200).json(data);
} catch (err) {
next(err);
}
});
export default router;

20
src/routes/v1/users.js Normal file
View File

@ -0,0 +1,20 @@
import express from 'express';
import { createUser } from '../../middleware/Users';
// eslint-disable-next-line new-cap
const router = express.Router();
router.route('/').post(async (req, res, next) => {
try {
const data = await createUser(req);
res.status(201).json(data);
} catch (err) {
if (err.name === 'SequelizeUniqueConstraintError') {
err.status = 409;
}
next(err);
}
});
export default router;

13
test/functions.test.js Normal file
View File

@ -0,0 +1,13 @@
import { isEmpty } from '../src/helpers/object';
describe('Test my profil', () => {
test('It should response true when send undefined object to isEmpty', async () => {
expect(isEmpty()).toBe(true);
});
test('It should response true when send empty object to isEmpty', async () => {
expect(isEmpty({})).toBe(true);
});
test('It should response false when send empty object to isEmpty', async () => {
expect(isEmpty({ true: true })).toBe(false);
});
});

11
test/index.test.js Normal file
View File

@ -0,0 +1,11 @@
const request = require('supertest');
const app = require('../src/app');
describe('Test router', () => {
test('It should response 404 when tries to get not found route', async () => {
const response = await request(app)
.post('/api')
.set('X-Requested-With', 'XMLHttpRequest');
expect(response.statusCode).toBe(404);
});
});

3
views/error.ejs Normal file
View File

@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>