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