Browse Source

chore: first commit

master
Shalma 1 year ago
commit
3088f27a9b
  1. 11
      .dockerignore
  2. 31
      .gitignore
  3. 10
      .sequelizerc
  4. 42
      Dockerfile
  5. 9
      configuration/cors.config.js
  6. 50
      configuration/logger.config.js
  7. 12
      configuration/server.config.js
  8. 54
      controllers/authentication.controller.js
  9. 132
      controllers/suivi.controller.js
  10. 124
      dao/history.dao.js
  11. 248
      dao/roster.dao.js
  12. 139
      dao/user.dao.js
  13. 16
      database/config/database.json
  14. 19
      database/migrations/20240715095741-create-users.js
  15. 26
      database/migrations/20240719075526-create-roster.js
  16. 50
      database/migrations/20240719080126-create-history.js
  17. 33
      database/models/history.js
  18. 25
      database/models/roster.js
  19. 24
      database/models/users.js
  20. 3
      database/package.json
  21. 15
      database/seeders/development/20240715100108-admin-user.js
  22. 15
      database/seeders/production/20240715100108-admin-user.js
  23. 9
      docker-entrypoint.sh
  24. 12
      error/types.error.js
  25. 4735
      package-lock.json
  26. 31
      package.json
  27. 18
      routes/health.routes.js
  28. 18
      routes/public.routes.js
  29. 52
      routes/suivi.routes.js
  30. 25
      server.js
  31. 55
      services/security.service.js
  32. 64
      services/suivi.service.js
  33. 19
      services/user.service.js
  34. 19
      webpack.config.js

11
.dockerignore

@ -0,0 +1,11 @@
node_modules
package-lock.json
.git
.env
dist
Dockerfile
.gitignore
.dockerignore
.gitlab-ci.yml
.vscode
*.db

31
.gitignore vendored

@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
*.db
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

10
.sequelizerc

@ -0,0 +1,10 @@
const path = require('path')
const env = process.env.NODE_ENV || 'development'
module.exports = {
'config': path.resolve('database', 'config', 'database.json'),
'migrations-path': path.resolve('database', 'migrations'),
'models-path': path.resolve('database', 'models'),
'seeders-path': path.resolve('database', 'seeders', env)
}

42
Dockerfile

@ -0,0 +1,42 @@
FROM node:20-slim as builder
WORKDIR /home/node/app
COPY . .
RUN npm install
RUN npm run build
##################################################################################
FROM node:20-slim
WORKDIR /home/node/app
RUN mkdir -p /opt/database
RUN mkdir -p /opt/schema
RUN mkdir -p /opt/logs
COPY --from=builder /home/node/app/package.json package.json
COPY --from=builder /home/node/app/package-lock.json package-lock.json
COPY --from=builder /home/node/app/database database
COPY --from=builder /home/node/app/.sequelizerc .sequelizerc
COPY --from=builder /home/node/app/dist/server.min.cjs server.min.cjs
RUN npm install --production
RUN rm package.json
RUN rm package-lock.json
RUN npm install -g sequelize-cli
RUN NODE_ENV=production sequelize db:migrate
RUN NODE_ENV=production sequelize db:seed:all
RUN mv /opt/database/suiviLootWow.db /opt/schema/suiviLootWow.db
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 3021
ENTRYPOINT "/docker-entrypoint.sh"

9
configuration/cors.config.js

@ -0,0 +1,9 @@
export default () => {
return (req, callback) => {
const corsOptions = {
origin: true
}
if (/^localhost$/m.test(req.headers.origin)) corsOptions.origin = false
callback(null, corsOptions)
}
}

50
configuration/logger.config.js

@ -0,0 +1,50 @@
import * as winston from 'winston'
const { format, transports, addColors } = winston
const { combine, timestamp, printf, colorize, label, errors } = format
const parseMessage = (message) => {
if (typeof message === 'object') {
if (message.req) {
return `Incoming request: route ${message.req.routeOptions.url}, method ${message.req.routeOptions.method}, from ${message.req.ip}`
}
if (message.res?.request) {
return `Request completed: route ${message.res.request.routeOptions.url}, method ${message.res.request.routeOptions.method}, from ${message.res.request.ip} `
}
}
return message
}
const logPrinter = printf((log) => {
const { level, message, stack } = log
const _label = log.label
const _timestamp = log.timestamp
const _message = parseMessage(message)
if (stack) return `${_timestamp} [${_label}] ${level}: ${_message}\n${stack}`
return `${_timestamp} [${_label}] ${level}: ${_message}`
})
// addColors({ ...winston.config.syslog.colors, fatal: winston.config.syslog.colors.error, warn: winston.config.syslog.colors.warn, trace: winston.config.syslog.colors.silly })
const init = (service) => {
winston.loggers.add('default', {
level: 'info',
levels: Object.assign({ 'fatal': 0, 'warn': 4, 'trace': 7 }, winston.config.syslog.levels),
format: combine(
// colorize(),
label({ label: service }),
timestamp(),
errors({ stack: true }),
logPrinter
),
transports: [
new transports.Console()
]
})
const logger = winston.loggers.get('default')
return logger
}
export default init

12
configuration/server.config.js

@ -0,0 +1,12 @@
export default {
port: 3201,
secret: 'e8p$2!76QKq%Wi3q8sU#qwvXqX2o2%mz',
database: {
development: {
path: 'D:/ProjetWeb/suivi-loot-wow-api/suiviLootWow.db'
},
production: {
path: '/opt/database/suiviLootWow.db'
}
}
}

54
controllers/authentication.controller.js

@ -0,0 +1,54 @@
import jwt from 'jsonwebtoken';
import ErrorType from '../error/types.error.js';
import serverConfig from '../configuration/server.config.js';
const signin = async (request, reply) => {
const user = request.user;
if (!user.message) {
request.log.info(`User ${user.username} authenticated.`);
try {
const token = await generateToken(user);
const body = {
success: true,
message: `User ${user.username} authenticated.`,
token
};
reply.code(200).send(body);
} catch (e) {
request.log.error(e);
return reply.code(500).send({ message: ErrorType.TECHNICAL_UNKNOWN });
}
} else {
request.log.info(`User ${user.username} not authenticated.`);
switch (user.message) {
case ErrorType.FUNCTIONAL_NOT_FOUND:
case ErrorType.FUNCTIONAL_FORBIDDEN:
return reply.code(401).send({ message: 'Bad user or password' });
case ErrorType.FUNCTIONAL_EXPIRED_ACCESS:
return reply.code(401).send({ message: ErrorType.FUNCTIONAL_EXPIRED_ACCESS });
case ErrorType.FUNCTIONAL_EXPIRED_PASSWORD:
return reply.code(419).send({ message: ErrorType.FUNCTIONAL_EXPIRED_PASSWORD });
default:
request.log.error(`User ${user.username} login internal error.`);
return reply.code(500).send({ message: ErrorType.TECHNICAL_UNKNOWN });
}
}
};
async function generateToken(user) {
const timestamp = new Date();
const iat = timestamp.getTime();
timestamp.setSeconds(timestamp.getSeconds() + (365 * 24 * 60 * 60));
const expiration = timestamp.getTime()
const payload = {
sub: user.username,
iat,
role: 'web_anon',
exp: expiration
};
return jwt.sign(payload, serverConfig.secret);
}
export default {
signin
};

132
controllers/suivi.controller.js

@ -0,0 +1,132 @@
import suiviService from '../services/suivi.service.js';
import typesError from '../error/types.error.js';
import { DateTime } from 'luxon'
const getAllRoster = async (request, reply) => {
try {
const rosters = await suiviService.getAllRoster()
const ret = []
for (let roster of rosters) {
ret.push(roster.toJson())
}
return reply.code(200).send({ rosters: ret })
} catch (e) {
return reply.code(500).send({ message: e.message })
}
}
const getAllHistory = async (request, reply) => {
try {
const histories = await suiviService.getAllHistory()
const ret = []
for (let history of histories) {
ret.push(history.toJson())
}
return reply.code(200).send({ histories: ret })
} catch (e) {
return reply.code(500).send({ message: e.message })
}
}
const addRoster = async (request, reply) => {
try {
await suiviService.addRoster(request.body)
return reply.code(200).send({
success: true,
message: `Roster member ${request.body.name} added`
})
} catch (e) {
return reply.code(500).send({ message: typesError.TECHNICAL_UNKNOWN })
}
}
const addHistories = async (request, reply) => {
const histories = request.body.history
const input = JSON.parse(histories)
const proms = []
if (Array.isArray(input)) {
for (const hist of input) {
const newHist = {
name: hist.player,
fullDate: formatDate(hist),
itemID: hist.itemID,
itemName: hist.itemName,
class: hist.class,
response: hist.response,
votes: hist.votes,
instance: hist.instance,
boss: hist.boss,
equipLoc: hist.equipLoc,
note: hist.note
}
proms.push(suiviService.addHistory(newHist).catch((e) => { console.log(e) }))
}
}
if (proms.length) {
try {
return Promise.all(proms).then(() => {
return reply.code(200).send({
success: true,
message: 'Histories added'
})
})
} catch (e) {
return reply.code(500).send({ message: typesError.TECHNICAL_UNKNOWN })
}
}
}
const deleteRoster = async (request, reply) => {
try {
await suiviService.removeRoster(request.params.name)
return reply.code(200).send({ success: true })
} catch (e) {
return reply.code(500).send({ message: e.message })
}
}
const deleteHistories = async (request, reply) => {
const histories = request.body.histories
const proms = []
if (Array.isArray(histories)) {
for (const id of histories) {
proms.push(suiviService.removeHistory(id).catch((e) => { console.log(e) }))
}
}
if (proms.length) {
try {
return Promise.all(proms).then(() => {
return reply.code(200).send({ success: true })
})
} catch (e) {
return reply.code(500).send({ message: typesError.TECHNICAL_UNKNOWN })
}
}
}
const getBisList = async (request, reply) => {
try {
const bisList = await suiviService.getBisList()
return reply.code(200).send({ bis: bisList })
} catch (e) {
return reply.code(500).send({ message: e.message })
}
}
const formatDate = (item) => {
const dArray = item.date.split('/')
const date = `${dArray[0].length === 1 ? `0${dArray[0]}` : dArray[0]}/${dArray[1].length === 1 ? `0${dArray[1]}` : dArray[1]}/${dArray[2]} ${item.time}`
return DateTime.fromFormat(date, 'dd/MM/yy HH:mm:ss').toMillis()
}
export default {
getAllRoster,
getAllHistory,
addRoster,
addHistories,
deleteRoster,
deleteHistories,
getBisList
}

124
dao/history.dao.js

@ -0,0 +1,124 @@
import sqlite3 from 'sqlite3'
import ErrorType from '../error/types.error.js'
import serverConfig from '../configuration/server.config.js'
sqlite3.verbose()
const tableName = 'Histories'
class History {
constructor(options = {}) {
if (!options.name || !options.fullDate || !options.itemID || !options.itemName) {
throw new Error('History has to have a name, a fullDate, an itemID and an itemName')
}
this.id = options.id
this.name = options.name
this.fullDate = options.fullDate
this.itemID = options.itemID
this.itemName = options.itemName
this.class = options.class
this.response = options.response
this.votes = options.votes
this.instance = options.instance
this.boss = options.boss
this.equipLoc = options.equipLoc
this.note = options.note
}
static findAll() {
return new Promise((resolve, reject) => {
const db = openDB()
db.serialize(() => {
db.all(`SELECT * FROM ${tableName} order by fullDate desc`, (err, histories) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else {
const array = [];
for (const history of histories) {
array.push(createHistoryFromDB(history))
}
resolve(array);
}
})
closeDB(db)
})
})
}
static delete(id) {
return new Promise((resolve, reject) => {
const db = openDB();
db.serialize(() => {
db.run(`DELETE FROM ${tableName} WHERE id = '${id}'`, (err, history) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else resolve(history)
})
})
closeDB(db)
})
}
async insert() {
return new Promise((resolve, reject) => {
const values = `'${this.name}', ${this.fullDate}, '${this.itemID}', "${this.itemName}", '${this.class}', '${this.response}', '${this.votes}', "${this.instance}", "${this.boss}", "${this.equipLoc}", "${this.note}"`;
const db = openDB();
db.serialize(() => {
db.run(`INSERT INTO ${tableName} (name, fullDate, itemID, itemName, class, response, votes, instance, boss, equipLoc, note) VALUES (${values})`, err => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else resolve()
})
})
closeDB(db)
})
}
toJson() {
return {
id: this.id,
name: this.name,
fullDate: this.fullDate,
itemID: this.itemID,
itemName: this.itemName,
class: this.class,
response: this.response,
votes: this.votes,
instance: this.instance,
boss: this.boss,
equipLoc: this.equipLoc,
note: this.note
}
}
}
export default History
function openDB() {
const env = process.env.NODE_ENV || 'development'
const db = new sqlite3.Database(serverConfig.database[env].path, err => {
if (err) throw new Error(err)
})
db.configure('busyTimeout', 30000)
return db
}
function closeDB(db) {
db.close(err => {
if (err) throw new Error(err)
})
}
function createHistoryFromDB(dbHistory) {
return new History({
id: dbHistory.id,
name: dbHistory.name,
fullDate: dbHistory.fullDate,
itemID: dbHistory.itemID,
itemName: dbHistory.itemName,
class: dbHistory.class,
response: dbHistory.response,
votes: dbHistory.votes,
instance: dbHistory.instance,
boss: dbHistory.boss,
equipLoc: dbHistory.equipLoc,
note: dbHistory.note
})
}

248
dao/roster.dao.js

@ -0,0 +1,248 @@
import sqlite3 from 'sqlite3'
import ErrorType from '../error/types.error.js'
import serverConfig from '../configuration/server.config.js'
sqlite3.verbose()
const tableName = 'Roster'
const historyTableName = 'Histories'
class Roster {
constructor(options = {}) {
if (!options.name || !options.classe || !options.role) {
throw new Error('Member has to have a name, a class and a role')
}
this.name = options.name
this.classe = options.classe
this.role = options.role
}
static findAll() {
return new Promise((resolve, reject) => {
const db = openDB()
db.serialize(() => {
db.all(`SELECT * FROM ${tableName} order by name asc`, (err, rosters) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else {
const array = [];
for (const roster of rosters) {
array.push(createRosterFromDB(roster))
}
resolve(array);
}
})
closeDB(db)
})
})
}
static getBisData() {
return new Promise((resolve, reject) => {
const db = openDB()
db.serialize(() => {
const query = `
SELECT
${tableName}.name,
${tableName}.classe,
'[' || GROUP_CONCAT(
'{' ||
'"equipLoc": "' || IFNULL(${historyTableName}.equipLoc, '') || '", ' ||
'"response": "' || IFNULL(${historyTableName}.response, '') || '", ' ||
'"instance": "' || IFNULL(${historyTableName}.instance, '') || '", ' ||
'"itemName": "' || IFNULL(${historyTableName}.itemName, '') || '"' ||
'}'
) || ']' AS historyData
FROM
${tableName}
LEFT JOIN
${historyTableName} ON ${tableName}.name = ${historyTableName}.name
WHERE ${historyTableName}.response = 'Bis'
GROUP BY
${tableName}.name, ${tableName}.classe;
`
db.all(query, (err, result) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else {
const array = [];
for (const res of result) {
array.push(createBisFromDB(res))
}
resolve(array);
}
})
closeDB(db)
})
})
}
static delete(name) {
return new Promise((resolve, reject) => {
const db = openDB();
db.serialize(() => {
db.run(`DELETE FROM ${tableName} WHERE name = '${name}'`, (err, roster) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else resolve(roster)
})
})
closeDB(db)
})
}
async insert() {
return new Promise((resolve, reject) => {
const values = `'${this.name}', '${this.classe}', '${this.role}'`;
const db = openDB();
db.serialize(() => {
db.run(`INSERT INTO ${tableName} (name, classe, role) VALUES (${values})`, err => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else resolve()
})
})
closeDB(db)
})
}
toJson() {
return {
name: this.name,
classe: this.classe,
role: this.role
}
}
}
export default Roster
function openDB() {
const env = process.env.NODE_ENV || 'development'
return new sqlite3.Database(serverConfig.database[env].path, err => {
if (err) throw new Error(err)
})
}
function closeDB(db) {
db.close(err => {
if (err) throw new Error(err)
})
}
function createRosterFromDB(dbRoster) {
return new Roster({
name: dbRoster.name,
classe: dbRoster.classe,
role: dbRoster.role
})
}
function createBisFromDB(res) {
const data = {
tete: 0,
cou: 0,
epaules: 0,
dos: 0,
torse: 0,
poignets: 0,
mains: 0,
taille: 0,
jambes: 0,
pied: 0,
doigt1: 0,
doigt2: 0,
bijou1: 0,
bijou2: 0,
arme: 0,
mainGauche: 0,
relique: 0
}
let tokenHM = 0
let totalBIS = 0
const histoData = JSON.parse(res.historyData)
for (const hist of histoData) {
if (hist.itemName && hist.equipLoc) {
switch (hist.equipLoc) {
case 'Tête':
data.tete += 1
totalBIS += 1
break
case 'Cou':
data.cou += 1
totalBIS += 1
break
case 'Épaule':
data.epaules += 1
totalBIS += 1
break
case 'Dos':
data.dos += 1
totalBIS += 1
break
case 'Torse':
data.torse += 1
totalBIS += 1
break
case 'Poignets':
data.poignets += 1
totalBIS += 1
break
case 'Mains':
data.mains += 1
totalBIS += 1
break
case 'Taille':
data.taille += 1
totalBIS += 1
break
case 'Jambes':
data.jambes += 1
totalBIS += 1
break
case 'Pieds':
data.pied += 1
totalBIS += 1
break
case 'Doigt':
if (data.doigt1 === 0) data.doigt1 += 1
else data.doigt2 += 1
totalBIS += 1
break
case 'Bijou':
if (data.bijou1 === 0) data.bijou1 += 1
else data.bijou2 += 1
totalBIS += 1
break
case 'À une main':
case 'Deux mains':
case 'Main droite':
data.arme += 1
totalBIS += 1
break
case 'Tenu(e) en main gauche':
case 'Main gauche':
data.mainGauche += 1
totalBIS += 1
break
case 'À distance':
case 'Relique':
data.relique += 1
totalBIS += 1
break
}
}
if (hist.equipLoc === '') {
if (hist.itemName.includes('Gantelets')) data.mains += 1
else if (hist.itemName.includes('Jambières')) data.jambes += 1
else if (hist.itemName.includes('Couronne')) data.tete += 1
else if (hist.itemName.includes('Epaulières')) data.epaules += 1
else if (hist.itemName.includes('Plastron')) data.torse += 1
tokenHM += 1
totalBIS += 1
}
}
return {
name: res.name,
classe: res.classe,
tokenHM,
totalBIS,
data
}
}

139
dao/user.dao.js

@ -0,0 +1,139 @@
import sqlite3 from 'sqlite3'
import bcrypt from 'bcryptjs'
import ErrorType from '../error/types.error.js'
import serverConfig from '../configuration/server.config.js'
sqlite3.verbose()
const tableName = 'Users'
class User {
constructor(options = {}) {
if (!options.username || !options.password) {
throw new Error('User has to have a username, a password')
}
this.username = options.username
this.password_ = options.password
}
static findAll() {
return new Promise((resolve, reject) => {
const db = openDB()
db.serialize(() => {
db.all(`SELECT * FROM ${tableName}`, (err, users) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else {
const array = [];
for (const user of users) {
array.push(createUserFromDB(user))
}
resolve(array);
}
})
closeDB(db)
})
})
}
static findByName(username) {
return new Promise((resolve, reject) => {
const db = openDB();
db.serialize(() => {
db.get(`SELECT * FROM ${tableName} WHERE username = '${username}'`, (err, user) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else {
if (user) resolve(createUserFromDB(user))
else resolve(null)
}
})
})
closeDB(db)
})
}
static delete(username) {
return new Promise((resolve, reject) => {
const db = openDB();
db.serialize(() => {
db.run(`DELETE FROM ${tableName} WHERE username = '${username}'`, (err, user) => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else resolve(user)
})
})
closeDB(db)
})
}
async insert() {
this.password_ = await hashPassword(this.password);
await this.write_();
}
async update() {
if (this.isModifiedPassword_) {
this.password_ = await hashPassword(this.password);
}
await this.write_();
}
write_() {
return new Promise((resolve, reject) => {
const values = `'${this.username}', '${this.password}'`;
const db = openDB();
db.serialize(() => {
db.run(`INSERT OR REPLACE INTO ${tableName} (username, password) VALUES (${values})`, err => {
if (err) reject(new Error(ErrorType.TECHNICAL_UNKNOWN))
else resolve()
})
})
closeDB(db)
})
}
comparePassword(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
}
toJson() {
return {
username: this.username
}
}
get password() {
return this.password_
}
set password(newPassword) {
this.password_ = newPassword
this.isModifiedPassword_ = true
this.activated = true
}
}
export default User
function openDB() {
const env = process.env.NODE_ENV || 'development'
return new sqlite3.Database(serverConfig.database[env].path, err => {
if (err) throw new Error(err)
})
}
function closeDB(db) {
db.close(err => {
if (err) throw new Error(err)
})
}
function createUserFromDB(dbUser) {
return new User({
username: dbUser.username,
password: dbUser.password
})
}
async function hashPassword(password) {
const salt = await bcrypt.genSalt(10)
return bcrypt.hash(password, salt)
}

16
database/config/database.json

@ -0,0 +1,16 @@
{
"development": {
"storage": "./suiviLootWow.db",
"dialect": "sqlite",
"define": {
"freezeTableName":true
}
},
"production": {
"storage": "/opt/database/suiviLootWow.db",
"dialect": "sqlite",
"define": {
"freezeTableName":true
}
}
}

19
database/migrations/20240715095741-create-users.js

@ -0,0 +1,19 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Users', {
username: {
allowNull: false,
primaryKey: true,
type: Sequelize.STRING
},
password: {
type: Sequelize.STRING
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Users');
}
};

26
database/migrations/20240719075526-create-roster.js

@ -0,0 +1,26 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Roster', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
classe: {
type: Sequelize.STRING
},
role: {
type: Sequelize.STRING
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Roster');
}
};

50
database/migrations/20240719080126-create-history.js

@ -0,0 +1,50 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('Histories', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
fullDate: {
type: Sequelize.INTEGER
},
itemID: {
type: Sequelize.STRING
},
itemName: {
type: Sequelize.STRING
},
class: {
type: Sequelize.STRING
},
response: {
type: Sequelize.STRING
},
votes: {
type: Sequelize.STRING
},
instance: {
type: Sequelize.STRING
},
boss: {
type: Sequelize.STRING
},
equipLoc: {
type: Sequelize.STRING
},
note: {
type: Sequelize.STRING
}
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('Histories');
}
};

33
database/models/history.js

@ -0,0 +1,33 @@
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class History extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
History.init({
name: DataTypes.STRING,
fullDate: DataTypes.INTEGER,
itemID: DataTypes.STRING,
itemName: DataTypes.STRING,
class: DataTypes.STRING,
response: DataTypes.STRING,
votes: DataTypes.STRING,
instance: DataTypes.STRING,
boss: DataTypes.STRING,
equipLoc: DataTypes.STRING,
note: DataTypes.STRING
}, {
sequelize,
modelName: 'History',
});
return History;
};

25
database/models/roster.js

@ -0,0 +1,25 @@
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Roster extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
Roster.init({
name: DataTypes.STRING,
classe: DataTypes.STRING,
role: DataTypes.STRING
}, {
sequelize,
modelName: 'Roster',
});
return Roster;
};

24
database/models/users.js

@ -0,0 +1,24 @@
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Users extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
Users.init({
username: DataTypes.STRING,
password: DataTypes.STRING
}, {
sequelize,
modelName: 'Users',
});
return Users;
};

3
database/package.json

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

15
database/seeders/development/20240715100108-admin-user.js

@ -0,0 +1,15 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Users', [{
username: 'admin',
password: '$2a$10$jtEXGBEVj1pCjXTLuPbXOOy0eErW00v/jnXxSp1jOtd9aIObtkkQW', // BNNDGez4de%W
}], {})
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Users', null, {})
}
};

15
database/seeders/production/20240715100108-admin-user.js

@ -0,0 +1,15 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.bulkInsert('Users', [{
username: 'admin',
password: '$2a$10$jtEXGBEVj1pCjXTLuPbXOOy0eErW00v/jnXxSp1jOtd9aIObtkkQW', // BNNDGez4de%W
}], {})
},
async down (queryInterface, Sequelize) {
await queryInterface.bulkDelete('Users', null, {})
}
};

9
docker-entrypoint.sh

@ -0,0 +1,9 @@
# !/bin/bash
if [ ! -f /opt/database/suiviLootWow.db ]; then
echo "No database found using an empty one"
cp /opt/schema/suiviLootWow.db /opt/database/suiviLootWow.db
fi
cd /home/node/app
NODE_ENV=production node server.min.cjs

12
error/types.error.js

@ -0,0 +1,12 @@
export default {
FUNCTIONAL_MISSING_PARAMETERS: 'Missing parameters',
FUNCTIONAL_MALFORMED_PARAMETER: 'Malformed parameter',
FUNCTIONAL_ALREADY_EXISTS: 'Already exists',
FUNCTIONAL_UNAUTHORIZE: 'Unauthorize',
FUNCTIONAL_FORBIDDEN: 'Forbidden',
FUNCTIONAL_NOT_FOUND: 'Not found',
FUNCTIONAL_FOREIGN_KEY_CONTRAINT: 'Foreign key constraint not respected',
FUNCTIONAL_EXPIRED_ACCESS: 'Expired access',
FUNCTIONAL_EXPIRED_PASSWORD: 'Expired password',
TECHNICAL_UNKNOWN: 'Internal error occure'
}

4735
package-lock.json generated

File diff suppressed because it is too large Load Diff

31
package.json

@ -0,0 +1,31 @@
{
"name": "suivi-loot-wow-api",
"version": "1.0.0",
"description": "Api for suivi-loot-wow frontend",
"main": "server.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server.js",
"build": "NODE_ENV=production webpack --mode=production --progress"
},
"author": "Shalma <sebcas@yahoo.fr>",
"license": "ISC",
"dependencies": {
"@fastify/cors": "^9.0.1",
"bcryptjs": "^2.4.3",
"fastify": "^4.28.1",
"jsonwebtoken": "^9.0.2",
"luxon": "^3.4.4",
"sequelize": "^6.37.3",
"sqlite3": "^5.1.7",
"winston": "^3.13.1"
},
"devDependencies": {
"nodemon": "^3.1.4",
"sequelize-cli": "^6.6.2",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
}
}

18
routes/health.routes.js

@ -0,0 +1,18 @@
const healthRoutes = async (app) => {
app.get('/health', async (request, reply) => {
const healthCheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now()
}
try {
return reply.send(healthCheck)
} catch (e) {
healthCheck.message = e
return reply.code(503).send(healthCheck)
}
})
}
export default healthRoutes

18
routes/public.routes.js

@ -0,0 +1,18 @@
import authenticationController from '../controllers/authentication.controller.js';
import securityService from '../services/security.service.js';
const publicRoutes = async (app) => {
app.get('/test', async (request, reply) => {
request.log.debug(request.headers);
reply.code(200).send({ success: true });
});
app.route({
method: 'POST',
url: '/signin',
preHandler: securityService.login,
handler: authenticationController.signin
});
};
export default publicRoutes;

52
routes/suivi.routes.js

@ -0,0 +1,52 @@
import suiviController from '../controllers/suivi.controller.js';
import securityService from '../services/security.service.js';
const suiviRoutes = async (app) => {
app.route({
method: 'GET',
url: '/rosters',
handler: suiviController.getAllRoster
});
app.route({
method: 'GET',
url: '/histories',
handler: suiviController.getAllHistory
});
app.route({
method: 'POST',
url: '/rosters',
preHandler: securityService.checkJWT,
handler: suiviController.addRoster
});
app.route({
method: 'POST',
url: '/histories',
preHandler: securityService.checkJWT,
handler: suiviController.addHistories
});
app.route({
method: 'DELETE',
url: '/rosters/:name',
preHandler: securityService.checkJWT,
handler: suiviController.deleteRoster
});
app.route({
method: 'POST',
url: '/deleteHistories',
preHandler: securityService.checkJWT,
handler: suiviController.deleteHistories
});
app.route({
method: 'GET',
url: '/bisList',
handler: suiviController.getBisList
});
};
export default suiviRoutes;

25
server.js

@ -0,0 +1,25 @@
import Fastify from 'fastify'
import cors from '@fastify/cors'
import init from './configuration/logger.config.js'
import corsConfig from './configuration/cors.config.js'
import serverConfig from './configuration/server.config.js'
import healthRoutes from './routes/health.routes.js'
import publicRoutes from './routes/public.routes.js'
import suiviRoutes from './routes/suivi.routes.js'
// import authRoutes from './routes/auth.routes.js'
// import adminRoutes from './routes/admin.routes.js'
const app = Fastify({ logger: init('SuiviLootWow-server') })
app.register(cors, corsConfig)
app.register(healthRoutes)
app.register(publicRoutes)
app.register(suiviRoutes)
app.listen({ port: serverConfig.port, host: '0.0.0.0' }, (err) => {
if (err) {
app.log.error(err)
process.exit(1)
}
})

55
services/security.service.js

@ -0,0 +1,55 @@
import jwt from 'jsonwebtoken'
import userService from './user.service.js'
import ErrorType from '../error/types.error.js'
import serverConfig from '../configuration/server.config.js'
const checkJWT = (request, reply, done) => {
isAuthorized(request, reply, done)
}
function isAuthorized(request, reply, done, condition = user => true) {
const token = request.headers['authorization']
if (!token) return reply.code(401).send({ message: 'No token provided' })
try {
const _token = token.replace('Bearer ', '')
const decoded = jwt.verify(_token, serverConfig.secret)
if (decoded && decoded.sub) {
if (new Date(decoded.exp) <= new Date()) {
return reply.code(401).send({ message: 'Invalid or expired token' })
}
}
if (condition(decoded)) {
request.user = decoded
request.user.username = request.user.sub
done()
} else return reply.code(403).send({ message: `Unauthorized access for ${decoded.sub}` })
} catch (e) {
request.log.error('Invalid or expired token')
return reply.code(401).send({ message: e.message })
}
}
const login = async (request, reply, done) => {
const { username, password } = request.body
try {
const user = await userService.getUser(username)
if (!user) {
request.log.error(`Login with user ${username} failed. User does not exist.`)
throw new Error(ErrorType.FUNCTIONAL_NOT_FOUND)
}
if (!await user.comparePassword(password)) {
request.log.error(`Password compare for user ${username} failed. Passwords don't match.`)
throw new Error(ErrorType.FUNCTIONAL_FORBIDDEN)
}
request.log.info(`Login with user ${username} succeeded.`)
request.user = user
} catch (e) {
request.log.error(`Login with user ${username} failed. Message: ${e.message}`)
request.user = { username, message: e.message }
}
}
export default {
checkJWT,
login
}

64
services/suivi.service.js

@ -0,0 +1,64 @@
import History from '../dao/history.dao.js'
import Roster from '../dao/roster.dao.js'
async function getAllRoster() {
return Roster.findAll()
}
async function getAllHistory() {
return History.findAll()
}
async function addRoster(options) {
if (!options.name || !options.classe || !options.role) throw new Error('Member has to have a name, a class and a role')
const roster = new Roster({
name: options.name,
classe: options.classe,
role: options.role
})
await roster.insert()
}
async function addHistory(options) {
if (!options.name || !options.fullDate || !options.itemID || !options.itemName) {
throw new Error('History has to have a name, a fullDate, an itemID and an itemName')
}
const history = new History({
name: options.name.replace('-Auberdine', ''),
fullDate: options.fullDate,
itemID: options.itemID,
itemName: options.itemName,
class: options.class,
response: options.response,
votes: options.votes,
instance: options.instance,
boss: options.boss,
equipLoc: options.equipLoc,
note: options.note
})
await history.insert()
}
async function removeRoster(name) {
const roster = await Roster.delete(name)
return roster
}
async function removeHistory(id) {
const history = await History.delete(id)
return history
}
async function getBisList() {
return Roster.getBisData()
}
export default {
getAllRoster,
getAllHistory,
addRoster,
addHistory,
removeRoster,
removeHistory,
getBisList
}

19
services/user.service.js

@ -0,0 +1,19 @@
import ErrorType from '../error/types.error.js'
import User from '../dao/user.dao.js'
async function getUser(username) {
if (!username) throw new Error(ErrorType.FUNCTIONAL_MISSING_PARAMETERS)
const user = await getUserDB(username)
if (!user) throw new Error(ErrorType.FUNCTIONAL_NOT_FOUND)
return user
}
export default {
getUser
}
async function getUserDB(username) {
let user = null
if (username) user = await User.findByName(username.toLowerCase())
return user
}

19
webpack.config.js

@ -0,0 +1,19 @@
import path from 'path';
import { fileURLToPath } from 'url';
import nodeExternals from 'webpack-node-externals';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let config = {
entry: './server.js',
output: {
filename: 'server.min.cjs',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2',
clean: true
},
target: 'node',
externals: [nodeExternals()]
}
export default config;
Loading…
Cancel
Save