web: ComplainIO

This commit is contained in:
agatha 2024-10-25 16:22:38 -04:00
parent f533f777a8
commit 6013199f17
30 changed files with 3717 additions and 0 deletions

View File

@ -0,0 +1,37 @@
FROM node:latest
WORKDIR /app/
# Sorry for that, carbone js library requires libreoffice to work
RUN apt update && \
apt install libxinerama1 libfontconfig1 libdbus-glib-1-2 libcairo2 libcups2 libglu1-mesa libsm6 netcat-traditional gcc -y && \
wget https://downloadarchive.documentfoundation.org/libreoffice/old/7.5.1.1/deb/x86_64/LibreOffice_7.5.1.1_Linux_x86-64_deb.tar.gz && \
tar -zxvf LibreOffice_7.5.1.1_Linux_x86-64_deb.tar.gz && \
rm LibreOffice_7.5.1.1_Linux_x86-64_deb.tar.gz && \
cd LibreOffice_7.5.1.1_Linux_x86-64_deb/DEBS && \
dpkg -i *.deb && \
mkdir /tmp/files && \
chmod 777 /tmp/files/
WORKDIR /build/
COPY getflag.c .
RUN gcc getflag.c -o getflag && \
mv getflag / && \
chmod u+s /getflag
COPY ./flag.txt /root/
WORKDIR /app/
COPY ./odt_templates/ /tmp/files/
COPY ./src/ .
RUN npm i --omit=dev && npm cache clean --force && \
rm -fr /build/ && \
chown -R node:node /app/
EXPOSE 3000
USER node
CMD ["node","index.js"]

View File

@ -0,0 +1 @@
HERO{FAKE_FLAG}

View File

@ -0,0 +1,21 @@
#include <stdio.h>
#include <stdlib.h>
int main() {
const char *file_path = "/root/flag.txt";
FILE *file = fopen(file_path, "r");
if (file == NULL) {
perror("An error occured, ping worty on discord");
return EXIT_FAILURE;
}
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,15 @@
const db = require("../models");
const Complains = db.Complains;
const utils = require("../utils");
exports.getAll = async (req, res) => {
let decoded = utils.verify_jwt(req);
if(decoded.username) {
res.status(200).send(await Complains.findAll());
} else {
res.status(401).send({
data:
"Invalid token."
});
}
}

View File

@ -0,0 +1,118 @@
const db = require("../models");
const utils = require("../utils");
const fs = require("fs");
const carbone = require('carbone');
const Files = db.Files;
const Users = db.Users;
exports.createTemplate = async (req, res) => {
let decoded = utils.verify_jwt(req);
if(decoded.username) {
if(req.body.uuid && req.body.id && typeof(req.body.uuid) === "string" && typeof(req.body.id) == "number") {
const file = await Files.findOne({where: {uuid: req.body.uuid}});
let current_user = await Users.findOne({where: {username: decoded.username}});
if(current_user.id != req.body.id) {
res.status(401).send({
data:
"Unauthorized."
});
} else {
if(req.body.firstname !== current_user.firstname || req.body.lastname !== current_user.lastname) {
await utils.update_user(req.body, decoded);
current_user = await Users.findOne({where: {username: decoded.username}});
}
if(file.uuid) {
var data = {
firstname: current_user.firstname,
lastname: current_user.lastname
};
carbone.render(file.path, data, function(err, result){
if (err) {
res.status(500).send({
data:
"Internal Server Error."
});
} else {
res.status(200).send({
data:
result.toString('base64')
});
}
});
} else {
res.status(404).send({
data:
"File not found."
});
}
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
} else {
res.status(401).send({
data:
"Invalid token."
});
}
}
exports.getProfilePicture = async (req, res) => {
if(req.query.token !== undefined && typeof(req.query.token) === "string") {
let decoded = utils.verify_jwt(req, req.query.token);
if(decoded.username) {
if(req.params.uuid && typeof(req.params.uuid) === "string") {
const file = await Files.findOne({where: {uuid: req.params.uuid}});
if(file && file.uuid) {
const current_user = await Users.findOne({where: {username: decoded.username}});
if(file.user_id === current_user.id) {
try {
var s = fs.createReadStream(file.path);
s.on('open', function() {
res.set('Content-Type', 'image/png');
s.pipe(res);
});
s.on('error', function() {
res.set('Content-Type', 'text/plain');
res.status(500).end('Internal Server Error');
})
} catch(err) {
res.status(500).send({
data:
"Internal Server Error."
});
}
} else {
res.status(401).send({
data:
"Unauthorized."
});
}
} else {
res.status(404).send({
data:
"File not found."
});
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
} else {
res.status(401).send({
data:
"Invalid token."
});
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
}

View File

@ -0,0 +1,192 @@
const db = require("../models");
const { createHash } = require('crypto');
const utils = require("../utils");
const { v4: uuidv4 } = require('uuid');
const Users = db.Users;
const Files = db.Files;
const UPLOADS_DIR = "/tmp/files/"
const fs = require("fs");
exports.login = async (req, res) => {
if(req.body.username !== undefined && req.body.password !== undefined &&
typeof(req.body.username) === "string" && typeof(req.body.password) === "string") {
const user = await Users.findOne({where: {username: req.body.username}});
if(user === null) {
res.status(400).send({
data:
"The username or password is incorrect."
});
} else {
if(createHash('sha256').update(req.body.password).digest('hex') === user.password) {
res.status(200).send({
data:
"User connected",
token:
utils.sign_jwt({username: req.body.username})
})
} else {
res.status(400).send({
data:
"The username or password is incorrect."
});
}
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
}
exports.register = async (req, res) => {
if(req.body.username !== undefined && req.body.password !== undefined && req.body.firstname !== undefined && req.body.lastname !== undefined &&
typeof(req.body.username) === "string" && typeof(req.body.password) === "string" && typeof(req.body.firstname) === "string" && typeof(req.body.lastname) === "string") {
const user = await Users.findOne({where: {username: req.body.username}});
if(user === null) {
const created_user = await Users.create({
username: req.body.username,
password: createHash('sha256').update(req.body.password).digest('hex'),
firstname: req.body.firstname,
lastname: req.body.lastname
});
if(created_user.id === null) {
res.status(500).send({
data:
"Internal Server Error."
});
} else {
res.status(200).send({
data:
"User created."
});
}
} else {
res.status(400).send({
data:
"User already exists."
});
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
}
exports.me = async (req, res) => {
let decoded = utils.verify_jwt(req);
if(decoded.username) {
const user = await Users.findOne({where: {username: decoded.username}, attributes: ['id', 'username', 'firstname', 'lastname']});
if(user) {
let infos = {"id": user.id, "lastname": user.lastname, "firstname": user.firstname, "username": user.username, "pp": null};
const potential_picture = await Files.findOne({where: {user_id: user.id}, attributes: ['uuid']});
if(potential_picture) {
infos["pp"] = potential_picture.uuid;
}
res.status(200).send(infos);
} else {
res.status(500).send({
data:
"Internal Server Error."
});
}
} else {
res.status(401).send({
data:
"Then token is invalid or was not provided."
});
}
}
exports.profile = async (req, res) => {
if(req.body.id !== undefined && req.body.username !== undefined && req.body.firstname !== undefined && req.body.lastname !== undefined &&
typeof(req.body.id) === "number" && typeof(req.body.username) === "string" && typeof(req.body.firstname) === "string" && typeof(req.body.lastname) === "string") {
let user_database_content = await Users.findByPk(req.body.id);
if(user_database_content === null) {
res.status(404).send({
data: "User not found."
});
} else {
let decoded = utils.verify_jwt(req);
if(decoded.username) {
let current_user = await Users.findOne({where: {username: decoded.username}});
if(current_user.id != req.body.id) {
res.status(401).send({
data:
"Unauthorized."
})
} else {
let result = await utils.update_user(req.body, decoded);
res.status(result[0]).send({
data:
result[1]
});
}
} else {
res.status(401).send({
data:
"Invalid token."
});
}
}
} else {
res.status(400).send({
data:
"Bad request."
})
}
}
exports.upload = async (req, res) => {
if(req.body.token !== undefined && typeof(req.body.token) === "string") {
let decoded = utils.verify_jwt(req, req.body.token);
if(decoded.username) {
if(req.files.picture !== undefined) {
let profile_picture_image = req.files.picture;
let new_uuid = uuidv4();
let filename = UPLOADS_DIR + new_uuid+'.png';
profile_picture_image.mv(filename);
const current_user = await Users.findOne({where: {username: decoded.username}});
const has_current_pp = await Files.findOne({where: {user_id: current_user.id}});
if(has_current_pp) {
try {
fs.unlinkSync(has_current_pp.path);
await has_current_pp.destroy();
} catch(err) {
res.status(500).send({
data:
"Internal Server Error."
});
return;
}
}
const created_file = await Files.create({uuid: new_uuid, path: filename, user_id: current_user.id});
if(created_file.id === null) {
res.status(500).send({
data:
"Internal Server Error."
});
} else {
res.redirect("/profile");
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
} else {
res.status(401).send({
data:
"Invalid token."
});
}
} else {
res.status(400).send({
data:
"Bad request."
});
}
}

View File

@ -0,0 +1,28 @@
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
const path = require("path");
const bodyParser = require("body-parser");
app.use(express.json());
app.use(fileUpload());
app.use(express.static(path.join(__dirname,'public')));
app.disable('x-powered-by');
require("./routes/complain.routes")(app);
const PORT = 3000;
app.get('/', (_, res) => {
res.sendFile(path.join(__dirname,'/views/index.html'));
});
app.get('/profile', (_, res) => {
res.sendFile(path.join(__dirname,'/views/profile.html'));
})
app.get('/complain', (_, res) => {
res.sendFile(path.join(__dirname,'/views/complain.html'));
})
app.listen(PORT, () => {
console.log(`Sales management application is running on port ${PORT}.`);
});

View File

@ -0,0 +1,21 @@
module.exports = (sequelize, DataTypes) => {
const Complains = sequelize.define("complains", {
id: {
type: DataTypes.STRING,
primaryKey: true
},
reason: {
type: DataTypes.STRING
},
file_id: {
type: DataTypes.STRING
}
},
{
timestamps: false,
updatedAt: false,
createdAt: false
});
return Complains;
};

View File

@ -0,0 +1,24 @@
module.exports = (sequelize, DataTypes) => {
const Files = sequelize.define("files", {
uuid: {
type: DataTypes.STRING,
primaryKey: true
},
path: {
type: DataTypes.STRING
},
user_id: {
type: DataTypes.INTEGER,
allowNull: true,
references: 'users',
referencesKey: 'id'
}
},
{
timestamps: false,
updatedAt: false,
createdAt: false
});
return Files;
};

View File

@ -0,0 +1,16 @@
const Sequelize = require("sequelize");
const sequelize = new Sequelize({
dialect: "sqlite",
storage: process.env.DB_PATH,
operatorsAliases: false
});
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.Files = require("./files.model.js")(sequelize, Sequelize);
db.Users = require("./users.model.js")(sequelize, Sequelize);
db.Complains = require("./complains.model.js")(sequelize, Sequelize);
module.exports = db;

View File

@ -0,0 +1,28 @@
module.exports = (sequelize, DataTypes) => {
const Users = sequelize.define("users", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: DataTypes.STRING
},
password: {
type: DataTypes.STRING
},
firstname: {
type: DataTypes.STRING
},
lastname: {
type: DataTypes.STRING
}
},
{
timestamps: false,
updatedAt: false,
createdAt: false
});
return Users;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
{
"name": "carboneio",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"carbone": "3.5.5",
"express": "^4.19.2",
"express-fileupload": "^1.5.1",
"jsonwebtoken": "^9.0.2",
"nodejs-base64": "^2.0.0",
"sequelize": "^6.37.3",
"sqlite3": "^5.1.7"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,101 @@
let uuid;
function load_infos(){
$.ajax({
url: "/api/complains",
type: "GET",
headers: {
"Authorization": "Bearer "+localStorage.getItem("token")
},
success: function(msg){
let html = "";
for(var i=0; i<msg.length; i++) {
html += '<div class="col-md-4 mb-4"><div class="card h-100"><div class="card-body"><h5 class="card-title">Complain template n°'+msg[i]["id"]+'</h5><p class="card-text">'+msg[i]["reason"]+'</p><a href="#" class="btn btn-primary" onclick="template(\''+msg[i]["file_id"]+'\')">Create template</a></div></div></div>'
}
$("#complains").append(html);
},
error: function(msg){
$("#error_content").text("Unexcepted error, please try log in again !");
$("#unexceptErrorModal").modal('show');
setTimeout(function(){
localStorage.removeItem("token");
window.location.href = "/#popupError=1";
},3000)
}
})
}
$( document ).ready(async function() {
if(!localStorage.getItem("token")) {
window.location.href = "/#popupLogin=1";
}
load_infos();
});
function template(uuid) {
$.ajax({
url: "/api/me",
type: "GET",
headers: {
"Authorization": "Bearer "+localStorage.getItem("token")
},
success: function(msg){
$("#firstname").val(msg["firstname"]);
$("#lastname").val(msg["lastname"]);
$("#user_id").val(msg["id"]);
},
error: function(msg){
$("#error_content").text("Unexcepted error, please try log in again !")
$("#unexceptErrorModal").modal('show');
setTimeout(function(){
localStorage.removeItem("token");
window.location.href = "/#popupError=1";
},3000)
}
})
$("#uuid").val(uuid);
$("#token").val(localStorage.getItem("token"));
$("#changeModal").modal('show');
}
function create_template(){
$("#changeModal").modal("hide");
$("#msgTemplateTxt").remove()
if($("#firstname").val().length == 0 || $("#lastname").val().length == 0) {
$("#msgTemplate").append("<p id='msgTemplateTxt' style='color: red;'>You must fill in all fields.</p>");
return;
}
var data = {"firstname": $("#firstname").val(), "lastname": $("#lastname").val(), "uuid": $("#uuid").val(), "id": parseInt($("#user_id").val())}
$.ajax({
url: "/api/create_template",
type: "POST",
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
headers: {
"Authorization": "Bearer "+localStorage.getItem("token")
},
success: function(msg){
var a = document.createElement("a");
a.href= "data:application/octet-stream;base64,"+msg["data"];
a.download = "complain.odt";
a.click();
},
error: function(msg){
$("#error_content").text("Unexcepted error, please try log in again !")
$("#unexceptErrorModal").modal('show');
setTimeout(function(){
localStorage.removeItem("token");
window.location.href = "/#popupError=1";
},3000)
}
})
$("#firstname").val("");
$("#lastname").val("");
}
$("#changeModal").on('hidden.bs.modal', function() {
$("#firstname").val("");
$("#lastname").val("");
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,101 @@
$( document ).ready(async function() {
if(localStorage.getItem("token")) {
$("#navbarNavAuth").css("visibility","visible");
} else {
$("#navbarNavUnauth").css("visibility","visible");
}
if(window.location.hash) {
hash_content = window.location.hash.split("#")[1];
data = hash_content.split("=");
if(data.length === 2) {
if(data[0] === "popupLogin" && data[1] === "1") {
$("#msgLogin").append("<p id='msgLoginTxt' style='color: red;'>An error occured, please try to log in again.</p>");
$("#loginModal").modal('show');
} else if(data[0] === "popupLogin" && data[1] == "2") {
$("#msgLogin").append("<p id='msgLoginTxt' style='color: red;'>Please log in before accessing this page.</p>");
$("#loginModal").modal('show');
}
}
history.replaceState(null, null, ' ');
}
});
function register() {
$("#msgRegisterTxt").remove();
if($("#usernameRegister").val().length == 0 || $("#passwordRegister").val().length == 0 || $("#firstname").val().length == 0 || $("#lastname").val().length == 0) {
$("#msgRegister").append("<p id='msgRegisterTxt' style='color: red;'>You must fill in all fields.</p>");
return;
}
var data = {"username": $("#usernameRegister").val(), "password": $("#passwordRegister").val(), "firstname": $("#firstname").val(), "lastname": $("#lastname").val()};
$.ajax({
url: "/api/register",
type: "POST",
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
success: function(msg) {
$("#usernameRegister").val('');
$("#passwordRegister").val('');
$("#successRegisterModal").modal('show');
},
error: function(msg) {
let error = msg.responseJSON;
if(error != undefined) {
if(msg.status == 400) {
$("#msgRegister").append("<p id='msgRegisterTxt' style='color: red;'>"+error["data"]+"</p>");
} else {
$("#msgRegister").append("<p id='msgRegisterTxt' style='color: red;'>Internal Server Error... 😔</p>");
}
} else {
$("#msgRegister").append("<p id='msgRegisterTxt' style='color: red;'>Internal Server Error... 😔</p>");
}
}
})
}
function login() {
$("#msgLoginTxt").remove();
if($("#usernameLogin").val().length == 0 || $("#passwordLogin").val().length == 0) {
$("#msgLogin").append("<p id='msgLoginTxt' style='color: red;'>You must fill in all fields.</p>");
return;
}
var data = {"username": $("#usernameLogin").val(), "password": $("#passwordLogin").val()};
$.ajax({
url: "/api/login",
type: "POST",
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
success: function(msg) {
localStorage.setItem("token", msg["token"]);
window.location.href = "/";
},
error: function(msg) {
let error = msg.responseJSON;
if(error != undefined) {
if(msg.status == 400) {
$("#msgLogin").append("<p id='msgLoginTxt' style='color: red;'>"+error["data"]+"</p>");
} else {
$("#msgLogin").append("<p id='msgLoginTxt' style='color: red;'>Internal Server Error... 😔</p>");
}
} else {
$("#msgLogin").append("<p id='msgLoginTxt' style='color: red;'>Internal Server Error... 😔</p>");
}
}
})
}
$("#loginModal").on('hidden.bs.modal', function() {
$("#usernameLogin").val("");
$("#passwordLogin").val("");
$("#msgLoginTxt").remove();
});
$("#registerModal").on('hidden.bs.modal', function() {
$("#firstname").val("");
$("#lastname").val("");
$("#usernameRegister").val("");
$("#passwordRegister").val("");
$("#msgRegisterTxt").remove();
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,94 @@
function load_infos(){
$.ajax({
cache: false,
url: "/api/me",
type: "GET",
headers: {
"Authorization": "Bearer "+localStorage.getItem("token")
},
success: function(msg){
$("#firstname").val(msg["firstname"]);
$("#lastname").val(msg["lastname"]);
$("#username").val(msg["username"]);
$("#user_id").val(msg["id"]);
$("#token").val(localStorage.getItem("token"));
if(msg["pp"] !== null) {
$("#pp").attr("src","/api/picture/"+msg["pp"]+"?token="+localStorage.getItem("token"));
$("#pp").width(200);
$("#pp").height(200);
}
},
error: function(msg){
$("#error_content").text("Unexcepted error, please try log in again !")
$("#unexceptErrorModal").modal('show');
setTimeout(function(){
localStorage.removeItem("token");
window.location.href = "/#popupError=1";
},3000)
}
})
}
$( document ).ready(async function() {
if(!localStorage.getItem("token")) {
window.location.href = "/#popupLogin=1";
}
load_infos();
$("#picture").on("change", function(){
document.forms["form_picture"].submit();
});
});
function save(){
$("#errorTxt").val('');
if($("#username").val().length == 0 || $("#firstname").val().length == 0 || $("#lastname").val().length == 0 || $("#user_id").val().length == 0) {
$("#errorModal").modal('show');
$("#errorTxt").text("You must fill in all fields.");
return;
}
var data = {"username": $("#username").val(), "firstname": $("#firstname").val(), "lastname": $("#lastname").val(), "id": parseInt($("#user_id").val())};
$.ajax({
url: "/api/profile",
type: "PATCH",
data: JSON.stringify(data),
contentType: 'application/json',
headers:{
"Authorization": "Bearer "+localStorage.getItem("token")
},
dataType: 'json',
success: function(msg) {
$("#successModal").modal('show');
load_infos();
},
error: function(msg) {
let error = msg.responseJSON;
if(error != undefined) {
if(msg.status == 400){
$("#errorModal").modal('show');
$("#errorTxt").text(error["data"]);
} else if(msg.status == 404) {
$("#errorModal").modal('show');
$("#errorTxt").text("It seems that your current user does not exist in database, please log in again.");
setTimeout(function(){
localStorage.removeItem("token");
window.location.href = "/#popupError=1";
},3000)
} else if(msg.status == 401) {
$("#errorModal").modal('show');
$("#errorTxt").text("Nasty things are going on, please log in again.");
setTimeout(function(){
localStorage.removeItem("token");
window.location.href = "/#popupError=1";
},3000)
} else {
$("#errorModal").modal('show');
$("#errorTxt").text("Internal Server Error... 😔");
}
} else {
$("#errorModal").modal('show');
$("#errorTxt").text("Internal Server Error... 😔");
}
}
})
}

View File

@ -0,0 +1,20 @@
module.exports = app => {
const usersController = require("../controllers/users.controller.js");
const filesController = require("../controllers/files.controller.js");
const complainsController = require("../controllers/complains.controller.js")
var router = require("express").Router();
router.post("/login", usersController.login);
router.post("/register", usersController.register);
router.patch("/profile", usersController.profile);
router.post("/upload", usersController.upload);
router.get("/me", usersController.me);
router.post("/create_template", filesController.createTemplate);
router.get("/picture/:uuid", filesController.getProfilePicture);
router.get('/complains', complainsController.getAll);
app.use("/api", router);
};

View File

@ -0,0 +1,66 @@
let jwt = require('jsonwebtoken');
const secret_key = process.env.SECRET_KEY || "hero";
const db = require("../models");
const Users = db.Users;
const FORBIDDEN_MODIFIED = ["id","username","password"];
const all_fields = FORBIDDEN_MODIFIED.concat(["firstname","lastname"]);
const merge = (obj1, obj2) => {
for (let key of Object.keys(obj2)) {
const val = obj2[key];
if(FORBIDDEN_MODIFIED.includes(key)) {
continue
}
if (typeof obj1[key] !== "undefined" && typeof val === "object") {
obj1[key] = merge(obj1[key], val);
} else {
obj1[key] = val;
}
}
return obj1;
};
exports.verify_jwt = (req, user_token=null) => {
let decoded = {};
let token = "";
if(req.headers.authorization !== undefined) {
let parts = req.headers.authorization.split("Bearer ")
if(parts.length == 2) {
token = parts[1];
}
}
if(user_token !== null) {
token = user_token;
}
try {
decoded = jwt.verify(token, secret_key);
} catch(err) {}
return decoded;
}
exports.sign_jwt = (data) => {
return jwt.sign(data, secret_key)
}
exports.update_user = async (data, _) => {
const user_id = data.id;
let current_user = await Users.findByPk(user_id);
if(!current_user) {
return [404, "User not found."];
}
let user_database_content = {};
let incoming_user_updates = data;
for(var i=0; i<all_fields.length; i++) {
user_database_content[all_fields[i]] = current_user[all_fields[i]];
}
merge(user_database_content, incoming_user_updates);
const num = await Users.update(user_database_content, {
where: {id: user_id}
})
if(num == 1) {
return [200,"User updated."];
} else {
return [500,"Internal server error."];
}
}

View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComplainIO - A platform for generating complaint templates</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">ComplainIO</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAuth">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/">Back to home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Create a complaint report</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile">My profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick='localStorage.removeItem("token"); window.location.href="/";'>Logout</a>
</li>
</ul>
</div>
</nav>
<div class="container mt-5">
<div class="row">
<div class="col text-center">
<h1>ComplainIO templates</h1>
</div>
</div>
<div class="row mt-4" id="complains">
</div>
<div class="modal fade" id="unexceptErrorModal" tabindex="-1" role="dialog" aria-labelledby="unexceptErrorModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unexceptErrorModalLabel">UhOh.. ❌</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p style="color: red;" id="error_content"></p>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="changeModal" tabindex="-1" role="dialog" aria-labelledby="changeModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="changeModalLabel">Do you want to change your firstname and lastname that will be displayed in the complain document ? (Keep theses values if not)</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick='$("#firstname").val("");$("#lastname").val("");'>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="msgTemplate"></div>
<form>
<div class="form-group">
<label for="firstname">Firstname</label>
<input type="text" class="form-control" id="firstname" name="firstname" required>
</div>
<div class="form-group">
<label for="lastname">Lastname</label>
<input type="text" class="form-control" id="lastname" name="lastname" required>
</div>
<input type="hidden" id="uuid" name="uuid" required>
<input type="hidden" id="token" name="token" required>
<input type="hidden" id="user_id" name="id" required>
<button type="button" class="btn btn-primary btn-block" onclick='create_template()'>Create complain</button>
</form>
</div>
</div>
</div>
</div>
<script src="/js/jquery-3.5.1.min.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/complain.js"></script>
</body>
</html>

View File

@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComplainIO - A platform for generating complaint templates</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="#">ComplainIO</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavUnauth" style="visibility: hidden;">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="#" type="button" data-toggle="modal" data-target="#loginModal">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" type="button" data-toggle="modal" data-target="#registerModal">Register</a>
</li>
</ul>
</div>
<div class="collapse navbar-collapse" id="navbarNavAuth" style="visibility: hidden;">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/complain">Create a complaint report</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile">My profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick='localStorage.removeItem("token"); window.location.href="/";'>Logout</a>
</li>
</ul>
</div>
<div class="modal fade" id="loginModal" tabindex="-1" role="dialog" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginModalLabel">Login</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick='$("#usernameLogin").val("");$("#passwordLogin").val("");$("#msgLoginTxt").remove();'>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="msgLogin"></div>
<form>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="usernameLogin" placeholder="Enter your username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="passwordLogin" placeholder="***********" required>
</div>
<button type="button" class="btn btn-primary btn-block" onclick="login()">Log in</button>
<p>No account ? <a href="#" onclick='$("#loginModal").modal("hide");$("#registerModal").modal("show");'>Create one</a> !</p>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="registerModal" tabindex="-1" role="dialog" aria-labelledby="registerModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="registerModalLabel">Register</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick='$("#emailRegister").val("");$("#usernameRegister").val("");$("#passwordRegister").val("");$("#firstname").val("");$("#lastname").val("");$("#msgRegisterTxt").remove();'>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="msgRegister"></div>
<form>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="usernameRegister" placeholder="Enter your username" required>
</div>
<div class="form-group">
<label for="firstname">Firstname</label>
<input type="text" class="form-control" id="firstname" placeholder="Enter your firstname" required>
</div>
<div class="form-group">
<label for="lastname">Lastname</label>
<input type="text" class="form-control" id="lastname" placeholder="Enter your lastname" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="passwordRegister" placeholder="***********" required>
</div>
<button type="button" class="btn btn-primary btn-block" onclick="register()">Register</button>
<p>Already have an account ? <a href="#" onclick='$("#registerModal").modal("hide");$("#loginModal").modal("show");'>Log in</a> !</p>
</form>
</div>
</div>
</div>
<div class="modal fade" id="successRegisterModal" tabindex="-1" role="dialog" aria-labelledby="successRegisterModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="successRegisterModal">Youhou ! 🎉</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p style="color: green;">Your account was created ! You can now log in to complain !</p>
</div>
</div>
</div>
</nav>
<!-- Contenu principal -->
<div class="container mt-5">
<div class="row">
<div class="col text-center">
<h1>Welcome on ComplainIO</h1>
<p>Need to complain for a legitimate reason? Or just for the fun of it, because you're French? You've come to the right place !</p>
</div>
</div>
</div>
</div>
<!-- Lien vers le JS de Bootstrap et ses dépendances -->
<script src="/js/jquery-3.5.1.min.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComplainIO - A platform for generating complaint templates</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/">ComplainIO</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAuth">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="/">Back to home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/complain">Create a complaint report</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">My profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick='localStorage.removeItem("token"); window.location.href="/";'>Logout</a>
</li>
</ul>
</div>
</nav>
<div class="container mt-5">
<div class="row">
<div class="col text-center">
<h1>Your ComplainIO profile</h1>
</div>
</div>
<div class="card profile-card">
<div class="card-body">
<img src="" alt="No profile picture, click here to upload one." class="profile-img" id="pp" style="border-radius: 50%;" onclick="$('input[type=file]').trigger('click');">
<h3 class="card-title"></h3>
<form>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" value="" required readonly>
</div>
<div class="form-group">
<label for="firstname">Firstname</label>
<input type="text" class="form-control" id="firstname" value="" required>
</div>
<div class="form-group">
<label for="lastname">Lastname</label>
<input type="text" class="form-control" id="lastname" value="" required>
</div>
<input type="hidden" id="user_id" required>
<button type="button" class="btn btn-primary" onclick="save()">Save</button>
</form>
</div>
</div>
<div class="modal fade" id="unexceptErrorModal" tabindex="-1" role="dialog" aria-labelledby="unexceptErrorModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unexceptErrorModalLabel">UhOh.. ❌</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p style="color: red;" id="error_content"></p>
</div>
</div>
</div>
</div>
<div class="modal fade" id="errorModal" tabindex="-1" role="dialog" aria-labelledby="errorModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="errorModalLabel">UhOh.. ❌</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p id="errorTxt" style="color: red;"></p>
</div>
</div>
</div>
</div>
<div class="modal fade" id="successModal" tabindex="-1" role="dialog" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="successModalLabel">We've taken the changes into account! 😊</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p style="color: green;">Everything is ready for you to complain !</p>
</div>
</div>
</div>
</div>
</div>
<script src="/js/jquery-3.5.1.min.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/profile.js"></script>
<!-- Div use for file upload -->
<div style="visibility: hidden;">
<form method="POST" enctype="multipart/form-data" id="form_picture" action="/api/upload">
<input type="file" id="picture" name="picture"/>
<input type="text" id="token" name="token"/>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
---
version: "2.2"
services:
complainio:
build:
context: ./challenge/
restart: always
ports:
- "3000:3000"
environment:
- DB_PATH=/app/database.sqlite