Merge pull request #42 from AlexisNP/issues/api-auth

Authentication system has been validated
This commit is contained in:
Joururi
2021-01-19 23:03:58 +01:00
committed by GitHub
32 changed files with 4405 additions and 4094 deletions

View File

@@ -6,7 +6,7 @@ USE auracle;
-- ROLES
CREATE TABLE IF NOT EXISTS `role` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`name` VARCHAR(255) NOT NULL,
`description` VARCHAR(255) NOT NULL,
PRIMARY KEY(`id`)
@@ -14,15 +14,15 @@ CREATE TABLE IF NOT EXISTS `role` (
-- PERMISSIONS
CREATE TABLE IF NOT EXISTS `permission` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`slug` VARCHAR(255) NOT NULL,
PRIMARY KEY(`id`)
);
-- USERS
CREATE TABLE IF NOT EXISTS `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(36) NOT NULL,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`uuid` VARCHAR(36) NOT NULL UNIQUE,
`name` VARCHAR(255) NOT NULL DEFAULT "Disciple",
`mail` VARCHAR(255) NOT NULL,
`avatar` VARCHAR(255),
@@ -37,16 +37,25 @@ CREATE TABLE IF NOT EXISTS `user` (
FOREIGN KEY(`role_id`) REFERENCES role(`id`)
);
-- API_TOKENS
CREATE TABLE IF NOT EXISTS `api_token` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`value` VARCHAR(255) NOT NULL,
`user_uuid` VARCHAR(36) NOT NULL UNIQUE,
PRIMARY KEY(`id`),
FOREIGN KEY(`user_uuid`) REFERENCES user(`uuid`)
);
-- SPELLS
CREATE TABLE IF NOT EXISTS `spell` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`name` VARCHAR(255) NOT NULL DEFAULT "Nom du sort",
`description` VARCHAR(1000) NOT NULL DEFAULT "Description du sort",
`level` INT UNSIGNED DEFAULT 0,
`charge` INT UNSIGNED DEFAULT 0,
`cost` VARCHAR(255) DEFAULT 0,
`is_ritual` BOOLEAN DEFAULT false,
`published` BOOLEAN DEFAULT true,
`published` BOOLEAN DEFAULT false,
`public` BOOLEAN DEFAULT true,
`author_id` INT UNSIGNED NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
@@ -55,7 +64,7 @@ CREATE TABLE IF NOT EXISTS `spell` (
-- META SCHOOLS
CREATE TABLE IF NOT EXISTS `meta_school` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`name` VARCHAR(255) NOT NULL DEFAULT "Nom de l'école mère",
`description` VARCHAR(255) DEFAULT "Description de l'école mère",
PRIMARY KEY (`id`)
@@ -63,9 +72,10 @@ CREATE TABLE IF NOT EXISTS `meta_school` (
-- SCHOOLS
CREATE TABLE IF NOT EXISTS `school` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`name` VARCHAR(255) NOT NULL DEFAULT "Nom de l'école",
`description` VARCHAR(255) DEFAULT "Description de l'école",
`published` BOOLEAN DEFAULT false,
`meta_school_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY(`meta_school_id`) REFERENCES meta_school(`id`)
@@ -73,16 +83,18 @@ CREATE TABLE IF NOT EXISTS `school` (
-- COMMON INGREDIENTS
CREATE TABLE IF NOT EXISTS `ingredient` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`name` VARCHAR(255) NOT NULL DEFAULT "Langue de salamandre",
`description` VARCHAR(255) NOT NULL DEFAULT "Une langue de salamandre de feu encore chaude.",
`published` BOOLEAN DEFAULT false,
PRIMARY KEY (`id`)
);
-- COMMON VARIABLES
CREATE TABLE IF NOT EXISTS `variable` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
`description` VARCHAR(255) NOT NULL DEFAULT "Nombre de créatures affectées",
`published` BOOLEAN DEFAULT false,
PRIMARY KEY (`id`)
);
@@ -138,51 +150,43 @@ DELIMITER ;
/* =========== PRIMARY INSERTS =========== */
SET NAMES utf8;
USE auracle;
-- ROLES
INSERT INTO `role` (name, description) VALUES
("Visiteur", "Utilisateur normal, peut consulter les sorts."),
("Scribe", "Gardiens des écrits, les scribes sont capables de soumettre des sortilèges."),
("Arcanologue", "Maîtres de l'arcane, ils ont le pouvoir et la responsabilité de juger les sortilèges récents et de les supprimer, ou valider."),
("Augure", "Régents des grimoires, ils ont droit d'accès à l'intégralité des informations connues, et pouvoir absolu sur le savoir arcanique.");
-- CSV DATA
LOAD DATA INFILE 'C:/temp/auracle_data/permission.csv'
INTO TABLE `permission`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
-- PERMISSIONS
INSERT INTO `permission` (slug) VALUES
("SUBMIT_SPELLS"),
("APPROVE_SPELLS"),
("MODIFY_SPELLS"),
("DELETE_SPELLS"),
("WARN_USERS"),
("BAN_USERS");
LOAD DATA INFILE 'C:/temp/auracle_data/role.csv'
INTO TABLE `role`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
INSERT INTO `role_permission` (role_id, permission_id) VALUES
(2, 1),
(3, 1),
(3, 2),
(3, 3),
(4, 1),
(4, 2),
(4, 3),
(4, 4),
(4, 5),
(4, 6);
LOAD DATA INFILE 'C:/temp/auracle_data/role_permission.csv'
INTO TABLE `role_permission`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
-- USERS
INSERT INTO `user` (uuid, name, mail, avatar, gender, register_date, password, role_id, verified, banned) VALUES
("08e5d2cf-3f0b-454f-b03f-504350d6be85", "Izàc", "tymos@ambrose.edu", null, null, "2020-12-27 17:47:02", "$2b$10$8KWGfRmdQ/ya32fROZNrWugXIEOciDaZwLz.3.GzQa5xrJaGF9RP2", 4, 1, 0);
LOAD DATA INFILE 'C:/temp/auracle_data/user.csv'
INTO TABLE `user`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
-- META SCHOOLS
INSERT INTO `meta_school` (name, description) VALUES
("Magies blanches", "Magies disciplinant les arts de soins et de lumières."),
("Magies noires", "Magies disciplinant l'art de la mort et des secrets."),
("Magies élémentaires", "Magies disciplinant les éléments basiques tels que l'eau, la foudre et le feu, pour n'en citer que les plus populaires."),
("Magies spirituelles", "Magies disciplinant l'esprit, tant pour le défendre que l'attaquer."),
("Magies spatio-temporelles", "Magies régissant le temps et l'espace."),
("Magies affiliées", "Magies rattachées à une forme d'énergie magique particulière."),
("Magies autres", "Magies trop spécifiques et ne rentrant dans aucune autre grande école.");
LOAD DATA INFILE 'C:/temp/auracle_data/api_token.csv'
INTO TABLE `api_token`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
-- CSV FILES
LOAD DATA INFILE 'C:/temp/auracle_data/spell.csv'
INTO TABLE `spell`
FIELDS TERMINATED BY ','
@@ -190,6 +194,13 @@ LOAD DATA INFILE 'C:/temp/auracle_data/spell.csv'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
LOAD DATA INFILE 'C:/temp/auracle_data/meta_school.csv'
INTO TABLE `meta_school`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
LOAD DATA INFILE 'C:/temp/auracle_data/school.csv'
INTO TABLE `school`
FIELDS TERMINATED BY ','
@@ -211,41 +222,9 @@ LOAD DATA INFILE 'C:/temp/auracle_data/variable.csv'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;
-- Insertions de masses
DELIMITER $$
CREATE PROCEDURE insertIntoSchoolRange(IN delimiter_start INT, IN delimiter_end INT, IN id_school INT)
BEGIN
SET @i = delimiter_start;
WHILE @i <= delimiter_end DO
INSERT INTO spell_school (spell_id, school_id) VALUES (@i, id_school);
SET @i = @i + 1;
END WHILE;
END$$
DELIMITER ;
CALL insertIntoSchoolRange(1, 33, 1);
CALL insertIntoSchoolRange(34, 67, 2);
CALL insertIntoSchoolRange(68, 90, 3);
CALL insertIntoSchoolRange(91, 119, 4);
CALL insertIntoSchoolRange(120, 135, 5);
CALL insertIntoSchoolRange(136, 139, 6);
CALL insertIntoSchoolRange(140, 165, 7);
CALL insertIntoSchoolRange(166, 195, 8);
CALL insertIntoSchoolRange(196, 222, 9);
CALL insertIntoSchoolRange(223, 247, 10);
CALL insertIntoSchoolRange(248, 252, 11);
CALL insertIntoSchoolRange(253, 274, 12);
CALL insertIntoSchoolRange(275, 292, 13);
CALL insertIntoSchoolRange(293, 312, 14);
CALL insertIntoSchoolRange(313, 323, 15);
CALL insertIntoSchoolRange(324, 343, 16);
CALL insertIntoSchoolRange(344, 364, 17);
CALL insertIntoSchoolRange(365, 366, 18);
CALL insertIntoSchoolRange(367, 383, 19);
CALL insertIntoSchoolRange(384, 385, 20);
CALL insertIntoSchoolRange(386, 403, 21);
CALL insertIntoSchoolRange(404, 435, 22);
CALL insertIntoSchoolRange(436, 438, 23);
CALL insertIntoSchoolRange(439, 443, 24);
CALL insertIntoSchoolRange(444, 455, 25);
CALL insertIntoSchoolRange(456, 462, 26);
LOAD DATA INFILE 'C:/temp/auracle_data/spell_school.csv'
INTO TABLE `spell_school`
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;

View File

@@ -5,11 +5,11 @@ const fs = require('fs');
const knex = require('knex')({
client: "mysql",
connection: {
host : process.env.DB_HOST,
user : process.env.DB_USER,
password : process.env.DB_PASSWORD,
database : process.env.DB_DATABASE,
charset : "utf8"
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
charset: "utf8"
},
});
const bookshelf = require('bookshelf')(knex);

View File

@@ -8,12 +8,9 @@ const morgan = require('morgan');
const cors = require('cors'); // module to format the json response
const dotenv = require('dotenv').config();
// Creates instances of database connections
const connection = require('./database/bookshelf');
const db = connection.db;
// CONSTANTS
const port = 2814;
const base_url = 'http://localhost:2814/api';
// Import routes
const routes = require('./routes');
@@ -33,19 +30,13 @@ app.use(helmet());
// Server
app.listen(port, () => console.log(`App listening on port ${port}`));
// TEMP Auth guard
const authguard = (req, res, next) => {
// if (req.headers.auracle_key !== process.env.API_KEY_PUBLIC) {
// return res.status(401).send('The API key is either missing or incorrect.');
// } else {
next();
// }
}
// Entry route
app.use('/api/v1/', routes.auth);
// Routing
app.use('/api/spells', authguard, routes.spells);
app.use('/api/schools', authguard, routes.schools);
app.use('/api/meta_schools', authguard, routes.meta_schools);
app.use('/api/variables', authguard, routes.variables);
app.use('/api/ingredients', authguard, routes.ingredients);
app.use('/api/users', authguard, routes.users);
app.use('/api/v1/spells', routes.spells);
app.use('/api/v1/schools', routes.schools);
app.use('/api/v1/meta_schools', routes.meta_schools);
app.use('/api/v1/variables', routes.variables);
app.use('/api/v1/ingredients', routes.ingredients);
app.use('/api/v1/users', routes.users);

View File

@@ -0,0 +1,14 @@
'use strict'
const bookshelf = require('../database/bookshelf').bookshelf;
require('./user-model');
let APIToken = bookshelf.Model.extend({
tableName: 'api_token',
hidden: ['id'],
user() {
return this.belongsTo('User', 'user_uuid', 'uuid');
}
})
module.exports = bookshelf.model('APIToken', APIToken);

View File

@@ -6,7 +6,7 @@ require('./spell-model')
let Ingredient = bookshelf.Model.extend({
tableName: 'ingredient',
spells() {
return this.belongsToMany( 'Spell', 'spell_ingredient')
return this.belongsToMany('Spell', 'spell_ingredient')
}
})

View File

@@ -6,7 +6,7 @@ require('./school-model')
let MetaSchool = bookshelf.Model.extend({
tableName: 'meta_school',
schools() {
return this.hasMany( 'School' )
return this.hasMany('School')
}
})

View File

@@ -4,10 +4,10 @@ require('./role-model')
let Permission = bookshelf.Model.extend({
tableName: 'permission',
hidden: [ 'id' ],
hidden: ['id'],
role() {
return this.belongsToMany( 'Role', 'role_permission' )
return this.belongsToMany('Role', 'role_permission')
}
})
module.exports = bookshelf.model( 'Permission', Permission );
module.exports = bookshelf.model('Permission', Permission);

View File

@@ -5,8 +5,8 @@ require('./permission-model')
let Role = bookshelf.Model.extend({
tableName: 'role',
permissions() {
return this.belongsToMany( 'Permission', 'role_permission' );
return this.belongsToMany('Permission', 'role_permission');
}
})
module.exports = bookshelf.model( 'Role', Role );
module.exports = bookshelf.model('Role', Role);

View File

@@ -7,10 +7,10 @@ require('./meta-school-model')
let School = bookshelf.Model.extend({
tableName: 'school',
spells() {
return this.belongsToMany( 'Spell', 'spell_school' )
return this.belongsToMany('Spell', 'spell_school')
},
meta_schools() {
return this.belongsTo( 'MetaSchool', 'meta_school_id' )
return this.belongsTo('MetaSchool', 'meta_school_id')
}
})

View File

@@ -6,12 +6,12 @@ require('./spell-model');
let User = bookshelf.Model.extend({
tableName: 'user',
hidden: [ 'password' ],
hidden: ['password', 'role_id'],
role() {
return this.belongsTo( 'Role' );
return this.belongsTo('Role');
},
spells() {
return this.hasMany( 'Spell', 'author_id' );
return this.hasMany('Spell', 'author_id');
}
})

View File

@@ -6,7 +6,7 @@ require('./spell-model')
let Variable = bookshelf.Model.extend({
tableName: 'variable',
spells() {
return this.belongsToMany( 'Spell', 'spell_variable')
return this.belongsToMany('Spell', 'spell_variable')
}
})

97
api/package-lock.json generated
View File

@@ -233,11 +233,6 @@
}
}
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -481,14 +476,6 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1040,9 +1027,9 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"interpret": {
"version": "2.0.0",
@@ -1214,49 +1201,6 @@
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.6.tgz",
"integrity": "sha512-SqhURKZG07JyKKeo/ir24QnS4/BV7a6gQy93bUSe4lUdNp0QNpIz2c9elWJQ9dpc5cQYY6cvCzgRwy0MQCLyqA=="
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"dependencies": {
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -1334,41 +1278,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"make-iterator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",

View File

@@ -4,7 +4,7 @@
"description": "API for Auracle database",
"main": "index.js",
"scripts": {
"run": "node index.js"
"start": "node index.js"
},
"repository": {
"type": "git",
@@ -35,7 +35,6 @@
"handlebars": "^4.7.6",
"helmet": "^3.22.0",
"jsonschema": "^1.2.6",
"jsonwebtoken": "^8.5.1",
"knex": "^0.21.1",
"morgan": "^1.10.0",
"mysql": "^2.18.1",

View File

@@ -5,9 +5,9 @@ const model = require('../models/ingredient-model')
// Model validation
const Validator = require('jsonschema').Validator
const v = new Validator()
const validator = new Validator()
const IngredientValidation = require("../validations/IngredientValidation")
v.addSchema(IngredientValidation, "/IngredientValidation")
validator.addSchema(IngredientValidation, "/IngredientValidation")
// Validations
const isXSSAttempt = require('../functions').isXSSAttempt
@@ -39,8 +39,8 @@ class IngredientRepository {
getOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['spells']})
.where({ 'id': id })
.fetch({ withRelated: ['spells'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -57,8 +57,8 @@ class IngredientRepository {
getSpellsFromOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['spells', 'spells.schools', 'spells.variables', 'spells.ingredients', 'spells.schools.meta_schools']})
.where({ 'id': id })
.fetch({ withRelated: ['spells', 'spells.schools', 'spells.variables', 'spells.ingredients', 'spells.schools.meta_schools'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -80,9 +80,9 @@ class IngredientRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(igr, IngredientValidation).valid) {
} else if (!validator.validate(igr, IngredientValidation).valid) {
reject({
"message": `Le modèle d'ingrédient n'est pas respecté : ${v.validate(s, IngredientValidation).errors}`,
"message": `Le modèle d'ingrédient n'est pas respecté : ${validator.validate(s, IngredientValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(igr.description)) {
@@ -127,9 +127,9 @@ class IngredientRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(igr, IngredientValidation).valid) {
} else if (!validator.validate(igr, IngredientValidation).valid) {
reject({
"message": `Le modèle d'ingrédient n'est pas respecté : ${v.validate(s, IngredientValidation).errors}`,
"message": `Le modèle d'ingrédient n'est pas respecté : ${validator.validate(s, IngredientValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(igr.description)) {
@@ -138,8 +138,8 @@ class IngredientRepository {
"code": 403,
});
} else {
new model({id: id})
.fetch({require: true, withRelated: ['spells']})
new model({ id: id })
.fetch({ require: true, withRelated: ['spells'] })
.then(v => {
bookshelf.transaction(t => {
return v.save({
@@ -182,8 +182,8 @@ class IngredientRepository {
deleteOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({require: true, withRelated: ['spells']})
.where({ 'id': id })
.fetch({ require: true, withRelated: ['spells'] })
.then(v => {
v.spells().detach()
v.destroy()

View File

@@ -5,9 +5,9 @@ const model = require('../models/meta-school-model')
// Model validation
const Validator = require('jsonschema').Validator
const v = new Validator()
const validator = new Validator()
const MetaSchoolValidation = require("../validations/MetaSchoolValidation")
v.addSchema(MetaSchoolValidation, "/MetaSchoolValidation")
validator.addSchema(MetaSchoolValidation, "/MetaSchoolValidation")
// Validations
const regexXSS = RegExp(/<[^>]*script/)
@@ -37,8 +37,8 @@ class MetaSchoolRepository {
getOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['schools']})
.where({ 'id': id })
.fetch({ withRelated: ['schools'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})

View File

@@ -5,9 +5,9 @@ const model = require('../models/school-model')
// Model validation
const Validator = require('jsonschema').Validator
const v = new Validator()
const validator = new Validator()
const SchoolValidation = require("../validations/SchoolValidation")
v.addSchema(SchoolValidation, "/SchoolValidation")
validator.addSchema(SchoolValidation, "/SchoolValidation")
// Validations
const isXSSAttempt = require('../functions').isXSSAttempt
@@ -38,8 +38,8 @@ class SchoolRepository {
getOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['meta_schools']})
.where({ 'id': id })
.fetch({ withRelated: ['meta_schools'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -56,8 +56,8 @@ class SchoolRepository {
getSpellsFromOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['spells', 'spells.schools', 'spells.variables', 'spells.ingredients', 'spells.schools.meta_schools']})
.where({ 'id': id })
.fetch({ withRelated: ['spells', 'spells.schools', 'spells.variables', 'spells.ingredients', 'spells.schools.meta_schools'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -79,9 +79,9 @@ class SchoolRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(s, SchoolValidation).valid) {
} else if (!validator.validate(s, SchoolValidation).valid) {
reject({
"message": `Le modèle d'école n'est pas respecté : ${v.validate(s, SchoolValidation).errors}`,
"message": `Le modèle d'école n'est pas respecté : ${validator.validate(s, SchoolValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(s.name) || isXSSAttempt(s.description)) {
@@ -127,9 +127,9 @@ class SchoolRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(s, SchoolValidation).valid) {
} else if (!validator.validate(s, SchoolValidation).valid) {
reject({
"message": `Le modèle d'école n'est pas respecté : ${v.validate(s, SchoolValidation).errors}`,
"message": `Le modèle d'école n'est pas respecté : ${validator.validate(s, SchoolValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(s.name) || isXSSAttempt(s.description)) {
@@ -138,8 +138,8 @@ class SchoolRepository {
"code": 403,
});
} else {
new model({id: id})
.fetch({require: true, withRelated: ['meta_schools']})
new model({ id: id })
.fetch({ require: true, withRelated: ['meta_schools'] })
.then(v => {
bookshelf.transaction(t => {
return v.save({
@@ -183,8 +183,8 @@ class SchoolRepository {
deleteOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({require: true, withRelated: ['spells', 'meta_schools']})
.where({ 'id': id })
.fetch({ require: true, withRelated: ['spells', 'meta_schools'] })
.then(v => {
v.spells().detach()
v.destroy()

View File

@@ -5,9 +5,9 @@ const model = require('../models/spell-model')
// Model validation
const Validator = require('jsonschema').Validator
const v = new Validator()
const validator = new Validator()
const SpellValidation = require("../validations/SpellValidation")
v.addSchema(SpellValidation, "/SpellValidation")
validator.addSchema(SpellValidation, "/SpellValidation")
// Validations
const isXSSAttempt = require('../functions').isXSSAttempt
@@ -25,12 +25,12 @@ class SpellRepository {
if (name) { query.where('name', 'like', `%${name}%`) }
if (description) { query.where('description', 'like', `%${description}%`) }
if (level) { query.where({ 'level' : level }) }
if (charge) { query.where({ 'charge' : charge }) }
if (cost) { query.where({ 'cost' : cost }) }
if (ritual) { query.where({ 'is_ritual' : ritual }) }
if (level) { query.where({ 'level': level }) }
if (charge) { query.where({ 'charge': charge }) }
if (cost) { query.where({ 'cost': cost }) }
if (ritual) { query.where({ 'is_ritual': ritual }) }
query.fetchAll({ withRelated: [ 'schools.meta_schools', 'variables', 'ingredients', 'author' ] })
query.fetchAll({ withRelated: ['schools.meta_schools', 'variables', 'ingredients', 'author'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -47,16 +47,16 @@ class SpellRepository {
getAllPublic(name, description, level, charge, cost, ritual) {
return new Promise((resolve, reject) => {
let query = new model().where({ 'public' : 1 })
let query = new model().where({ 'public': 1 })
if (name) { query.where('name', 'like', `%${name}%`) }
if (description) { query.where('description', 'like', `%${description}%`) }
if (level) { query.where({ 'level' : level }) }
if (charge) { query.where({ 'charge' : charge }) }
if (cost) { query.where({ 'cost' : cost }) }
if (ritual) { query.where({ 'is_ritual' : ritual }) }
if (level) { query.where({ 'level': level }) }
if (charge) { query.where({ 'charge': charge }) }
if (cost) { query.where({ 'cost': cost }) }
if (ritual) { query.where({ 'is_ritual': ritual }) }
query.fetchAll({ withRelated: [ 'schools.meta_schools', 'variables', 'ingredients', 'author' ] })
query.fetchAll({ withRelated: ['schools.meta_schools', 'variables', 'ingredients', 'author'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }));
})
@@ -73,11 +73,11 @@ class SpellRepository {
getPage(page) {
return new Promise((resolve, reject) => {
new model()
.where({ 'public' : 1 })
.where({ 'public': 1 })
.fetchPage({
pageSize: 20,
page: page,
withRelated: [ 'schools.meta_schools', 'variables', 'ingredients', 'author' ],
withRelated: ['schools.meta_schools', 'variables', 'ingredients', 'author'],
})
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
@@ -95,8 +95,8 @@ class SpellRepository {
getOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: [ 'schools.meta_schools', 'variables', 'ingredients', 'author' ]})
.where({ 'id': id })
.fetch({ withRelated: ['schools.meta_schools', 'variables', 'ingredients', 'author'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -118,9 +118,9 @@ class SpellRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(s, SpellValidation).valid) {
} else if (!validator.validate(s, SpellValidation).valid) {
reject({
"message": `Le modèle de sortilège n'est pas respecté : ${v.validate(s, SpellValidation).errors}`,
"message": `Le modèle de sortilège n'est pas respecté : ${validator.validate(s, SpellValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(s.name) || isXSSAttempt(s.description) || isXSSAttempt(s.cost)) {
@@ -134,9 +134,9 @@ class SpellRepository {
'name': s.name,
'description': s.description,
'level': s.level,
'charge' : s.charge,
'cost' : s.cost,
'is_ritual' : s.is_ritual
'charge': s.charge,
'cost': s.cost,
'is_ritual': s.is_ritual
}).save(null, {
transacting: t
})
@@ -170,7 +170,7 @@ class SpellRepository {
})
})
.then(v => {
return v.load([ 'schools.meta_schools', 'variables', 'ingredients', 'author' ])
return v.load(['schools.meta_schools', 'variables', 'ingredients', 'author'])
})
.then(v => {
resolve(this.getOne(v.id))
@@ -194,9 +194,9 @@ class SpellRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(s, SpellValidation).valid) {
} else if (!validator.validate(s, SpellValidation).valid) {
reject({
"message": `Le modèle de sortilège n'est pas respecté : ${v.validate(s, SpellValidation).errors}`,
"message": `Le modèle de sortilège n'est pas respecté : ${validator.validate(s, SpellValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(s.name) || isXSSAttempt(s.description) || isXSSAttempt(s.cost)) {
@@ -205,17 +205,17 @@ class SpellRepository {
"code": 403,
});
} else {
new model({id: id})
.fetch({require: true, withRelated: [ 'schools.meta_schools', 'variables', 'ingredients', 'author' ]})
new model({ id: id })
.fetch({ require: true, withRelated: ['schools.meta_schools', 'variables', 'ingredients', 'author'] })
.then(v => {
bookshelf.transaction(t => {
return v.save({
'name': s.name,
'description': s.description,
'level': s.level,
'charge' : s.charge,
'cost' : s.cost,
'is_ritual' : s.is_ritual
'charge': s.charge,
'cost': s.cost,
'is_ritual': s.is_ritual
}, {
method: 'update',
transacting: t
@@ -224,19 +224,19 @@ class SpellRepository {
.tap(spell => {
if (s.schools) {
let schools = spell.related('school');
return spell.schools().detach(schools, { transacting: t});
return spell.schools().detach(schools, { transacting: t });
}
})
.tap(spell => {
if (s.variables) {
let variables = spell.related('variable');
return spell.variables().detach(variables, { transacting: t});
return spell.variables().detach(variables, { transacting: t });
}
})
.tap(spell => {
if (s.ingredients) {
let ingredients = spell.related('ingredient');
return spell.ingredients().detach(ingredients, { transacting: t});
return spell.ingredients().detach(ingredients, { transacting: t });
}
})
.tap(spell => {
@@ -269,7 +269,7 @@ class SpellRepository {
})
})
.then(v => {
return v.load([ 'schools.meta_schools', 'variables', 'ingredients', 'author' ]);
return v.load(['schools.meta_schools', 'variables', 'ingredients', 'author']);
})
.then(v => {
resolve(this.getOne(v.id));
@@ -296,8 +296,8 @@ class SpellRepository {
deleteOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({require: true, withRelated: [ 'schools.meta_schools', 'variables', 'ingredients', 'author' ]})
.where({ 'id': id })
.fetch({ require: true, withRelated: ['schools.meta_schools', 'variables', 'ingredients', 'author'] })
.then(v => {
v.schools().detach();
v.variables().detach();

View File

@@ -1,34 +1,41 @@
'use strict'
// Bookshelf
const bookshelf = require('../database/bookshelf').bookshelf
const model = require('../models/user-model')
const bookshelf = require('../database/bookshelf').bookshelf;
const model = require('../models/user-model');
const token_model = require('../models/api-token-model');
// Hashing and passwords
const bcrypt = require('bcrypt')
const { v4: uuidv4 } = require('uuid')
const bcrypt = require('bcrypt');
const { v4: uuidv4 } = require('uuid');
// Mailing methods
const mails = require('../smtp/mails')
const mails = require('../smtp/mails');
// Model validation
const Validator = require('jsonschema').Validator
const v = new Validator()
const UserValidation = require("../validations/UserValidation")
v.addSchema(UserValidation, "/UserValidation")
const Validator = require('jsonschema').Validator;
const validator = new Validator();
const UserValidation = require("../validations/UserValidation");
validator.addSchema(UserValidation, "/UserValidation");
// Validations
const isXSSAttempt = require('../functions').isXSSAttempt
const isEmptyObject = require('../functions').isEmptyObject
const isXSSAttempt = require('../functions').isXSSAttempt;
const isEmptyObject = require('../functions').isEmptyObject;
class UserRepository {
constructor() {
}
/**
* Fetches all users in the dabatase.
*
* @returns { Promise }
* Fulfilled data: Array of user objects.
*/
getAll() {
return new Promise((resolve, reject) => {
new model()
.fetchAll({ withRelated: [ 'role.permissions' ] })
.fetchAll({ withRelated: ['role.permissions'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }));
})
@@ -42,11 +49,29 @@ class UserRepository {
})
}
/**
* Fetches a user object associated with the uuid.
*
* @param { string } uuid
* @param { boolean } full
* Whether the password should also be fetched. (should never be true unless you want to log the user)
*
* @returns { Promise }
* Fulfilled data: Queried user object.
*/
getOneByUUID(uuid, full) {
return new Promise((resolve, reject) => {
if (!(uuid)) {
reject({
"message": "La requête doit renseigner un uuid.",
"code": 400,
})
}
new model()
.where({ 'uuid' : uuid })
.fetch({ withRelated: [ 'role.permissions' ] })
.where({ 'uuid': uuid })
.fetch({ withRelated: ['role.permissions'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true, visibility: !full }));
})
@@ -59,11 +84,29 @@ class UserRepository {
})
}
/**
* Fetches a user object associated with the mail address.
*
* @param { string } mail
* @param { boolean } full
* Whether the password should also be fetched. (should never be true unless you want to log the user)
*
* @returns { Promise }
* Fulfilled data: Queried user object.
*/
getOneByEmail(mail, full) {
return new Promise((resolve, reject) => {
if (!(mail)) {
reject({
"message": "La requête doit renseigner un email.",
"code": 400,
})
}
new model()
.where({ 'mail': mail })
.fetch({ withRelated: [ 'role' ] })
.fetch({ withRelated: ['role'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true, visibility: !full }));
})
@@ -76,11 +119,26 @@ class UserRepository {
})
}
/**
* Fetches all spells linked to a user's uuid
*
* @param { string } uuid
*
* @returns
*/
getSpellsFromOne(uuid) {
return new Promise((resolve, reject) => {
if (!(uuid)) {
reject({
"message": "La requête doit renseigner un uuid.",
"code": 400,
})
}
new model()
.where({ 'uuid': uuid })
.fetch({ withRelated: [ 'role', 'spells.schools.meta_schools', 'spells.variables', 'spells.ingredients' ] })
.fetch({ withRelated: ['role', 'spells.schools.meta_schools', 'spells.variables', 'spells.ingredients'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }));
})
@@ -93,6 +151,15 @@ class UserRepository {
})
}
/**
* Registers a user based on the model at ./models/user-model.js.
*
* @param { object} u
* User object
*
* @returns { Promise }
* Fulfilled data: Queried user object.
*/
addOne(u) {
return new Promise(async (resolve, reject) => {
// Checks if body exists and if the model fits, and throws errors if it doesn't
@@ -101,9 +168,9 @@ class UserRepository {
"message": "Le corps de requête ne peut être vide.",
"code": 403,
})
} else if (!v.validate(u, UserValidation).valid) {
} else if (!validator.validate(u, UserValidation).valid) {
reject({
"message": "Structure de la requête invalide - " + v.validate(u, UserValidation).errors,
"message": "Structure de la requête invalide - " + validator.validate(u, UserValidation).errors,
"code": 403,
})
} else if (isXSSAttempt(u.name) || isXSSAttempt(u.password) || isXSSAttempt(u.mail)) {
@@ -112,9 +179,9 @@ class UserRepository {
"code": 403,
})
} else {
let hash = await bcrypt.hash(u.password, 10)
let hash = await bcrypt.hash(u.password, 10);
let uuid = uuidv4()
let uuid = uuidv4();
let verification_token = uuidv4();
this.checkIfEmailAvailable(u.mail)
@@ -150,16 +217,17 @@ class UserRepository {
mail: newUser.mail,
token: verification_token,
}
})
});
// Then resolves the api call
resolve({
"message": `Compte utilisateur #${newUser.id} créé avec succès.`,
"code": 201,
"user": newUser,
})
});
})
.catch(err => {
console.log(err);
resolve({
"message": "Une erreur s'est produite en créant votre compte. Veuillez réessayer ultérieurement ou contactez l'administrateur.",
"code": 500,
@@ -173,10 +241,18 @@ class UserRepository {
})
}
/**
* Verifies an account based on a private UUID token
*
* @param { string } token
* A UUID v4 identification token provided at registration and on special demands
*
* @returns Redirects to login page
*/
verifyUser(token) {
return new Promise((resolve, reject) => {
new model()
.where({ 'verification_token' : token })
.where({ 'verification_token': token })
.fetch()
.then(v => {
bookshelf.transaction(t => {
@@ -188,7 +264,7 @@ class UserRepository {
transacting: t
})
})
.then(v => {
.then(() => {
resolve({
"message": "Insérez ici une future redirection vers le client.",
"code": 202,
@@ -204,52 +280,228 @@ class UserRepository {
});
}
// Log user with an email address and a password
/**
* Logs a user by comparing the dual mail/password inputs
*
* @param { string } mail
* @param { string } password
*
* @return { Promise }
* Fulfilled data: Queried user object.
*/
logUser(mail, password) {
return new Promise((resolve, reject) => {
this.getOneByEmail(mail, true)
.then(async fetchedUser => {
let match = await bcrypt.compare(password, fetchedUser.password)
delete fetchedUser.id
delete fetchedUser.password
let match = await bcrypt.compare(password, fetchedUser.password);
// Makes sure no hash gets out
delete fetchedUser.password;
// If you found a user...
if (match) {
// If they're banned...
if (fetchedUser.banned) {
reject({
"message": `L'utilisateur #${fetchedUser.name} a été banni, la connexion est impossible.`,
"code": 403,
})
});
// If they're not verified...
} else if (!fetchedUser.verified) {
reject({
"message": `L'utilisateur #${fetchedUser.name} n'as pas été vérifié, le compte doit être activé avant la connexion.`,
"code": 401,
})
});
} else {
resolve({
"message": `L'utilisateur #${fetchedUser.name} s'est connecté.`,
"code": 200,
"user": fetchedUser,
})
});
}
} else {
reject({
"message": "Les informations de connexions sont erronées.",
"code": 400,
})
});
}
})
.catch(err => {
reject(err)
reject(err);
})
})
}
// Check if one user already has that email
/**
* Generate a token for the user to use the API
* Requires mail and password for verifying the user
*
* @param { string } mail
* @param { string } password
*
* @returns { Promise }
* Fulfilled data: A unique UUID token string.
*/
genAPIToken(mail, password) {
return new Promise((resolve, reject) => {
this.logUser(mail, password)
.then(v => {
let user = v.user;
let new_token = uuidv4();
bookshelf.transaction(t => {
return new token_model({
'value': new_token,
'user_uuid': user.uuid,
})
.save(null, {
transacting: t
})
.catch(err => {
// If the account already has an API key linked...
if (err.errno == 1062) {
this.fetchAPIKey(user.uuid)
.then(old_api_key => {
reject({
"message": "Votre compte a déjà généré une clé d'API.",
"code": 409,
"API_key": old_api_key.value,
});
});
// Default errors
} else {
throw err
}
});
})
.then(api_key => {
resolve({
"message": "La clé d'API a été généré.",
"code": 201,
"API_key": api_key,
})
})
.catch(err => {
console.log(err);
reject({
"message": "La génération de jeton d'API n'a pas pu être conclue.",
"code": 500,
});
})
})
.catch(err => {
reject(err);
})
})
}
checkAPITokenPerms(token, permissions) {
return new Promise(async (resolve, reject) => {
// If the request doesn't have a token, reject it.
if (!token) {
reject({
"message": "Vous devez utiliser un jeton d'API dans l'en-tête de votre requête.",
"code": 401,
})
}
// Fetches user from token
new token_model()
.where({ 'value': token })
.fetch({ withRelated: 'user.role.permissions' })
// Catches not found errors
.catch(err => {
if (err instanceof token_model.NotFoundError) {
reject({
"message": "Ce jeton n'est affilié à aucun compte.",
"code": 404,
});
} else {
reject({
"message": "Le jeton ou l'utilisateur n'a pas pu être récupéré.",
"code": 500,
});
}
})
.then(fullToken => {
token = fullToken.toJSON({ omitPivot: true });
// If the token is associated with a banned user...
if (token.user.banned) {
reject({
"message": "Le jeton est lié à un utilisateur banni, il n'est donc plus utilisable.",
"code": 401,
});
}
// If the token is associated with a non-verified user...
if (!token.user.verified) {
reject({
"message": "Le jeton est lié à un utilisateur non-vérifié, il n'est donc pas utilisable.",
"code": 401,
});
}
let user_permissions = token.user.role.permissions;
// Convert user_perm to array
user_permissions.forEach((user_perm, i) => {
user_permissions[i] = user_perm.slug;
});
// Loops to check if the person has all the permissions required...
permissions.forEach(perm => {
// If not, reject...
if (!user_permissions.includes(perm)) {
reject({
"message": "Permissions insuffisantes.",
"code": 401
})
}
});
// If everything went well, resolve the promise.
resolve({
"message": "Credentials accepted.",
"code": 200
});
})
// Unhandled errors
.catch(err => {
reject({
"message": "Une erreur inconnue est survenue.",
"code": 500,
});
});
})
}
/**
* Check if the email that was input is available for account creation.
*
* @param {string} mail
*
* @returns { Promise }
* Fulfilled: HTTP 200 if email is available.
* Rejected: HTTP 400-409 if email is already used.
*/
checkIfEmailAvailable(mail) {
return new Promise((resolve, reject) => {
if (!this.validateEmail(mail)) {
if (!(mail)) {
reject({
"message": "La requête doit renseigner un email.",
"code": 400,
})
}
if (!this.validateMail(mail)) {
reject({
"message": "La requête n'est pas un email valide.",
"code": 400,
@@ -272,9 +524,38 @@ class UserRepository {
})
}
validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
/**
* Fetches the associated api_token from a user uuid.
*
* @param { string } uuid
*
* @returns { Promise }
* Fulfilled data: Queried API token object
*/
fetchAPIKey(uuid) {
return new Promise((resolve, reject) => {
new token_model()
.where({ 'user_uuid': uuid })
.fetch()
.then(v => {
resolve(v.toJSON({ omitPivot: true }));
})
.catch(err => {
console.log(err);
})
})
}
/**
* Whether a mail is correctly formed and ripe for receiving, ie: xxx@yyy.zzz.
*
* @param { string } mail
*
* @returns { boolean }
*/
validateMail(mail) {
const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return regex.test(String(mail).toLowerCase());
}
}

View File

@@ -5,9 +5,9 @@ const model = require('../models/variable-model')
// Model validation
const Validator = require('jsonschema').Validator
const v = new Validator()
const validator = new Validator()
const VariableValidation = require("../validations/VariableValidation")
v.addSchema(VariableValidation, "/VariableValidation")
validator.addSchema(VariableValidation, "/VariableValidation")
// Validations
const isXSSAttempt = require('../functions').isXSSAttempt
@@ -39,8 +39,8 @@ class VariableRepository {
getOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['spells']})
.where({ 'id': id })
.fetch({ withRelated: ['spells'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -57,8 +57,8 @@ class VariableRepository {
getSpellsFromOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({ withRelated: ['spells', 'spells.schools', 'spells.variables', 'spells.ingredients', 'spells.schools.meta_schools']})
.where({ 'id': id })
.fetch({ withRelated: ['spells', 'spells.schools', 'spells.variables', 'spells.ingredients', 'spells.schools.meta_schools'] })
.then(v => {
resolve(v.toJSON({ omitPivot: true }))
})
@@ -80,9 +80,9 @@ class VariableRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(vr, VariableValidation).valid) {
} else if (!validator.validate(vr, VariableValidation).valid) {
reject({
"message": `Le modèle de variable n'est pas respecté : ${v.validate(s, VariableValidation).errors}`,
"message": `Le modèle de variable n'est pas respecté : ${validator.validate(s, VariableValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(vr.description)) {
@@ -126,9 +126,9 @@ class VariableRepository {
"message": "Le corps de la requête ne peut pas être vide.",
"code": 403,
});
} else if (!v.validate(vr, VariableValidation).valid) {
} else if (!validator.validate(vr, VariableValidation).valid) {
reject({
"message": `Le modèle de variable n'est pas respecté : ${v.validate(s, VariableValidation).errors}`,
"message": `Le modèle de variable n'est pas respecté : ${validator.validate(s, VariableValidation).errors}`,
"code": 403,
});
} else if (isXSSAttempt(vr.description)) {
@@ -137,8 +137,8 @@ class VariableRepository {
"code": 403,
});
} else {
new model({id: id})
.fetch({require: true, withRelated: ['spells']})
new model({ id: id })
.fetch({ require: true, withRelated: ['spells'] })
.then(v => {
bookshelf.transaction(t => {
return v.save({
@@ -180,8 +180,8 @@ class VariableRepository {
deleteOne(id) {
return new Promise((resolve, reject) => {
new model()
.where({ 'id' : id })
.fetch({require: true, withRelated: ['spells']})
.where({ 'id': id })
.fetch({ require: true, withRelated: ['spells'] })
.then(v => {
v.spells().detach()
v.destroy()

29
api/routes/auth.js Normal file
View File

@@ -0,0 +1,29 @@
'use strict'
// Router
const express = require('express');
let router = express.Router();
// Repository
const UserRepository = require('../repositories/user-repository');
const Users = new UserRepository();
// ROUTES
// GEN API TOKEN
const generateAPIToken = (mail, password) => {
return Users.genAPIToken(mail, password)
.catch(err => {
throw err
})
}
router.get('/genToken', async (req, res) => {
generateAPIToken(req.body.mail, req.body.password)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.end(JSON.stringify(v))
})
.catch(err => {
res.status(err.code).send(JSON.stringify(err))
})
})
module.exports = router;

View File

@@ -1,11 +1,14 @@
const spells = require('./spells')
const schools = require('./schools')
const meta_schools = require('./meta_schools')
const variables = require('./variables')
const ingredients = require('./ingredients')
const users = require('./users')
const spells = require('./spells');
const schools = require('./schools');
const meta_schools = require('./meta_schools');
const variables = require('./variables');
const ingredients = require('./ingredients');
const users = require('./users');
const auth = require('./auth');
module.exports = {
auth,
spells,
schools,
meta_schools,

View File

@@ -1,17 +1,19 @@
'use strict'
// Router
const express = require('express')
let router = express.Router()
const express = require('express');
let router = express.Router();
// Connection
const connection = require('../database/bookshelf')
const functions = require('../functions')
// AuthGuard
const authGuard = require('./middleware/authGuard');
// Repository
const IngredientRepository = require('../repositories/ingredient-repository');
const Ingredients = new IngredientRepository();
// Functions
const functions = require('../functions');
// ROUTES
// GET ALL ------------------
const getIngredients = () => {
@@ -21,7 +23,9 @@ const getIngredients = () => {
throw err
})
}
router.get('/', async (req, res) => {
router.get(
'/',
async (req, res) => {
getIngredients()
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -35,7 +39,7 @@ router.get('/', async (req, res) => {
})
)
})
})
})
// GET ONE ------------------
@@ -46,7 +50,9 @@ const getIngredient = (id) => {
throw err
})
}
router.get('/:id/', async (req, res) => {
router.get(
'/:id/',
async (req, res) => {
getIngredient(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -60,7 +66,7 @@ router.get('/:id/', async (req, res) => {
})
)
})
})
})
// GET SPELLS FROM ONE ------------------
@@ -71,7 +77,9 @@ const getSpellsFromOne = (id) => {
throw err
})
}
router.get('/:id/spells', async (req, res) => {
router.get(
'/:id/spells',
async (req, res) => {
getSpellsFromOne(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -85,7 +93,7 @@ router.get('/:id/spells', async (req, res) => {
})
)
})
})
})
// CREATE ONE ------------------
@@ -96,7 +104,10 @@ const addIngredient = (igr) => {
throw err
})
}
router.post('/', async (req, res) => {
router.post(
'/',
authGuard(['SUBMIT_INGREDIENTS']),
async (req, res) => {
addIngredient(req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -110,7 +121,7 @@ router.post('/', async (req, res) => {
})
)
})
})
})
// UPDATE ONE ------------------
@@ -121,7 +132,10 @@ const updateIngredient = (id, igr) => {
throw err
})
}
router.put('/:id/', async (req, res) => {
router.put(
'/:id/',
authGuard(['SUBMIT_INGREDIENTS', 'MODIFY_INGREDIENTS']),
async (req, res) => {
updateIngredient(req.params.id, req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -135,7 +149,7 @@ router.put('/:id/', async (req, res) => {
})
)
})
})
})
// DELETE ONE ------------------
@@ -146,7 +160,10 @@ const deleteIngredient = (id) => {
throw err
})
}
router.delete('/:id/', async (req, res) => {
router.delete(
'/:id/',
authGuard(['SUBMIT_INGREDIENTS', 'MODIFY_INGREDIENTS', 'DELETE_INGREDIENTS']),
async (req, res) => {
deleteIngredient(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -160,7 +177,7 @@ router.delete('/:id/', async (req, res) => {
})
)
})
})
})
// Param validations
router.param('id', functions.paramIntCheck)

View File

@@ -1,12 +1,11 @@
'use strict'
// Router
const express = require('express')
let router = express.Router()
const express = require('express');
let router = express.Router();
// Connection
const connection = require('../database/bookshelf')
const functions = require('../functions')
// Functions
const functions = require('../functions');
// Repository
const MetaSchoolRepository = require('../repositories/meta-school-repository');

View File

@@ -0,0 +1,23 @@
// Repository
const UserRepository = require('../../repositories/user-repository');
const Users = new UserRepository();
// AUTHGUARD
const authGuard = (permissions) => {
return async (req, res, next) => {
// Get token from headers
let api_token = req.headers['auracle_key'];
// Uses repo to validate the associated perms with the token
Users.checkAPITokenPerms(api_token, permissions)
.then(v => {
next();
})
.catch(err => {
res.status(err.code).send(JSON.stringify(err))
});
}
}
module.exports = authGuard;

View File

@@ -1,17 +1,19 @@
'use strict'
// Router
const express = require('express')
let router = express.Router()
const express = require('express');
let router = express.Router();
// Connection
const connection = require('../database/bookshelf')
const functions = require('../functions')
// AuthGuard
const authGuard = require('./middleware/authGuard');
// Repository
const SchoolRepository = require('../repositories/school-repository');
const Schools = new SchoolRepository();
// Functions
const functions = require('../functions');
// ROUTES
// GET ALL ------------------
const getSchools = () => {
@@ -21,7 +23,9 @@ const getSchools = () => {
throw err
})
}
router.get('/', async (req, res) => {
router.get(
'/',
async (req, res) => {
getSchools()
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -35,7 +39,7 @@ router.get('/', async (req, res) => {
})
)
})
})
})
// GET ONE ------------------
@@ -46,7 +50,9 @@ const getSchool = (id) => {
throw err
})
}
router.get('/:id/', async (req, res) => {
router.get(
'/:id/',
async (req, res) => {
getSchool(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -60,7 +66,7 @@ router.get('/:id/', async (req, res) => {
})
)
})
})
})
// GET SPELLS FROM ONE ------------------
@@ -71,7 +77,9 @@ const getSpellsFromOne = (id) => {
throw err
})
}
router.get('/:id/spells', async (req, res) => {
router.get(
'/:id/spells',
async (req, res) => {
getSpellsFromOne(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -85,7 +93,7 @@ router.get('/:id/spells', async (req, res) => {
})
)
})
})
})
// CREATE ONE ------------------
@@ -96,7 +104,10 @@ const addSchool = (s) => {
throw err
})
}
router.post('/', async (req, res) => {
router.post(
'/',
authGuard(['SUBMIT_SCHOOL']),
async (req, res) => {
addSchool(req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -110,7 +121,7 @@ router.post('/', async (req, res) => {
})
)
})
})
})
// UPDATE ONE ------------------
const updateSchool = (id, s) => {
@@ -120,7 +131,10 @@ const updateSchool = (id, s) => {
throw err
})
}
router.put('/:id/', async (req, res) => {
router.put(
'/:id/',
authGuard(['SUBMIT_SCHOOLS', 'MODIFY_SCHOOLS']),
async (req, res) => {
updateSchool(req.params.id, req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -134,7 +148,7 @@ router.put('/:id/', async (req, res) => {
})
)
})
})
})
// DELETE ONE ------------------
@@ -145,7 +159,10 @@ const deleteSchool = (id) => {
throw err
})
}
router.delete('/:id/', async (req, res) => {
router.delete(
'/:id/',
authGuard(['SUBMIT_SCHOOLS', 'MODIFY_SCHOOLS', 'DELETE_SCHOOLS']),
async (req, res) => {
deleteSchool(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -159,7 +176,7 @@ router.delete('/:id/', async (req, res) => {
})
)
})
})
})
// Param validations
router.param('id', functions.paramIntCheck)

View File

@@ -1,17 +1,19 @@
'use strict'
// Router
const express = require('express')
let router = express.Router()
const express = require('express');
let router = express.Router();
// Connection
const connection = require('../database/bookshelf')
const functions = require('../functions')
// AuthGuard
const authGuard = require('./middleware/authGuard');
// Repository
const SpellReposity = require('../repositories/spell-repository');
const Spells = new SpellReposity();
// Functions
const functions = require('../functions');
// ROUTES
// GET ALL PUBLIC ------------------
const getPublicSpells = (name, description, level, charge, cost, ritual) => {
@@ -21,7 +23,9 @@ const getPublicSpells = (name, description, level, charge, cost, ritual) => {
throw err
})
}
router.get('//:name?/:description?/:level?/:charge?/:cost?/:ritual?/', async (req, res) => {
router.get(
'//:name?/:description?/:level?/:charge?/:cost?/:ritual?/',
async (req, res) => {
getPublicSpells(req.query.name, req.query.description, req.query.level, req.query.charge, req.query.cost, req.query.ritual)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -35,7 +39,7 @@ router.get('//:name?/:description?/:level?/:charge?/:cost?/:ritual?/', async (re
})
)
})
})
})
// GET ALL ------------------
const getSpells = (name, description, level, charge, cost, ritual) => {
@@ -45,7 +49,10 @@ const getSpells = (name, description, level, charge, cost, ritual) => {
throw err
})
}
router.get('/private/:name?/:description?/:level?/:charge?/:cost?/:ritual?/', async (req, res) => {
router.get(
'/private/:name?/:description?/:level?/:charge?/:cost?/:ritual?/',
authGuard(['SECRET_SPELLS']),
async (req, res) => {
getSpells(req.query.name, req.query.description, req.query.level, req.query.charge, req.query.cost, req.query.ritual)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -59,7 +66,7 @@ router.get('/private/:name?/:description?/:level?/:charge?/:cost?/:ritual?/', as
})
)
})
})
})
// GET SOME ------------------
const getSomeSpells = (page) => {
@@ -69,7 +76,9 @@ const getSomeSpells = (page) => {
throw err
})
}
router.get('/page/:page', async (req, res) => {
router.get(
'/page/:page',
async (req, res) => {
getSomeSpells(req.params.page)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -83,7 +92,7 @@ router.get('/page/:page', async (req, res) => {
})
)
})
})
})
// GET ONE ------------------
const getSpell = (id) => {
@@ -93,7 +102,9 @@ const getSpell = (id) => {
throw err
})
}
router.get('/:id/', async (req, res) => {
router.get(
'/:id/',
async (req, res) => {
getSpell(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -107,7 +118,7 @@ router.get('/:id/', async (req, res) => {
})
)
})
})
})
// CREATE ONE ------------------
@@ -118,7 +129,10 @@ const addSpell = (s) => {
throw err
})
}
router.post('/', async (req, res) => {
router.post(
'/',
authGuard(['SUBMIT_SPELLS']),
async (req, res) => {
addSpell(req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -132,7 +146,7 @@ router.post('/', async (req, res) => {
})
)
})
})
})
// UPDATE ONE ------------------
@@ -143,7 +157,10 @@ const updateSpell = (id, s) => {
throw err
})
}
router.put('/:id/', async (req, res) => {
router.put(
'/:id/',
authGuard(['SUBMIT_SPELLS', 'MODIFY_SPELLS']),
async (req, res) => {
updateSpell(req.params.id, req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -157,7 +174,7 @@ router.put('/:id/', async (req, res) => {
})
)
})
})
})
// DELETE ONE ------------------
@@ -168,7 +185,10 @@ const deleteSpell = (id) => {
throw err
})
}
router.delete('/:id/', async (req, res) => {
router.delete(
'/:id/',
authGuard(['SUBMIT_SPELLS', 'MODIFY_SPELLS', 'DELETE_SPELLS']),
async (req, res) => {
deleteSpell(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -182,7 +202,7 @@ router.delete('/:id/', async (req, res) => {
})
)
})
})
})
// Param validations
router.param('id', functions.paramIntCheck)

View File

@@ -1,12 +1,11 @@
'use strict'
// Router
const express = require('express')
let router = express.Router()
const express = require('express');
let router = express.Router();
// Connection
const connection = require('../database/bookshelf')
const db = connection.db
// AuthGuard
const authGuard = require('./middleware/authGuard');
// Repository
const UserRepository = require('../repositories/user-repository');

View File

@@ -1,17 +1,19 @@
'use strict'
// Router
const express = require('express')
let router = express.Router()
const express = require('express');
let router = express.Router();
// Connection
const connection = require('../database/bookshelf')
const functions = require('../functions')
// AuthGuard
const authGuard = require('./middleware/authGuard');
// Repository
const VariableRepository = require('../repositories/variable-repository');
const Variables = new VariableRepository();
// Functions
const functions = require('../functions');
// ROUTES
// GET ALL ------------------
const getvariables = () => {
@@ -21,7 +23,9 @@ const getvariables = () => {
throw err
})
}
router.get('/', async (req, res) => {
router.get(
'/',
async (req, res) => {
getvariables()
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -35,7 +39,7 @@ router.get('/', async (req, res) => {
})
)
})
})
})
// GET ONE ------------------
@@ -46,7 +50,9 @@ const getVariable = (id) => {
throw err
})
}
router.get('/:id/', async (req, res) => {
router.get(
'/:id/',
async (req, res) => {
getVariable(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -60,7 +66,7 @@ router.get('/:id/', async (req, res) => {
})
)
})
})
})
// GET SPELLS FROM ONE ------------------
@@ -71,7 +77,9 @@ const getSpellsFromOne = (id) => {
throw err
})
}
router.get('/:id/spells', async (req, res) => {
router.get(
'/:id/spells',
async (req, res) => {
getSpellsFromOne(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -85,7 +93,7 @@ router.get('/:id/spells', async (req, res) => {
})
)
})
})
})
// CREATE ONE ------------------
@@ -96,7 +104,10 @@ const addVariable = (vr) => {
throw err
})
}
router.post('/', async (req, res) => {
router.post(
'/',
authGuard(['SUBMIT_VARIABLES']),
async (req, res) => {
addVariable(req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -110,7 +121,7 @@ router.post('/', async (req, res) => {
})
)
})
})
})
// UPDATE ONE ------------------
@@ -121,7 +132,10 @@ const updateVariable = (id, vr) => {
throw err
})
}
router.put('/:id/', async (req, res) => {
router.put(
'/:id/',
authGuard(['SUBMIT_VARIABLES', 'MODIFY_VARIABLES']),
async (req, res) => {
updateVariable(req.params.id, req.body)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -135,7 +149,7 @@ router.put('/:id/', async (req, res) => {
})
)
})
})
})
// DELETE ONE ------------------
@@ -146,7 +160,10 @@ const deleteVariable = (id) => {
throw err
})
}
router.delete('/:id/', async (req, res) => {
router.delete(
'/:id/',
authGuard(['SUBMIT_VARIABLES', 'MODIFY_VARIABLES', 'DELETE_VARIABLES']),
async (req, res) => {
deleteVariable(req.params.id)
.then(v => {
res.setHeader('Content-Type', 'application/json;charset=utf-8')
@@ -160,7 +177,7 @@ router.delete('/:id/', async (req, res) => {
})
)
})
})
})
// Param validations
router.param('id', functions.paramIntCheck)

View File

@@ -8,7 +8,7 @@ const sender = 'tymos@auracle.io';
// Fetches a HTML template file for parsing
const getTemplateFile = (path) => {
return new Promise((resolve, reject) => {
fs.readFile(path, {encoding: 'utf-8'}, (err, html) => {
fs.readFile(path, { encoding: 'utf-8' }, (err, html) => {
if (err) {
reject(err);
} else {

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<head>
<title>Template Email Auracle Inscription</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Playfair+Display:wght@700;800;900&display=swap');
@@ -44,6 +45,7 @@
padding: 10vh 5vw;
background: url('https://i.imgur.com/IxEKKEY.png') center center fixed repeat;
}
.wrapper:before {
display: block;
content: '';
@@ -62,7 +64,8 @@
z-index: 5;
}
header, footer {
header,
footer {
text-align: center;
color: var(--white);
background-color: rgb(var(--black));
@@ -76,7 +79,8 @@
padding: 30px;
}
</style>
</head>
</head>
<body>
<div class="wrapper">
<div class="container">
@@ -85,8 +89,17 @@
</header>
<main>
<p>Votre compte Auracle a bien été enregistré et est rattaché à cette adresse mail.</p>
<p>Cependant, afin de garantir la sécurité de vos données, votre compte doit être vérifié avant votre première connexion. Vous pouvez <a href="http://localhost:2814/api/users/verification/{{ user.token }}">suivre ce lien afin de confirmer votre inscription.</a></p>
<p>Si vous rencontrez des problèmes, n'hésitez pas à envoyer un mail à <a href="mailto:support@auracle.io">support@auracle.io</a></p>
<p>Cependant, afin de garantir la sécurité de vos données, votre compte doit être vérifié avant votre
première connexion. Vous pouvez
<a href="http: //localhost:2814/api/v1/users/verification/{{ user.token }}">
suivre ce lien afin de confirmer votre inscription.
</a>
</p>
<p>Si vous rencontrez des problèmes, n'hésitez pas à envoyer un mail à
<a href="mailto:support@auracle.io">
support@auracle.io
</a>
</p>
<p><strong>Merci de votre soutien !</strong></p>
<p class="italics">Bien sincèrement, Izàc Tymos.</p>
</main>
@@ -97,4 +110,5 @@
</div>
</div>
</body>
</html>