Version 0.6.0 - Ajout des utilisateurs et des paramètres V1
All checks were successful
Neutral/pipeline/head This commit looks good
All checks were successful
Neutral/pipeline/head This commit looks good
This commit is contained in:
parent
ff42630c8d
commit
43d65d093b
10
bin/auth.js
10
bin/auth.js
@ -14,7 +14,7 @@ const users = require("./users")
|
||||
module.exports.check = function(token) {
|
||||
var isApproved = false;
|
||||
var username = null
|
||||
users.getUsers().forEach((fetchUser) => {
|
||||
users.fetchUsers().forEach((fetchUser) => {
|
||||
if(fetchUser.tokens.includes(token)) {
|
||||
isApproved = true
|
||||
username = fetchUser.username
|
||||
@ -42,8 +42,8 @@ module.exports.login = function(data) {
|
||||
var username = data.username
|
||||
var password = data.password
|
||||
|
||||
if(users.getUsers().has(username)) {
|
||||
const user = users.getUsers().get(username)
|
||||
if(users.fetchUsers().has(username)) {
|
||||
const user = users.fetchUsers().get(username)
|
||||
if(password == user.getPassword()) {
|
||||
const token = user.generateToken()
|
||||
alog.log("Connexion approuvé de l'utilisateur : " + username)
|
||||
@ -67,7 +67,7 @@ module.exports.login = function(data) {
|
||||
module.exports.signout = function(token) {
|
||||
var isDone = false;
|
||||
var username = null
|
||||
users.getUsers().forEach((fetchUser) => {
|
||||
users.fetchUsers().forEach((fetchUser) => {
|
||||
if(fetchUser.tokens.includes(token)) {
|
||||
isDone = true
|
||||
username = fetchUser.username
|
||||
@ -92,7 +92,7 @@ module.exports.signout = function(token) {
|
||||
module.exports.getUserByToken = function(token) {
|
||||
var isApproved = false;
|
||||
var userGetted = null
|
||||
users.getUsers().forEach((fetchUser) => {
|
||||
users.fetchUsers().forEach((fetchUser) => {
|
||||
if(fetchUser.tokens.includes(token)) {
|
||||
userGetted = fetchUser
|
||||
}
|
||||
|
@ -35,3 +35,14 @@ module.exports.updateFile = function (file) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getSettings = function () {
|
||||
const file = this.getFile()
|
||||
return {"jenkins_token": file.JENKINS_TOKEN, "omega_token": file.OMEGA_KEY}
|
||||
}
|
||||
|
||||
module.exports.saveSettings = function (settings) {
|
||||
const file = this.getFile()
|
||||
file.JENKINS_TOKEN = settings.jenkins_token
|
||||
file.OMEGA_KEY = settings.omega_token
|
||||
this.updateFile(file)
|
||||
}
|
||||
|
@ -33,6 +33,12 @@ module.exports.getFiles = function(root) {
|
||||
|
||||
}
|
||||
|
||||
if(root == "logpath") {
|
||||
|
||||
root = __glob.LOGS
|
||||
|
||||
}
|
||||
|
||||
|
||||
if(!fs.existsSync(root)) {
|
||||
response.content = "NOT_EXIST"
|
||||
|
@ -10,6 +10,7 @@ const __glob = {
|
||||
USERS: root + path.sep + "data" + path.sep + "users.json",
|
||||
CONFIG: root + path.sep + "data" + path.sep + "config.json",
|
||||
SHARED: root + path.sep + "data" + path.sep + "shared",
|
||||
USERS_IMAGES: root + path.sep + "public" + path.sep + 'images' + path.sep + "users",
|
||||
};
|
||||
|
||||
|
||||
|
@ -3,7 +3,9 @@ const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { __glob } = require("./global-variables.js")
|
||||
const auth = require("./auth.js")
|
||||
const users = require("./users.js")
|
||||
const files = require("./files.js")
|
||||
const config = require("./config.js")
|
||||
const links = require("./links.js")
|
||||
const service = require("./services.js")
|
||||
const pipeline = require("./pipelines.js")
|
||||
@ -11,6 +13,7 @@ const plog = new LogType("Web")
|
||||
const cook = require("cookie")
|
||||
const http = require("http")
|
||||
const servermetrics = require("./server-metrics.js")
|
||||
const pm2 = require('pm2');
|
||||
|
||||
/**
|
||||
* NOTE INTERNE
|
||||
@ -25,11 +28,16 @@ const servermetrics = require("./server-metrics.js")
|
||||
|
||||
module.exports.serverIO = function(server) {
|
||||
|
||||
const io = require('socket.io')(server)
|
||||
const io = require('socket.io')(server, {
|
||||
maxHttpBufferSize: 1e8,
|
||||
pingTimeout: 60000
|
||||
})
|
||||
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
|
||||
let token = cook.parse(socket.handshake.headers.cookie).token
|
||||
const user = auth.getUserByToken(token)
|
||||
var user = auth.getUserByToken(token)
|
||||
|
||||
if(user) {
|
||||
plog.log("Connexion au panel par '" + user.username + "' avec le socket : " + socket.id)
|
||||
@ -40,7 +48,7 @@ module.exports.serverIO = function(server) {
|
||||
|
||||
// Get Users
|
||||
GetRequest("USERINFO", () => {
|
||||
|
||||
user = auth.getUserByToken(token)
|
||||
GetAnswer("USERINFO", {username: user.username, display_name: user.display_name ,picture: user.picture, permission: user.permission})
|
||||
})
|
||||
|
||||
@ -163,11 +171,57 @@ module.exports.serverIO = function(server) {
|
||||
|
||||
}
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
if(user.checkPermission("USERS")) {
|
||||
GetRequest("US_ALL", async () => {
|
||||
GetAnswer("US_ALL", await users.getAllUsers())
|
||||
})
|
||||
|
||||
PostRequest("US_ADD", async (settings) => {
|
||||
PostAnswer("US_ADD", await users.addUser(settings))
|
||||
})
|
||||
|
||||
PostRequest("US_DELETE", async (settings) => {
|
||||
PostAnswer("US_DELETE", await users.deleteUser(settings))
|
||||
})
|
||||
|
||||
PostRequest("US_EDIT", async (settings) => {
|
||||
PostAnswer("US_EDIT", await users.editUser(settings))
|
||||
})
|
||||
}
|
||||
|
||||
if(user.checkPermission("SETTINGS")) {
|
||||
PostRequest("SETTINGS_SAVE", async (settings) => {
|
||||
PostAnswer("SETTINGS_SAVE", await config.saveSettings(settings))
|
||||
})
|
||||
PostRequest("SERVER_RESTART", async () => {
|
||||
pm2.restart('Neutral')
|
||||
})
|
||||
PostRequest("SERVER_STOP", async () => {
|
||||
pm2.stop('Neutral')
|
||||
})
|
||||
GetRequest("SERVER_GET_LOGS", async () => {
|
||||
GetAnswer("SERVER_GET_LOGS", await fs.readdirSync(__glob.LOGS))
|
||||
})
|
||||
|
||||
PostRequest("SERVER_READ_LOG", async (logs) => {
|
||||
PostAnswer("SERVER_READ_LOG", await fs.readFileSync(__glob.LOGS + path.sep + logs).toString())
|
||||
})
|
||||
|
||||
GetRequest("SETTINGS_GET", async () => {
|
||||
GetAnswer("SETTINGS_GET", await config.getSettings())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
plog.log("Déconnexion du panel par '" + user.username + "' avec le socket : " + socket.id)
|
||||
})
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.log(err)
|
||||
console.log(err.message); // prints the message associated with the error
|
||||
});
|
||||
|
||||
function GetRequest(GQname, GQcallback) {
|
||||
socket.on("GET/" + GQname, () => {
|
||||
|
||||
|
154
bin/users.js
154
bin/users.js
@ -18,17 +18,25 @@ function setup() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Liste des utilisateurs
|
||||
*/
|
||||
module.exports.getUsers = function () {
|
||||
module.exports.getAllUsers = async function() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const users = await this.fetchUsers()
|
||||
|
||||
// Remove for every people the password & the tokens
|
||||
|
||||
for(var user of users) {
|
||||
user = user[1]
|
||||
user.password = null
|
||||
user.tokens = null
|
||||
}
|
||||
|
||||
|
||||
resolve(JSON.stringify(Array.from(users)))
|
||||
})
|
||||
|
||||
|
||||
return usersList
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get all users from Users Data Base
|
||||
*/
|
||||
@ -80,6 +88,8 @@ module.exports.fetchUsers = function () {
|
||||
|
||||
|
||||
ulog.step.end("fetch_user")
|
||||
|
||||
return usersList
|
||||
}
|
||||
|
||||
/**
|
||||
@ -177,6 +187,7 @@ module.exports.User = class {
|
||||
}
|
||||
userFile.push(this)
|
||||
updateFile(userFile)
|
||||
|
||||
usersList.set(this.username, this)
|
||||
}
|
||||
|
||||
@ -298,12 +309,37 @@ module.exports.User = class {
|
||||
|
||||
}
|
||||
|
||||
setNewUsername(text) {
|
||||
this.#sync()
|
||||
module.exports.deleteUser(this.username)
|
||||
this.username = text
|
||||
this.register()
|
||||
|
||||
ulog.log("Le nom d'utilisateur de l'utilisateur a été modifié : " + this.username)
|
||||
|
||||
}
|
||||
|
||||
setLastLogin(text) {
|
||||
this.#sync()
|
||||
this.lastLogin = text
|
||||
this.register()
|
||||
}
|
||||
|
||||
setPicture(file) {
|
||||
this.#sync()
|
||||
var pictureDir = __glob.USERS_IMAGES + path.sep + uuid.v4().toString() + ".png"
|
||||
fs.writeFileSync(pictureDir, file)
|
||||
|
||||
this.picture = pictureDir.replace(__glob.USERS_IMAGES + path.sep, "/images/users/")
|
||||
this.register()
|
||||
}
|
||||
|
||||
setPermissions(permissions) {
|
||||
this.#sync()
|
||||
this.permission = permissions
|
||||
this.register()
|
||||
}
|
||||
|
||||
#sync() {
|
||||
|
||||
for(var userGet of usersList.keys()) {
|
||||
@ -321,6 +357,108 @@ module.exports.User = class {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.addUser = function(settings) {
|
||||
|
||||
if(settings.username == '') {
|
||||
ulog.error("Le nom d'utilisateur est manquant")
|
||||
return "USERNAME_MISSING"
|
||||
} else if(settings.password == '') {
|
||||
ulog.error("Le mot de passe est manquant")
|
||||
return "PASSWORD_MISSING"
|
||||
} else if(settings.display_name == '') {
|
||||
ulog.error("Le nom d'affichage est manquant")
|
||||
return "DISPLAY_NAME_MISSING"
|
||||
} else if(this.getUser(settings.username)) {
|
||||
ulog.error("L'utilisateur existe déjà : " + settings.username)
|
||||
return "ALREADY_EXIST"
|
||||
} else {
|
||||
ulog.step.init("add_user", "Ajout d'un utilisateur dans la base de donnée : " + settings.username)
|
||||
var pictureDir = null
|
||||
|
||||
|
||||
if(settings.picture == null) {
|
||||
pictureDir = "/images/users/default.jpg"
|
||||
} else {
|
||||
pictureDir = __glob.USERS_IMAGES + path.sep + uuid.v4().toString() + ".png"
|
||||
fs.writeFileSync(pictureDir, settings.picture)
|
||||
|
||||
|
||||
}
|
||||
|
||||
const user = new this.User({
|
||||
username: settings.username,
|
||||
display_name: settings.display_name,
|
||||
permission: settings.permissions,
|
||||
picture: pictureDir.replace(__glob.USERS_IMAGES + path.sep, "/images/users/")
|
||||
|
||||
})
|
||||
|
||||
user.setPassword(settings.password)
|
||||
user.register()
|
||||
ulog.step.end("add_user")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.deleteUser = function(username) {
|
||||
ulog.step.init("delete_user", "Suppression d'un utilisateur dans la base de donnée : " + username)
|
||||
const user = this.getUser(username)
|
||||
user.unregister()
|
||||
ulog.step.end("delete_user")
|
||||
return "OK"
|
||||
}
|
||||
|
||||
|
||||
module.exports.getUser = function(username) {
|
||||
return usersList.get(username)
|
||||
}
|
||||
|
||||
module.exports.editUser = function(settings) {
|
||||
if(settings.username == '') {
|
||||
ulog.error("Le nom d'utilisateur est manquant")
|
||||
return "USERNAME_MISSING"
|
||||
} else if(settings.display_name == '') {
|
||||
ulog.error("Le nom d'affichage est manquant")
|
||||
return "DISPLAY_NAME_MISSING"
|
||||
} else {
|
||||
ulog.step.init("edit_user", "Modification d'un utilisateur dans la base de donnée : " + settings.username)
|
||||
const user = this.fetchUsers().get(settings.username)
|
||||
if(user) {
|
||||
console.log(settings)
|
||||
if(settings.newusername && settings.newusername != settings.username) {
|
||||
if(this.getUser(settings.newusername)) {
|
||||
ulog.error("L'utilisateur existe déjà : " + settings.username)
|
||||
return "ALREADY_EXIST"
|
||||
} else {
|
||||
|
||||
user.setNewUsername(settings.newusername)
|
||||
}
|
||||
}
|
||||
if(settings.display_name) {
|
||||
user.setDisplayName(settings.display_name)
|
||||
}
|
||||
|
||||
if(settings.password) {
|
||||
user.setPassword(settings.password)
|
||||
}
|
||||
|
||||
if(settings.picture) {
|
||||
user.setPicture(settings.picture)
|
||||
}
|
||||
if(settings.permissions) {
|
||||
|
||||
user.setPermissions(settings.permissions)
|
||||
}
|
||||
ulog.step.end("edit_user")
|
||||
return "OK"
|
||||
} else {
|
||||
ulog.step.end("edit_user")
|
||||
return "NOT_EXIST"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
|
2693
package-lock.json
generated
2693
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "neutral",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"description": "Panel d'administration de Raphix",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@ -26,9 +26,11 @@
|
||||
"ejs": "~2.6.1",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"install": "^0.13.0",
|
||||
"loguix": "1.4.1",
|
||||
"nodemon": "^3.0.1",
|
||||
"os-utils": "^0.0.14",
|
||||
"pm2": "^5.3.0",
|
||||
"serve-favicon": "^2.0.4",
|
||||
"socket.io": "^4.7.2",
|
||||
"uuid": "^9.0.1"
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@ -74,8 +74,6 @@ class TextResponse {
|
||||
this.element = getID(this.name)
|
||||
this.element.innerHTML = " "
|
||||
this.element.style.fontSize = "14px"
|
||||
this.element.style.position = "sticky"
|
||||
this.element.style.width = this.element.parentElement.offsetWidth + "px"
|
||||
this.element.style.textAlign = "center"
|
||||
}
|
||||
|
||||
@ -103,6 +101,11 @@ class TextResponse {
|
||||
this.element.style.fontSize = size
|
||||
}
|
||||
|
||||
setWidth(width) {
|
||||
this.element.style.width = width
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -110,6 +113,7 @@ const AllViews = new Map()
|
||||
var zIndex = 5
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Permet de créer une fenêtre
|
||||
* @param {object} properties Propriétés de la fenêtre
|
||||
@ -123,7 +127,7 @@ class ViewWindow {
|
||||
ViewPopupHTML = null
|
||||
ViewPopupTitle = null
|
||||
ViewItem = new ViewItem(this)
|
||||
constructor(properties) {
|
||||
constructor (properties) {
|
||||
if(!AllViews.has(properties.title)) {
|
||||
|
||||
this.ViewProperties = properties
|
||||
@ -656,7 +660,7 @@ class Service {
|
||||
const stopButton = getID(`${this.name}_stop`)
|
||||
const restartButton = getID(`${this.name}_restart`)
|
||||
const info = new TextResponse("sv-power-info")
|
||||
|
||||
info.setWidth("350px")
|
||||
|
||||
|
||||
if(this.isOnline) {
|
||||
@ -950,6 +954,221 @@ class Pipeline {
|
||||
|
||||
}
|
||||
|
||||
class User {
|
||||
constructor(properties) {
|
||||
this.username = properties.username
|
||||
this.display_name = properties.display_name
|
||||
this.picture = properties.picture
|
||||
this.permission = properties.permission
|
||||
this.View = properties.window
|
||||
this.Component = properties.component
|
||||
}
|
||||
|
||||
generateHTML() {
|
||||
return `
|
||||
<div class='user-line'>
|
||||
<div class='user-line-info'>
|
||||
<img class='taskbar-image' src='${this.picture}' alt='${this.username}'>
|
||||
<div>
|
||||
<p class='user-line-displayname'>${this.display_name}</p>
|
||||
<p class='taskbar-username'>${this.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class='user-actions'>
|
||||
<button id='${this.username}_edit' class='btn blue'><span><i class='fa fa-pencil'></i> Editer<span></button>
|
||||
${this.username == "raphix" ? "" : `<button id='${this.username}_delete' class='btn red'><span><i class='fa fa-trash'></i> Supprimer<span></button>`}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
loadScript() {
|
||||
const editButton = getID(`${this.username}_edit`)
|
||||
const deleteButton = getID(`${this.username}_delete`)
|
||||
|
||||
editButton.addEventListener("click", () => {
|
||||
this.View.createPopup({
|
||||
title: `<i class='fa fa-pencil'></i> Editer l'utilisateur : ${this.username}`,
|
||||
content: `
|
||||
<div class='us-edit'>
|
||||
<div class='user-edit-actual'>
|
||||
<img class='taskbar-image' src='${this.picture}' alt='${this.username}'>
|
||||
<p class='user-line-displayname'>${this.display_name}</p>
|
||||
<p class='taskbar-username'>${this.username}</p>
|
||||
</div>
|
||||
<div class='user-edit-line'>
|
||||
<p class='user-edit-info'>Nom d'utilisateur</p>
|
||||
<input id='${this.username}_editusername' class='field' type='text' value='${this.username}'>
|
||||
</div>
|
||||
<div class='user-edit-line'>
|
||||
<p class='user-edit-info'>Nom d'affichage</p>
|
||||
<input id='${this.username}_editdisplay' class='field' type='text' value='${this.display_name}'>
|
||||
</div>
|
||||
<div class='user-edit-line'>
|
||||
<p class='user-edit-info'>Mot de passe</p>
|
||||
<input id='${this.username}_editpassword' class='field' type='password' id='user-edit-password'>
|
||||
</div>
|
||||
<div class='user-edit-line'>
|
||||
<p class='user-edit-info'>Permissions</p>
|
||||
<div class='permissions'>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_FILES_EXPLORER">
|
||||
<label for="${this.username}_perm_FILES_EXPLORER">Fichiers</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_SERVICES">
|
||||
<label for="${this.username}_perm_SERVICES">Services</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_LINKS">
|
||||
<label for="${this.username}_perm_LINKS">Liens</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_SERVERS">
|
||||
<label for="${this.username}_perm_SERVERS">Serveurs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_PIPELINES">
|
||||
<label for="${this.username}_perm_PIPELINES">Pipelines</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_METRICS">
|
||||
<label for="${this.username}_perm_METRICS">Metrics</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_USERS">
|
||||
<label for="${this.username}_perm_USERS">Utilisateurs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="${this.username}_perm_SETTINGS">
|
||||
<label for="${this.username}_perm_SETTINGS">Paramètres</label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class='user-edit-line'>
|
||||
<p class='user-edit-info'>Photo de profil</p>
|
||||
<input type="file" id="${this.username}_addpicture" accept="image/png, image/jpeg">
|
||||
</div>
|
||||
<br>
|
||||
<span id="${this.username}_returninfo"></span>
|
||||
|
||||
</div>
|
||||
<div class="user-edit-actions">
|
||||
<button id="${this.username}_cedit" class="btn green"><span><i class='fa fa-pencil'></i> Editer</span></button>
|
||||
<button id="${this.username}_edittokens" class="btn yellow"><span><i class='fa fa-trash'></i> Tokens</span></button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
const editCButton = getID(`${this.username}_cedit`)
|
||||
const editTokensButton = getID(`${this.username}_edittokens`)
|
||||
const editUsername = getID(`${this.username}_editusername`)
|
||||
const editDisplay = getID(`${this.username}_editdisplay`)
|
||||
const editPassword = getID(`${this.username}_editpassword`)
|
||||
const editPicture = getID(`${this.username}_addpicture`)
|
||||
const returnInfo = new TextResponse(`${this.username}_returninfo`)
|
||||
|
||||
const editPermissions = new Array()
|
||||
|
||||
|
||||
editPermissions.push(getID(`${this.username}_perm_FILES_EXPLORER`))
|
||||
editPermissions.push(getID(`${this.username}_perm_SERVICES`))
|
||||
editPermissions.push(getID(`${this.username}_perm_LINKS`))
|
||||
editPermissions.push(getID(`${this.username}_perm_SERVERS`))
|
||||
editPermissions.push(getID(`${this.username}_perm_PIPELINES`))
|
||||
editPermissions.push(getID(`${this.username}_perm_METRICS`))
|
||||
editPermissions.push(getID(`${this.username}_perm_USERS`))
|
||||
editPermissions.push(getID(`${this.username}_perm_SETTINGS`))
|
||||
|
||||
|
||||
for(const actualPerm of this.permission) {
|
||||
|
||||
getID(`${this.username}_perm_${actualPerm}`).checked = true
|
||||
}
|
||||
|
||||
editUsername.addEventListener("change", () => {
|
||||
editUsername.value = editUsername.value.replace(/[^a-zA-Z0-9]/g, '')
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
editCButton.addEventListener("click", () => {
|
||||
const newPerms = new Array()
|
||||
for(const permission of editPermissions) {
|
||||
if(permission.checked) {
|
||||
newPerms.push(permission.id.replace(`${this.username}_perm_`, ""))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const request = post(`US_EDIT`, {username: this.username, newusername: editUsername.value, display_name: editDisplay.value, password: editPassword.value, permissions: newPerms, picture: editPicture.files[0]})
|
||||
request.then((answer) => {
|
||||
if(answer == "ALREADY_EXIST") {
|
||||
|
||||
returnInfo.err("L'utilisateur existe déjà !")
|
||||
|
||||
} else if(answer == "USERNAME_MISSING") {
|
||||
|
||||
returnInfo.err("Le nom d'utilisateur est manquant !")
|
||||
} else if(answer == "DISPLAY_NAME_MISSING") {
|
||||
|
||||
returnInfo.err("Le nom d'affichage est manquant !")
|
||||
} else if(answer == "PASSWORD_MISSING") {
|
||||
|
||||
returnInfo.err("Le mot de passe est manquant !")
|
||||
} else {
|
||||
|
||||
this.View.destroyPopup()
|
||||
this.View.destroy()
|
||||
this.Component.forceWindow()
|
||||
loadUserInfo()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
if(this.username != "raphix") {
|
||||
deleteButton.addEventListener("click", () => {
|
||||
this.View.createPopup({
|
||||
title: `<i class='fa fa-trash'></i> Supprimer l'utilisateur : ${this.username}`,
|
||||
content: `
|
||||
<p class='us-delete'>Voulez-vous vraiment supprimer l'utilisateur <strong>${this.username}</strong> ?</p>
|
||||
|
||||
<button id="${this.username}_deleteconfirm" class="btn red"><span><i class='fa fa-trash'></i> Supprimer</span></button>
|
||||
|
||||
`
|
||||
})
|
||||
|
||||
const deleteCButton = getID(`${this.username}_deleteconfirm`)
|
||||
|
||||
deleteCButton.addEventListener("click", () => {
|
||||
const request = post(`US_DELETE`, this.username)
|
||||
request.then((answer) => {
|
||||
if(answer == "OK") {
|
||||
this.View.destroyPopup()
|
||||
this.View.destroy()
|
||||
this.Component.forceWindow()
|
||||
} else {
|
||||
console.log(answer)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Permet de créer un item de la barre des tâches
|
||||
|
@ -14,10 +14,6 @@ explorer.createWindow(() => {
|
||||
})
|
||||
|
||||
|
||||
console.log("")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -39,12 +35,22 @@ explorer.createWindow(() => {
|
||||
})
|
||||
}
|
||||
|
||||
function goLogPath() {
|
||||
const rFiles = post("FX_GET", "logpath")
|
||||
|
||||
rFiles.then((result) => {
|
||||
loadFiles(result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
View.setContent(`
|
||||
<div class="fx-window">
|
||||
<div class='fx-bar'>
|
||||
<span id='${View.getViewTitle()}_home' class='btn-cover'><i class='fa fa-home'></i></span>
|
||||
<span id='${View.getViewTitle()}_sharebtn' class='btn-cover'><i class="fa-solid fa-share-from-square"></i></span>
|
||||
<span id='${View.getViewTitle()}_logsdir' class='btn-cover'><i class="fa-solid fa-file-lines"></i></i></span>
|
||||
<input class='fx-root-input' type="text" id='${View.getViewTitle()}_rootInput'>
|
||||
<div class='fx-bar-actions'>
|
||||
<button id='${View.getViewTitle()}_newFolder' class='btn blue'><span><i class='fa fa-folder'></i> Nouv. dossier</span></button>
|
||||
@ -63,6 +69,9 @@ explorer.createWindow(() => {
|
||||
const newFile = getID(View.getViewTitle() + '_newFile')
|
||||
const home = getID(View.getViewTitle() + '_home')
|
||||
const sharebtn = getID(View.getViewTitle() + '_sharebtn')
|
||||
const logsdir = getID(View.getViewTitle() + '_logsdir')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -127,6 +136,12 @@ explorer.createWindow(() => {
|
||||
|
||||
})
|
||||
|
||||
logsdir.addEventListener("click", () => {
|
||||
|
||||
goLogPath()
|
||||
|
||||
})
|
||||
|
||||
newFolder.addEventListener("click", () => {
|
||||
|
||||
View.createPopup({
|
||||
@ -263,6 +278,7 @@ explorer.createWindow(() => {
|
||||
}
|
||||
|
||||
const file = fileuploadInput.files[0]
|
||||
console.log(file)
|
||||
|
||||
const reqFiles = post("FX_UPLOAD", {name: file.name ,root: rootInput.value, file: file})
|
||||
|
||||
|
@ -1,15 +1,23 @@
|
||||
const infoUsername = getID("infoUsername")
|
||||
const infoUserimage = getID("infoUserimage")
|
||||
const infoDisplayname = getID("infoDisplayname")
|
||||
const views = getID("views")
|
||||
const panelBox = getID("panel-box")
|
||||
|
||||
|
||||
|
||||
// User Request
|
||||
|
||||
const REQ_user = get("USERINFO")
|
||||
loadUserInfo()
|
||||
|
||||
REQ_user.then((ANS_user) => {
|
||||
|
||||
function loadUserInfo() {
|
||||
|
||||
const infoUsername = getID("infoUsername")
|
||||
const infoUserimage = getID("infoUserimage")
|
||||
const infoDisplayname = getID("infoDisplayname")
|
||||
const views = getID("views")
|
||||
const panelBox = getID("panel-box")
|
||||
|
||||
const REQ_user = get("USERINFO")
|
||||
|
||||
REQ_user.then((ANS_user) => {
|
||||
console.log(ANS_user)
|
||||
infoUserimage.src = ANS_user.picture
|
||||
infoUsername.innerHTML = ANS_user.username
|
||||
infoDisplayname.innerHTML = ANS_user.display_name
|
||||
@ -51,5 +59,8 @@ REQ_user.then((ANS_user) => {
|
||||
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,7 @@ socket.on("connect", () => {
|
||||
console.log("Connecté au serveur par le Socket avec l'ID : " + socket.id)
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
|
||||
socket.on("disconnect", (log) => {
|
||||
window.location.href = "/"
|
||||
})
|
||||
|
||||
|
@ -52,6 +52,7 @@ function login() {
|
||||
|
||||
if(response == "AUTH_FAILED") {
|
||||
|
||||
|
||||
loginInfo.err("Le nom d'utilisateur et le mot de passe sont incorrects.")
|
||||
|
||||
} else if(response == "AUTH_SUCCESS") {
|
||||
|
0
public/javascripts/metric.js
Normal file
0
public/javascripts/metric.js
Normal file
123
public/javascripts/setting.js
Normal file
123
public/javascripts/setting.js
Normal file
@ -0,0 +1,123 @@
|
||||
settings.createWindow(async () => {
|
||||
|
||||
const View = new ViewWindow({
|
||||
title: `<i class="fa fa-cog"></i> Paramètres`,
|
||||
width: "500px",
|
||||
height: "620px"
|
||||
})
|
||||
|
||||
View.setContent(`
|
||||
<div style='overflow-y: auto; height: 550px;'>
|
||||
<div class="category">
|
||||
<p>Options d'alimentation</p>
|
||||
|
||||
<div class='st-act'>
|
||||
<button id="st-restart" class="btn yellow"><span>Redémarrer</span></button>
|
||||
<button id="st-stop" class="btn red"><span> Arrêter</span></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="category">
|
||||
<p>Configuration des Tokens</p>
|
||||
<p class='user-line-displayname'>Jenkins</p>
|
||||
<input class='field' type="text" id="jenkins_token" placeholder="Token Jenkins">
|
||||
<p class='user-line-displayname'> Omega </p>
|
||||
<input class='field' type="text" id="omega_token" placeholder="Token Omega">
|
||||
<button id="st-save" class="btn green"><span> Sauvegarder</span></button>
|
||||
</div>
|
||||
<div class="category">
|
||||
<p>Accès aux logs</p>
|
||||
<select id="all-logs" class="field">
|
||||
<option>Chargment en cours ...</option>
|
||||
</select>
|
||||
<button id="read-logs" class="btn green"><span>Lire</span></button>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
|
||||
const allLogs = document.getElementById("all-logs")
|
||||
const readLogs = document.getElementById("read-logs")
|
||||
|
||||
get("SERVER_GET_LOGS").then((logs) => {
|
||||
logs.reverse()
|
||||
allLogs.innerHTML = logs.map((log) => {
|
||||
return `<option value="${log}">${log}</option>`
|
||||
}).join("")
|
||||
})
|
||||
|
||||
readLogs.addEventListener("click", () => {
|
||||
const log = allLogs.value
|
||||
post("SERVER_READ_LOG", log).then((logContent) => {
|
||||
const logView = new ViewWindow({
|
||||
title: `<i class="fa fa-file"></i> ${log}`,
|
||||
width: "1000px",
|
||||
height: "520px"
|
||||
})
|
||||
|
||||
logContent = logContent.replaceAll("[INFO]", "<span class='blue'>[INFO]</span>")
|
||||
logContent = logContent.replaceAll("[WARN]", "<span class='yellow'>[WARN]</span>")
|
||||
logContent = logContent.replaceAll("[ERROR]", "<span class='lightred'>[ERROR]</span>")
|
||||
logContent = logContent.replaceAll("[STEP]", "<span class='green'>[STEP]</span>")
|
||||
|
||||
logContent = logContent.replaceAll("[Users]", "<span style='color:#c7b8ff; '>[Users]</span>")
|
||||
logContent = logContent.replaceAll("[Web]", "<span style='color:#fffd8a; '>[Web]</span>")
|
||||
logContent = logContent.replaceAll("[Serveur]", "<span style='color:#ff7a5c; '>[Serveur]</span>")
|
||||
logContent = logContent.replaceAll("[Authentification]", "<span style='color:#d9e6ff; '>[Authentification]</span>")
|
||||
|
||||
// Get every line of logs and add a set the style in blue when it's the date in []
|
||||
|
||||
const logLines = logContent.split("\n")
|
||||
const newLogLines = new Array()
|
||||
|
||||
logLines.forEach((line) => {
|
||||
if(line.startsWith("[") && line.includes("]")) {
|
||||
const date = line.split("]")[0] + "]"
|
||||
const content = line.replace(date, "")
|
||||
newLogLines.push(`<span style='color: #a6c2f7;'>${date}</span>${content}\n`)
|
||||
} else {
|
||||
|
||||
newLogLines.push(`${line}\n`)
|
||||
}
|
||||
})
|
||||
|
||||
logView.setContent(`
|
||||
<div style='overflow-y: auto; height: 450px;'>
|
||||
<pre style='font-size: 12px'>${newLogLines.join("")}</pre>
|
||||
</div>
|
||||
`)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
const restartButton = document.getElementById("st-restart")
|
||||
const stopButton = document.getElementById("st-stop")
|
||||
const saveButton = document.getElementById("st-save")
|
||||
const jenkinsToken = document.getElementById("jenkins_token")
|
||||
const omegaToken = document.getElementById("omega_token")
|
||||
|
||||
get("SETTINGS_GET").then((settings) => {
|
||||
jenkinsToken.value = settings.jenkins_token
|
||||
omegaToken.value = settings.omega_token
|
||||
})
|
||||
|
||||
restartButton.addEventListener("click", () => {
|
||||
post("SERVER_RESTART")
|
||||
})
|
||||
|
||||
stopButton.addEventListener("click", () => {
|
||||
post("SERVER_STOP")
|
||||
})
|
||||
|
||||
saveButton.addEventListener("click", () => {
|
||||
post("SETTINGS_SAVE", {
|
||||
jenkins_token: jenkinsToken.value,
|
||||
omega_token: omegaToken.value
|
||||
})
|
||||
|
||||
get("SETTINGS_GET").then((settings) => {
|
||||
jenkinsToken.value = settings.jenkins_token
|
||||
omegaToken.value = settings.omega_token
|
||||
})
|
||||
})
|
||||
})
|
180
public/javascripts/user.js
Normal file
180
public/javascripts/user.js
Normal file
@ -0,0 +1,180 @@
|
||||
users.createWindow(async () => {
|
||||
const usersComponent = users
|
||||
|
||||
const View = new ViewWindow({
|
||||
title: `<i class="fa fa-users"></i> Gestion des utilisateurs`,
|
||||
width: "500px",
|
||||
height: "700px"
|
||||
})
|
||||
|
||||
loadUsers()
|
||||
|
||||
function loadUsers() {
|
||||
|
||||
View.setContent(`
|
||||
<div class="users">
|
||||
<div class='us-bar'>
|
||||
<p> <strong>Utilisateurs</strong> : <span id='${View.getViewTitle()}_number'><i>Calcul en cours</i></span></p>
|
||||
<button id='${View.getViewTitle()}_add' class='btn blue'><span><i class='fa fa-add'></i> Ajouter un utilisateur</span></button>
|
||||
</div>
|
||||
<div class='us-users' id='${View.getViewTitle()}_users'>
|
||||
|
||||
</div>
|
||||
</div>`)
|
||||
|
||||
|
||||
const usersList = new Array()
|
||||
const users = get("US_ALL")
|
||||
|
||||
const usersDiv = document.getElementById(`${View.getViewTitle()}_users`)
|
||||
const usersNumber = document.getElementById(`${View.getViewTitle()}_number`)
|
||||
const addButton = document.getElementById(`${View.getViewTitle()}_add`)
|
||||
|
||||
addButton.addEventListener("click", () => {
|
||||
View.createPopup({
|
||||
title: "Ajouter un utilisateur",
|
||||
content: `
|
||||
<div class='us-add'>
|
||||
<p>Nom d'utilisateur</p>
|
||||
<input class='field' type="text" id="us-add-username" placeholder="Nom d'utilisateur">
|
||||
<p>Nom d'affichage</p>
|
||||
<input class='field' type="text" id="us-add-displayname" placeholder="Nom d'affichage">
|
||||
<p>Mot de passe</p>
|
||||
<input class='field' type="password" id="us-add-password" placeholder="Mot de passe">
|
||||
<p>Permissions</p>
|
||||
<div class='permissions'>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_FILES_EXPLORER">
|
||||
<label for="perm_FILES_EXPLORER">Fichiers</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_SERVICES">
|
||||
<label for="perm_SERVICES">Services</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_LINKS">
|
||||
<label for="perm_LINKS">Liens</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_SERVERS">
|
||||
<label for="perm_SERVERS">Serveurs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_PIPELINES">
|
||||
<label for="perm_PIPELINES">Pipelines</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_METRICS">
|
||||
<label for="perm_METRICS">Metrics</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_USERS">
|
||||
<label for="perm_USERS">Utilisateurs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="perm_SETTINGS">
|
||||
<label for="perm_SETTINGS">Paramètres</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Photo de profil</p>
|
||||
|
||||
<input type="file" id="us-add-picture" accept="image/png, image/jpeg">
|
||||
<span id='user-addreturn-info'></span>
|
||||
<button id='us-add-button' class='btn green'><span>Ajouter</span></button>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
|
||||
const addButton = document.getElementById("us-add-button")
|
||||
const username = document.getElementById("us-add-username")
|
||||
const displayname = document.getElementById("us-add-displayname")
|
||||
const password = document.getElementById("us-add-password")
|
||||
const picture = document.getElementById("us-add-picture")
|
||||
const permissions = document.getElementsByClassName("permissions")[0].children
|
||||
const returnInfo = new TextResponse("user-addreturn-info")
|
||||
|
||||
|
||||
addButton.addEventListener("click", () => {
|
||||
var permissionsList = new Array()
|
||||
for(var permission of permissions) {
|
||||
console.log(permission.children[0].checked)
|
||||
if(permission.children[0].checked) {
|
||||
permissionsList.push(permission.children[0].id.replace("perm_", ""))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
post("US_ADD", {
|
||||
username: username.value,
|
||||
display_name: displayname.value,
|
||||
password: password.value,
|
||||
picture: picture.files[0],
|
||||
permissions: permissionsList
|
||||
}).then((answer) => {
|
||||
if(answer == "ALREADY_EXIST") {
|
||||
|
||||
returnInfo.err("L'utilisateur existe déjà !")
|
||||
|
||||
} else if(answer == "USERNAME_MISSING") {
|
||||
|
||||
returnInfo.err("Le nom d'utilisateur est manquant !")
|
||||
} else if(answer == "DISPLAY_NAME_MISSING") {
|
||||
|
||||
returnInfo.err("Le nom d'affichage est manquant !")
|
||||
} else if(answer == "PASSWORD_MISSING") {
|
||||
|
||||
returnInfo.err("Le mot de passe est manquant !")
|
||||
} else {
|
||||
|
||||
View.destroyPopup()
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
users.then(users => {
|
||||
users = new Map(JSON.parse(users))
|
||||
for(var user of users) {
|
||||
|
||||
user = user[1]
|
||||
console.log(user)
|
||||
const userComponent = new User({
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
picture: user.picture,
|
||||
permission: user.permission,
|
||||
window: View,
|
||||
component: usersComponent
|
||||
})
|
||||
usersList.push(userComponent)
|
||||
}
|
||||
|
||||
|
||||
usersNumber.innerHTML = usersList.length
|
||||
|
||||
|
||||
|
||||
const usersContent = new Array()
|
||||
|
||||
for(var user of usersList) {
|
||||
usersContent.push(user.generateHTML())
|
||||
}
|
||||
|
||||
usersDiv.innerHTML = usersContent.join("")
|
||||
|
||||
for(var user of usersList) {
|
||||
user.loadScript()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
@ -213,6 +213,12 @@ a {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
.logo {
|
||||
@ -1051,3 +1057,148 @@ a {
|
||||
background-color: rgba(44, 40, 42, 0.614);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* USERS */
|
||||
|
||||
.us-bar{
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.us-bar p {
|
||||
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.user-line {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: #1b1b1bc1;
|
||||
transition: 0.1s;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.user-line-info {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-line-displayname {
|
||||
|
||||
font-size: 15px;
|
||||
margin-block: 0 !important;
|
||||
}
|
||||
|
||||
.us-users {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
overflow-y: auto ;
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.user-edit-actual {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.user-edit-line {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-edit-info {
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-edit-actions {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: space-around;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.category {
|
||||
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #1b1b1bc1;
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.permissions {
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
font-size: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.us-add {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.us-add p {
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
.us-edit {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#st-save {
|
||||
margin-top: 10px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.st-act {
|
||||
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.us-delete {
|
||||
|
||||
font-size: 12px;
|
||||
}
|
@ -28,8 +28,10 @@ router.post("/", (req, res) => {
|
||||
|
||||
if(token == "AUTH_FAILED") {
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
res.status(403).send("AUTH_FAILED")
|
||||
}, 1000)
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
|
@ -40,12 +40,15 @@
|
||||
<script src="/javascripts/basics.js"></script>
|
||||
<script src="/javascripts/middle.js"></script>
|
||||
<script src="/javascripts/io.js"></script>
|
||||
<script src="/javascripts/indexscript.js"></script>
|
||||
<script src="/javascripts/link.js"></script>
|
||||
<script src="/javascripts/service.js"></script>
|
||||
<script src="/javascripts/server.js"></script>
|
||||
<script src="/javascripts/pipeline.js"></script>
|
||||
<script src="/javascripts/filexplorer.js"></script>
|
||||
<script src="/javascripts/user.js"></script>
|
||||
<script src="/javascripts/metric.js"></script>
|
||||
<script src="/javascripts/setting.js"></script>
|
||||
<script src="/javascripts/indexscript.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user