parent
65c81de4ef
commit
c153385c40
@ -0,0 +1,167 @@
|
||||
/*
|
||||
Ecrire une API en node, tournant sur le port 8088,
|
||||
permettant de vérifier qu'un utilisateur est existant dans une
|
||||
base de donnée "sécurisée" dans laquelle le mot de passe est
|
||||
stocké en MD5.
|
||||
Les informations de connexion seront stockées en json.
|
||||
|
||||
Votre réponse sera sous forme d'un zip.
|
||||
Elle contiendra au minimum :
|
||||
- un fichier SQL pour la création de la base avec un utilisateur dédié
|
||||
- un deuxième fichier SQL pour la création de la table et ajoutant un au moins un utilisateur à la table
|
||||
- un fichier README indiquant comment installer votre projet et les routes disponibles
|
||||
- un fichier package.json
|
||||
- un fichier api.js contenant votre code Node
|
||||
*/
|
||||
|
||||
// testing :
|
||||
// https://mariadb.com/kb/en/nodejs-connector/
|
||||
|
||||
import http from 'http'; // to create server
|
||||
import url from 'url'; // to parse url
|
||||
import fs from 'fs'; // to load config
|
||||
|
||||
// https://www.npmjs.com/package/email-validator
|
||||
// pour vérifier que le mail est un mail valid :
|
||||
import validator from 'email-validator';
|
||||
|
||||
// pour valider le mot de passe
|
||||
import md5 from 'md5';
|
||||
|
||||
// on ajoute la possibilité de require du json en objet
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const BASIC_API_PORT = 8088;
|
||||
|
||||
var config=null;
|
||||
if(fs.existsSync(process.cwd() + '/config.json')){
|
||||
config = require(process.cwd() + '/config.json');
|
||||
}
|
||||
else {
|
||||
console.log('no config');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
const mariadb = require('mariadb');
|
||||
const { exit } = require('process');
|
||||
const pool = mariadb.createPool({
|
||||
host: config.DB_HOST,
|
||||
port: config.DB_PORT,
|
||||
user: config.DB_USR,
|
||||
password:config.DB_PWD,
|
||||
database: config.DB_NAME,
|
||||
connectionLimit: 5,
|
||||
});
|
||||
|
||||
//const server = http.createServer(function (req, resp) {
|
||||
const app = http.createServer
|
||||
(
|
||||
async (req, res) =>
|
||||
{
|
||||
const parsedURL = url.parse(req.url, true);
|
||||
let response_json = JSON.stringify({"up":false});
|
||||
if(req.method === 'GET' && parsedURL.pathname.startsWith('/up'))
|
||||
{
|
||||
var cnx = null;
|
||||
try
|
||||
{
|
||||
cnx=await pool.getConnection();
|
||||
const tables_res = await cnx.query("SHOW TABLES");
|
||||
if(tables_res.length > 0)
|
||||
{
|
||||
response_json = JSON.stringify({'up':true});
|
||||
}
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
console.log(err);
|
||||
res.statusCode = 400;
|
||||
res.end(response_json);
|
||||
if(cnx)
|
||||
{
|
||||
return cnx.end();
|
||||
}
|
||||
throw(err);
|
||||
}
|
||||
finally
|
||||
{
|
||||
res.statusCode = 200;
|
||||
res.end(response_json);
|
||||
if(cnx)
|
||||
{
|
||||
return cnx.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(req.method === 'POST' && parsedURL.pathname.startsWith('/check'))
|
||||
{
|
||||
console.log("checking :");
|
||||
let responseString = "";
|
||||
req.on('data', function (varposted) {
|
||||
responseString += varposted;
|
||||
}).on('end', async function () {
|
||||
if(responseString.indexOf('?')<0) {
|
||||
responseString = '?'+responseString;
|
||||
}
|
||||
let parsedData = url.parse(responseString, true);
|
||||
console.log("ParsedData", parsedData);
|
||||
let response_content = {'up':false,'emailtocheck':''};
|
||||
if(parsedData.query['email'] && parsedData.query['password']) {
|
||||
response_content.emailtocheck = parsedData.query['email'];
|
||||
response_content.hashedpassword = md5(parsedData.query['password']);
|
||||
if(validator.validate(response_content.emailtocheck))
|
||||
{
|
||||
cnx=await pool.getConnection();
|
||||
//let query="SELECT * FROM users WHERE courriel = '" + response_content.emailtocheck +"'";
|
||||
let query="SELECT * FROM users WHERE courriel = ? AND mot_de_passe = ?";
|
||||
console.log('query : ',query,' with ? = ', response_content.emailtocheck);
|
||||
const email_res = await cnx.query(query, [response_content.emailtocheck, response_content.hashedpassword]);
|
||||
if(email_res.length > 0)
|
||||
{
|
||||
response_content.up=true;
|
||||
response_content.emailValid=true;
|
||||
}
|
||||
else
|
||||
{
|
||||
response_content.up=true;
|
||||
response_content.emailValid=false;
|
||||
response_content.error='Not Found';
|
||||
response_content.error_code=404;
|
||||
}
|
||||
res.statusCode = 200;
|
||||
res.end(JSON.stringify(response_content));
|
||||
if(cnx)
|
||||
{
|
||||
return cnx.end();
|
||||
}
|
||||
}
|
||||
else {
|
||||
response_content.up=true;
|
||||
response_content.emailValid=false;
|
||||
response_content.error='I\'m a teapot';
|
||||
response_content.error_code=418;
|
||||
res.statusCode = 200;
|
||||
res.end(JSON.stringify(response_content));
|
||||
}
|
||||
}
|
||||
else {
|
||||
response_content.up=true;
|
||||
response_content.emailValid=false;
|
||||
response_content.emailtocheck=parsedData.query['email'];
|
||||
response_content.error='Invalid query mail or password do match requirements';
|
||||
response_content.error_code=400;
|
||||
res.statusCode = 200;
|
||||
res.end(JSON.stringify(response_content));
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
res.statusCode = 400;
|
||||
res.end("API Endpoint Not Supported");
|
||||
}
|
||||
}
|
||||
);
|
||||
app.listen(BASIC_API_PORT);
|
||||
console.log('mariadb test api started');
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"DB_HOST" : "127.0.0.1",
|
||||
"DB_PORT" : "3306",
|
||||
"DB_USR" : "eval_user",
|
||||
"DB_PWD" : "ov62p67t7PId9N3",
|
||||
"DB_NAME" : "eval_bdd"
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
CREATE DATABASE IF NOT EXISTS `eval_bdd`
|
||||
DEFAULT CHARACTER SET utf8
|
||||
DEFAULT COLLATE utf8_general_ci;
|
||||
|
||||
GRANT USAGE ON *.* TO "eval_user"@"localhost" IDENTIFIED BY "ov62p67t7PId9N3"
|
||||
WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0;
|
||||
|
||||
GRANT USAGE ON *.* TO "eval_user"@"%" IDENTIFIED BY "ov62p67t7PId9N3"
|
||||
WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0;
|
||||
|
||||
GRANT ALL PRIVILEGES ON `eval_bdd`.* TO 'eval_user'@'%';
|
||||
GRANT ALL PRIVILEGES ON `eval_bdd`.* TO 'eval_user'@'localhost';
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
Ecrire une API en node, tournant sur le port 8088,
|
||||
permettant de vérifier qu'un utilisateur est existant dans une
|
||||
base de donnée "sécurisée" dans laquelle le mot de passe est
|
||||
stocké en MD5.
|
||||
Les informations de connexion seront stockées en json.
|
||||
|
||||
Votre réponse sera sous forme d'un zip.
|
||||
Elle contiendra au minimum :
|
||||
- un fichier SQL pour la création de la base avec un utilisateur dédié
|
||||
- un deuxième fichier SQL pour la création de la table et ajoutant un au moins un utilisateur à la table
|
||||
- un fichier README indiquant comment installer votre projet et les routes disponibles
|
||||
- un fichier package.json
|
||||
- un fichier api.js contenant votre code Node
|
||||
*/
|
||||
|
||||
// testing :
|
||||
// https://mariadb.com/kb/en/nodejs-connector/
|
||||
|
||||
|
||||
const http = require ('http'); // to create server
|
||||
const url = require('url'); // to parse url
|
||||
const fs = require('fs'); // to load config
|
||||
const BASIC_API_PORT = 8088;
|
||||
|
||||
var config=null;
|
||||
if(fs.existsSync(process.cwd() + '/config.json')){
|
||||
config = require(process.cwd() + '/config.json');
|
||||
}
|
||||
else {
|
||||
console.log('no config');
|
||||
}
|
||||
|
||||
|
||||
const mariadb = require('mariadb');
|
||||
const { exit } = require('process');
|
||||
const pool = mariadb.createPool({
|
||||
host: config.DB_HOST,
|
||||
port: config.DB_PORT,
|
||||
user: config.DB_USR,
|
||||
password:config.DB_PWD,
|
||||
database: config.DB_NAME,
|
||||
connectionLimit: 5,
|
||||
});
|
||||
|
||||
//const server = http.createServer(function (req, resp) {
|
||||
const app = http.createServer
|
||||
(
|
||||
async (req, res) =>
|
||||
{
|
||||
const parsedURL = url.parse(req.url, true);
|
||||
var response_json = JSON.stringify({"up":false});
|
||||
if(req.method === 'GET' && parsedURL.pathname.startsWith('/up'))
|
||||
{
|
||||
var cnx = null;
|
||||
try
|
||||
{
|
||||
cnx=await pool.getConnection();
|
||||
const tables_res = await cnx.query("SHOW TABLES");
|
||||
if(tables_res.length > 0)
|
||||
{
|
||||
response_json = JSON.stringify({'up':true});
|
||||
}
|
||||
}
|
||||
catch(err)
|
||||
{
|
||||
console.log(err);
|
||||
res.statusCode = 400;
|
||||
res.end(response_json);
|
||||
if(cnx)
|
||||
{
|
||||
return cnx.end();
|
||||
}
|
||||
throw(err);
|
||||
}
|
||||
finally
|
||||
{
|
||||
res.statusCode = 200;
|
||||
res.end(response_json);
|
||||
if(cnx)
|
||||
{
|
||||
return cnx.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(req.method === 'POST' && parsedURL.pathname.startsWith('/check'))
|
||||
{
|
||||
console.log("checking :");
|
||||
responseString = "?";
|
||||
req.on('data', function (varposted) {
|
||||
responseString += varposted;
|
||||
}).on('end', async function () {
|
||||
parsedData = url.parse(responseString, true);
|
||||
console.log("ParsedData", parsedData);
|
||||
let response_content = {'up':false,'emailtocheck':''};
|
||||
if(parsedData.query['email']) {
|
||||
response_content.emailtocheck = parsedData.query['email'];
|
||||
cnx=await pool.getConnection();
|
||||
let query="SELECT * FROM users WHERE courriel = '"+ response_content.emailtocheck +"'";
|
||||
console.log(query);
|
||||
const email_res = await cnx.query(query);
|
||||
if(email_res.length > 0)
|
||||
{
|
||||
response_content.up=true;
|
||||
response_content.emailValid=true;
|
||||
}
|
||||
else
|
||||
{
|
||||
response_content.up=true;
|
||||
response_content.emailValid=false;
|
||||
}
|
||||
res.statusCode = 200;
|
||||
res.end(JSON.stringify(response_content));
|
||||
if(cnx)
|
||||
{
|
||||
return cnx.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
res.statusCode = 400;
|
||||
res.end("API Endpoint Not Supported");
|
||||
}
|
||||
}
|
||||
);
|
||||
app.listen(BASIC_API_PORT);
|
||||
console.log('mariadb test api started');
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"DB_HOST" : "127.0.0.1",
|
||||
"DB_PORT" : "3306",
|
||||
"DB_USR" : "eval_user",
|
||||
"DB_PWD" : "ov62p67t7PId9N3",
|
||||
"DB_NAME" : "eval_bdd"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "eval",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "front.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"mariadb": "^3.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"node-fetch": "^3.2.6"
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
Faire un site en node, tourant sur le port 80.
|
||||
Le site permet de saisir un email et un mot de passe dans un formulaire.
|
||||
En retour de soumission du formulaire le site affichera l'email, la chaine MD5 du mot de passe, et l'heure+date de la tentative connexion.
|
||||
*/
|
||||
|
||||
/*
|
||||
Votre réponse sera sous forme d'un zip.
|
||||
Elle contiendra au minimum :
|
||||
- un fichier README indiquant comment installer votre projet et les routes disponibles
|
||||
- un fichier package.json
|
||||
- un fichier front.js contenant votre code Node
|
||||
*/
|
||||
import http from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
// npm i md5
|
||||
// https://www.npmjs.com/package/md5
|
||||
import md5 from 'md5';
|
||||
|
||||
function outputHTML5(status, response, content, title)
|
||||
{
|
||||
response.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
response.write(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${title}</title>
|
||||
<style type="text/css">*{ font-family: sans-serif }</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
);
|
||||
response.end();
|
||||
}
|
||||
|
||||
var server = http.createServer(function (request, response) {
|
||||
if(request.url === "/" && request.method==="GET") {
|
||||
let formHtml = "<form method='post' action='/'>";
|
||||
formHtml += "<label for='email'>Email</label><input id='email' type='email' value='' name='email' />";
|
||||
formHtml += "<br/><label for='password'>Mot de passe</label><input id='password' type='password' value='' name='password' />";
|
||||
formHtml += "<br/><button type='submit'>aller !</button>"
|
||||
formHtml += "</form>";
|
||||
outputHTML5(200,response,formHtml,"login");
|
||||
}
|
||||
else if(request.url === "/" && request.method==="POST") {
|
||||
let responseHTML ="informations saisies : ";
|
||||
let responseString = "?";
|
||||
request.on('data', function (varposted) {
|
||||
responseHTML += varposted;
|
||||
responseString += varposted;
|
||||
}).on('end', function () {
|
||||
let parsedData = url.parse(responseString, true);
|
||||
console.log("ParsedData", parsedData);
|
||||
Object.keys(parsedData.query).forEach(function(key) {
|
||||
var value=parsedData.query[key];
|
||||
if(key==='password') {
|
||||
value=md5(value);
|
||||
}
|
||||
responseHTML+= "<br/>"+key+" : "+value;
|
||||
});
|
||||
outputHTML5(200,response,responseHTML,"login");
|
||||
});
|
||||
} else {
|
||||
outputHTML5(404,response,'Y\'a rien ici',"Oups");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(80);
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
Modifier votre front (code fait à la section 2) pour qu'il interroge
|
||||
l'API et indique à l'utilisateur si il existe en base de données
|
||||
|
||||
Elle contiendra au minimum :
|
||||
- un fichier README indiquant comment installer votre projet et les routes disponibles
|
||||
- un fichier package.json
|
||||
- un fichier index.js contenant votre code Node
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
import * as url from "url";
|
||||
// npm i node-fetch
|
||||
// https://www.npmjs.com/package/node-fetch
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
function outputHTML5(status, response, content, title)
|
||||
{
|
||||
response.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
response.write(
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${title}</title>
|
||||
<style type="text/css">*{ font-family: sans-serif }</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
);
|
||||
response.end();
|
||||
}
|
||||
|
||||
var server = http.createServer(function (request, response) {
|
||||
if(request.url === "/" && request.method==="GET") {
|
||||
let formHtml = "<form method='post' action='/'>";
|
||||
formHtml += "<label for='email'>Email</label><input id='email' type='email' value='' name='email' />";
|
||||
formHtml += "<br/><label for='password'>Mot de passe</label><input id='password' type='password' value='' name='password' />";
|
||||
formHtml += "<br/><button type='submit'>aller !</button>"
|
||||
formHtml += "</form>";
|
||||
outputHTML5(200,response,formHtml,"login");
|
||||
}
|
||||
else if(request.url === "/" && request.method==="POST") {
|
||||
let responseString = "/?";
|
||||
console.log("In post");
|
||||
request.on('data', function (varposted) {
|
||||
responseString += varposted;
|
||||
}).on('end', async function () {
|
||||
let parsedData = url.parse(responseString, true);
|
||||
|
||||
if(parsedData.query['email']) {
|
||||
console.log('got' + parsedData.query['email'])
|
||||
fetch('http://localhost:8088/check',
|
||||
{
|
||||
method:'POST',
|
||||
body:responseString
|
||||
}
|
||||
)
|
||||
.then(apiresponse =>
|
||||
apiresponse.json()
|
||||
)
|
||||
.then( json => {
|
||||
if(json.emailValid) {
|
||||
outputHTML5(200,response,'Ok ! Connexion possible :)',"Cool!");
|
||||
}
|
||||
else {
|
||||
outputHTML5(200,response,"Cet mail n'existe pas ou le mot de passe ne correspond pas à l'email","Zut!");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
outputHTML5(500,response,'Y\'a eu un souci',"Oups!");
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
outputHTML5(404,response,'Y\'a rien ici',"Oups");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(80);
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "eval",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "front.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"email-validator": "^2.0.4",
|
||||
"mariadb": "^3.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"node-fetch": "^3.2.6"
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
Bonjour à tous,
|
||||
|
||||
Voici le sujet pour ce matin :
|
||||
https://forms.gle/DMdrnCPvyN9WYJMr8
|
||||
|
||||
J'essaye de rester disponible sur teams si vous avez des questions.
|
||||
|
||||
Je ferme la possiblité de soumettre des réponse à 12h30.
|
||||
Commencer donc à relire / préparer vos zips à 12h15.
|
||||
|
||||
Bon courage !
|
@ -0,0 +1,46 @@
|
||||
-- phpMyAdmin SQL Dump
|
||||
-- version 5.1.1
|
||||
-- https://www.phpmyadmin.net/
|
||||
--
|
||||
-- Hôte : 127.0.0.1
|
||||
-- Généré le : mer. 22 juin 2022 à 15:33
|
||||
-- Version du serveur : 10.4.19-MariaDB
|
||||
-- Version de PHP : 7.4.20
|
||||
|
||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
--
|
||||
-- Base de données : `eval_bdd`
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Structure de la table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users` (
|
||||
`courriel` varchar(255) NOT NULL,
|
||||
`mot_de_passe` varchar(32) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
--
|
||||
-- Déchargement des données de la table `users`
|
||||
--
|
||||
TRUNCATE `users`;
|
||||
INSERT INTO `users` (`courriel`, `mot_de_passe`) VALUES
|
||||
('test@test.Fr', MD5('be1b4'));
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
@ -0,0 +1,24 @@
|
||||
{
|
||||
"development": {
|
||||
"config_id": "development",
|
||||
"app_name": "conf-test",
|
||||
"app_desc": "An app to test environnement variables",
|
||||
"node_port": 3000,
|
||||
"json_indentation": 4,
|
||||
"database": "nodesandbox_env-dev"
|
||||
},
|
||||
"testing": {
|
||||
"config_id": "testing",
|
||||
"database": "nodesandbox_env-tst"
|
||||
},
|
||||
"staging": {
|
||||
"config_id": "staging - local",
|
||||
"node_port": 8080,
|
||||
"database": "nodesandbox_env-stage"
|
||||
},
|
||||
"production": {
|
||||
"config_id": "production",
|
||||
"node_port": 8080,
|
||||
"database": "nodesandbox_env"
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "jsonconfig",
|
||||
"version": "1.0.0",
|
||||
"description": "simple js on config test",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js production users=5",
|
||||
"dev" : "nodemon server.js development"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
Loading…
Reference in new issue