commit
3088f27a9b
34 changed files with 6145 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||||||
|
node_modules |
||||||
|
package-lock.json |
||||||
|
.git |
||||||
|
.env |
||||||
|
dist |
||||||
|
Dockerfile |
||||||
|
.gitignore |
||||||
|
.dockerignore |
||||||
|
.gitlab-ci.yml |
||||||
|
.vscode |
||||||
|
*.db |
||||||
@ -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 |
||||||
@ -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) |
||||||
|
} |
||||||
@ -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" |
||||||
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -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' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
|
}; |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
}) |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
@ -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) |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"development": { |
||||||
|
"storage": "./suiviLootWow.db", |
||||||
|
"dialect": "sqlite", |
||||||
|
"define": { |
||||||
|
"freezeTableName":true |
||||||
|
} |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"storage": "/opt/database/suiviLootWow.db", |
||||||
|
"dialect": "sqlite", |
||||||
|
"define": { |
||||||
|
"freezeTableName":true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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'); |
||||||
|
} |
||||||
|
}; |
||||||
@ -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'); |
||||||
|
} |
||||||
|
}; |
||||||
@ -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'); |
||||||
|
} |
||||||
|
}; |
||||||
@ -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; |
||||||
|
}; |
||||||
@ -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; |
||||||
|
}; |
||||||
@ -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; |
||||||
|
}; |
||||||
@ -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, {}) |
||||||
|
} |
||||||
|
}; |
||||||
@ -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, {}) |
||||||
|
} |
||||||
|
}; |
||||||
@ -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 |
||||||
@ -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' |
||||||
|
} |
||||||
@ -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" |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -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; |
||||||
@ -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; |
||||||
@ -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) |
||||||
|
} |
||||||
|
}) |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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…
Reference in new issue