Skip to content
Snippets Groups Projects
Commit 8b700fe7 authored by justin.foltz's avatar justin.foltz
Browse files

Add project

parent 2699735e
Branches
No related tags found
No related merge requests found
Showing
with 5566 additions and 0 deletions
File added
./DB/Dockerfile
node_modules
.git
.gitignore
Dockerfile
MONGODB_URL=mongodb://user:test@db:27017/admin
JWT_SECRET=qsQlG4an6wxIjsn
# Project
node_modules/*
let port = 27017
let mongoose = require('mongoose');
let options = { server: { socketOptions: { keepAlive: 300000, connectTimeoutMS: 30000 } },
replset: { socketOptions: { keepAlive: 300000, connectTimeoutMS : 30000 } } };
mongoose.Promise = global.Promise;
mongoose.connect(process.env.MONGODB_URL, {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
});
var db = mongoose.connection;
db.on('error', console.error.bind(console, "[DB] - Error with the connexion"));
db.once('open', function (){
console.log(console.log("[DB] - Connected"));
});
// Schema definition
let profilSchema = mongoose.Schema({
username: {
type: String,
required: true,
},
pass: {
type: String,
required: true,
select: false,
},
name: String,
age: String,
preferences: [],
token: {
type: String,
select: false,
}
});
let Profil = mongoose.model('Profil', profilSchema);
// Check if the username is already in the DB
containUsername = async (username) => {
const users = await Profil.find({ "username" : username });
if(users.length > 0) {
return true;
}
return false;
}
// Check if the ID is already in the DB
containId = async (id) => {
const users = await Profil.find({ "_id" : id });
if(users.length > 0) {
return true;
}
return false;
}
// Register user
registration = async (username, pass, name) => {
const containsUsername = await containUsername(username);
if(containsUsername){
console.log("[DB] - ID already registered");
return false;
}
let newProfil = new Profil();
newProfil.username = username;
newProfil.pass = pass;
newProfil.name = name;
newProfil.save(function(err){
if(err){
console.log("[DB] - Error saving the profil")
return false;
}
});
console.log("[DB] - ID registered");
return true;
}
// Check if the password matches the one provided
authentication = async (username, pass) => {
const containsUsername = await containUsername(username);
if(containsUsername){
const user = await Profil.findOne({ "username" : username }).select('+pass');
if(user.toObject().pass === pass){
return true;
}
}
return false;
}
// Retrieve profil from the DB
getProfilByUsername = async (username) => {
const containsUsername = await containUsername(username);
if(containsUsername){
const user = await Profil.findOne({ "username" : username });
return user.toObject();
}
return undefined;
}
// Retrieve profil from the DB
getProfilById = async (id) => {
const containsId = await containId(id);
if(containsId){
const user = await Profil.findOne({ "_id" : id });
return user.toObject();
}
return undefined;
}
// Search for a profil based on the id or name
searchProfil = async (name) => {
let regex = new RegExp("^" + name);
const users = await Profil.find({ $or:[{"username" : regex}, {"name" : regex}] });
if(users.length > 0){
return users;
}
return undefined;
}
// Update the token field
saveToken = async (username, token) => {
const containsUsername = await containUsername(username);
if(containsUsername){
const res = await Profil.updateOne({ "username" : username }, { "token": token });
if(res.ok == 1){
return true;
}
}
return false;
}
// Delete the current token field
deleteToken = async (id) => {
const containsId= await containId(id);
if(containsId){
const res = await Profil.updateOne({ "_id" : id }, { "token": undefined });
if(res.ok == 1){
return true;
}
}
return false;
}
// Make sure the token is correct in the token field
checkToken = async (id, token) => {
const containsId = await containId(id);
if(containsId){
const user = await Profil.findOne({ "_id" : id }).select('+token');
if(user.toObject().token === token)
return true;
}
return false;
}
// Check if event in already in profil
containEvent = async (username, eventID) => {
const containsUsername = await containUsername(username);
if(containsUsername){
const res = await Profil.findOne({ "username" : username }).find({ "preferences.eventfulID" : eventID });
return res.length > 0;
}
return false;
}
// Push event in a profil
pushEventInProfil = async (username, event) => {
const containsEvent = await containEvent(username, event.eventfulID);
if(containsEvent){
return false;
}
const res = await Profil.updateOne({ "username" : username }, { $push: { "preferences": event } });
return res.ok == 1
}
// Remove event in a profil
removeEventInProfil = async (username, eventID) => {
const containsUsername = await containUsername(username);
if(containsUsername){
const res = await Profil.update({ "username" : username }, { $pull : { "preferences" : { "eventfulID" : eventID } } });
return res.ok == 1;
}
return false;
}
// Get the different events in a profil
getEventInProfil = async (username) => {
const containsUsername = await containUsername(username);
if(containsUsername){
const events = await Profil.find({ "username" : username }, "preferences");
return events[0].preferences;
}
return undefined;
}
// Modify username of a user
modifyUsername = async (id, newUsername) => {
const containsId = await containId(id);
if(containsId){
const res = await Profil.updateOne({ "_id" : id }, { "username": newUsername });
if(res.ok == 1){
return true;
}
}
return false;
}
// Modify name of a user
modifyName = async (id, newName) => {
const containsId = await containId(id);
if(containsId){
const res = await Profil.updateOne({ "_id" : id }, { "name": newName });
if(res.ok == 1){
return true;
}
}
return false;
}
// Modify pass of a user
modifyPass = async (id, newPass) => {
const containsId = await containId(id);
if(containsId){
const res = await Profil.updateOne({ "_id" : id }, { "pass": newPass });
if(res.ok == 1){
return true;
}
}
return false;
}
exports.registration = function(username, pass, name){
return registration(username, pass, name);
}
exports.containId = function(id){
return containId(id);
}
exports.containUsername = function(username){
return containUsername(username);
}
exports.login = function(username, pass){
return authentication(username, pass);
}
exports.getProfilByUsername = function(username){
return getProfilByUsername(username);
}
exports.getProfilById = function(id){
return getProfilById(id);
}
exports.saveToken = function(username, token){
return saveToken(username, token);
}
exports.deleteToken = function(id){
return deleteToken(id);
}
exports.checkToken = function(id, token){
return checkToken(id, token);
}
exports.searchProfil = function(name){
return searchProfil(name);
}
exports.pushEventInProfil = function(username, event){
return pushEventInProfil(username, event);
}
exports.containEvent = function(username, eventID){
return containEvent(username, eventID);
}
exports.removeEventInProfil = function(username, eventID){
return removeEventInProfil(username, eventID);
}
exports.getEventInProfil = function(username){
return getEventInProfil(username);
}
exports.modifyUsername = function(id, newUsername){
return modifyUsername(id, newUsername);
}
exports.modifyName = function(id, newName){
return modifyName(id, newName);
}
exports.modifyPass = function(id, newPass){
return modifyPass(id, newPass);
}
\ No newline at end of file
FROM mongo:3.4-xenial
ENV MONGO_INITDB_ROOT_USERNAME user
ENV MONGO_INITDB_ROOT_PASSWORD test
EXPOSE 27017
FROM node:12.13-stretch-slim
COPY . /server/
WORKDIR /server
RUN npm update && npm install
CMD npm start
EXPOSE 8080
function Event(title, description, latitude, longitude, address, zipCode,
eventfulID, cityName, countryName, eventStartTime, venueName, URL) {
this.title = title;
this.description = description!==null?description.substr(0, 100) + "...":"";
this.latitude = latitude;
this.longitude = longitude;
this.address = address;
this.zipCode = zipCode;
this.eventfulID = eventfulID;
this.cityName = cityName;
this.countryName = countryName;
this.eventStartTime = eventStartTime;
this.venueName = venueName;
this.URL = URL;
}
module.exports = Event;
const request = require('request');
let Event = require('./Event.js');
function EventfulClient() { }
var APIKey = "3NmN8QDHDstXNrfC";
EventfulClient.prototype.search = async function(latitude, longitude, radius) {
let URL = "http://api.eventful.com/json/events/search" +
"?app_key=" + APIKey +
"&where=" + latitude + "," + longitude +
"&within=" + radius +
"&units=km" +
"&category=festivals_parades,music" +
"&sort_order=popularity" +
"&page_size=50";
return await requestEvents(URL);
}
EventfulClient.prototype.searchEvent = async function(eventID) {
let URL = "http://api.eventful.com/json/events/get" +
"?app_key=" + APIKey +
"&id=" + eventID;
return await requestUniqueEvent(URL);
}
/*
* http://api.eventful.com/json/events/search?app_key=3NmN8QDHDstXNrfC&where=46.4123346,6.2650554&within=13&units=km&category=festivals_parades,music&sort_order=popularity&page_size=50";
*/
function requestEvents(url) {
return new Promise( resolve => {
request(url, (err,response, body) => {
let lstEvents = [];
try { data = JSON.parse(body); }
catch(error) { data = []; }
if( data["total_items"] > 0) {
events = data["events"]["event"];
lstEvents = events.map(e => new Event( e["title"],
e["description"],
e["latitude"],
e["longitude"],
e["venue_address"],
e["postal_code"],
e["id"],
e["city_name"],
e["country_name"],
e["start_time"],
e["venue_name"],
e["url"] ));
resolve(lstEvents);
}
});
});
}
function requestUniqueEvent(url) {
return new Promise( resolve => {
request(url, (err,response, body) => {
let event = [];
data = JSON.parse(body);
event = new Event( data["title"],
data["description"],
data["latitude"],
data["longitude"],
data["address"],
data["postal_code"],
data["id"],
data["city"],
data["country_name"],
data["start_time"],
data["venue_name"],
data["url"] );
resolve(event);
});
});
}
module.exports = EventfulClient;
const Profil = function(username, name) {
this.username = username;
this.name = name;
this.preferences = [];
this.token = undefined;
}
module.exports = Profil;
"use strict"
const express = require('express');
const app = express();
const bodyParser = require("body-parser");
const jwt = require('jsonwebtoken');
const eventfulClient = require('./EventfulClient.js');
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
require('dotenv').config();
const db = require("./DB/DB.js");
db.registration("Aze", "aze", "aze");
// Make sure that the user token is valid
const auth = async(req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '');
const data = jwt.verify(token, process.env.JWT_SECRET);
if(!db.checkToken(data.userId, token)) {
throw new Error();
}
req.userId = data.userId;
req.token = token;
next();
} catch (error) {
res.status(401).send( { error: 'Not authorized to access this resource' } );
}
}
// Redirect the user to the /map route
app.get('/', (req, res) => {
res.redirect('/map');
});
// Send the register.html file
app.get('/register', (req, res) => {
res.sendFile("./views/register.html", { root: __dirname });
});
// Save the user in the database
app.post('/register', async (req, res) => {
let isValid = await db.registration(req.body.username, req.body.pass, req.body.name);
if(!isValid) {
res.status(409).json( {error: "username already used"} );
} else {
res.sendStatus(200).send();
}
});
// Send the login.html file
app.get('/login', (req, res) => {
res.sendFile("./views/login.html", { root: __dirname });
});
// Save the user token in the database
app.post("/login", async (req,res) => {
let isValid = await db.login(req.body.username, req.body.pass);
if(!isValid) {
res.status(401).json( {error: "authentication error"} );
} else {
let user = await db.getProfilByUsername(req.body.username);
let token = jwt.sign({userId: user._id}, process.env.JWT_SECRET, {expiresIn: '24h'});
let isSaved = await db.saveToken(user.username, token);
if(isSaved){
res.status(200).send( {user: user, token:token} );
}
}
});
// Delete the token from the database
app.get('/logout', auth, async (req, res) => {
let response = await db.deleteToken(req.userId);
if(response){
res.status(200).send();
}
});
// Send the map.html file
app.get('/map', (req, res) => {
res.sendFile("./views/map.html", { root: __dirname });
});
// Return the username and name of the current user
app.get('/profil', auth, async (req, res) => {
let user = await db.getProfilById(req.userId);
res.status(200).json( {username: user.username, name: user.name, token: req.token} );
});
// Return a list of the users favorite events
app.get('/profil/favorite', auth, async (req, res) => {
let username = (await db.getProfilById(req.userId)).username;
let events = await db.getEventInProfil(username);
res.status(200).json( {events: events, token: req.token} );
});
// Add an event to the users profil
app.post('/profil/event', auth, async (req, res) => {
new eventfulClient().searchEvent(req.body.eventID).then( async(results) => {
let username = (await db.getProfilById(req.userId)).username;
let response = await db.pushEventInProfil(username, results);
if(response){
res.status(200).send( {token: req.token} );
}
else {
res.status(409).send();
}
});
});
// Return a list of events depending of latitude, longitude and radius
app.get('/event/:latitude/:longitude/:radius', auth, (req, res) => {
new eventfulClient().search( req.params.latitude,
req.params.longitude,
req.params.radius)
.then( results=> {
res.status(200).send( {events: results, token: req.token} );
});
});
// Return a list of profil matching a keyword
app.get('/profil/names/:name', auth, async (req, res) => {
let users = await db.searchProfil(req.params.name);
if(users){
res.status(200).json( {users: users, token:req.token} );
}
});
// Return one single profil depending on id (used to show a profil user)
app.get('/profil/:name', auth, async (req, res) => {
let user = await db.searchProfil(req.params.name);
if(user){
res.status(200).json( {user: user[0], token:req.token} );
}
else{
res.status(401).send( {token:token} );
}
});
// Delete a specific event from the users profil
app.delete('/profil/event/:eventID', auth, async (req, res) => {
let username = (await db.getProfilById(req.userId)).username;
let response = await db.removeEventInProfil(username, req.params.eventID)
if(response){
res.status(200).send( {token: req.token} );
}
});
// Check if a username is already used before edition
app.post('/profil/edit/check', auth, async (req, res) => {
let username = (await db.getProfilById(req.userId)).username;
let isValid = true;
if(req.body.username !== username)
isValid = !(await db.containUsername(req.body.username));
res.status(200).send( {valid:isValid, token: req.token} );
});
// Verify login before active password modification
app.post('/profil/edit/activate', auth, async (req, res) => {
let username = (await db.getProfilById(req.userId)).username;
let isValid = await db.login(username, req.body.pass);
res.status(200).send( {valid:isValid, token: req.token} );
});
// Verify login before active password modification
app.post('/profil/edit', auth, async (req, res) => {
let user = (await db.getProfilById(req.userId));
let username = user.username;
let name = user.name;
if(req.body.username !== "" && req.body.username !== username) {
await db.modifyUsername(req.userId, req.body.username);
}
if(req.body.name !== "" && req.body.name !== name) {
await db.modifyName(req.userId, req.body.name);
}
if(req.body.pass !== "undefined") {
await db.modifyPass(req.userId, req.body.pass);
}
res.status(200).send( {token:req.token} );
});
app.use(auth);
app.listen(8080);
version: '3'
networks:
eventfinder:
services:
db:
build:
context: ./DB
networks:
- eventfinder
web:
depends_on:
- db
build:
context: .
networks:
- eventfinder
ports:
- "80:8080"
This diff is collapsed.
{
"name": "advanced_web",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "nodemon app.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "ssh://git@ssh.hesge.ch:10572/advanced-web/project.git"
},
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^3.0.6",
"body-parser": "^1.19.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.8.1",
"nodemon": "^1.19.4",
"pug": "^2.0.4",
"request": "^2.88.0"
}
}
.containerForm {
width: 40%;
margin: auto;
margin-top: 50px;
padding: 20px;
background-color: rgb(199, 213, 219);
border-radius: 30px;
}
.formTitle {
display: block;
text-align: center;
margin: auto;
margin-bottom: 15px;
}
.message {
display: block;
text-align: center;
margin: auto;
}
.containerMessageOk {
width: 40%;
margin: auto;
margin-top: 50px;
padding: 10px;
background-color: rgb(204, 231, 207);
border-radius: 30px;
}
.containerMessageFail {
width: 40%;
margin: auto;
margin-top: 50px;
padding: 10px;
background-color: #c7b9b0;
border-radius: 30px;
}
.formSign {
margin: auto;
display: flex;
flex-direction: column;
justify-content: center;
}
.field {
margin: auto;
}
.submit {
width: 50%;
margin: auto;
margin-top: 20px
}
#map {
height: 92%;
width: 100%;
margin: auto;
z-index: 0;
position: relative;
}
.welcome {
margin: 0;
position: absolute;
z-index: 2;
right: 10px;
top: 50px;
}
.form-inline {
margin: 0;
}
#dropdown-choice {
margin: auto;
}
#cards-favorite-events {
top: 100px;
z-index: 2;
width: 350px;
position: absolute;
}
.map-main {
position: relative;
}
#button-fav {
position: absolute;
z-index: 2;
top: 100px;
left: 10px;
}
#btn-more-info, #add-to-list, #delete-event {
display: inline-block;
margin: auto;
margin-top: 5%;
}
/* #btn-more-info {
display: inline-block;
margin-top: 5%;
}
*/
#btn-more-info {
color: white;
margin-left: 5%;
}
#favorites {
/* max-height: 650px; */
max-height: 40rem;
overflow-y:scroll;
overflow-x:scroll;
}
$('.alert').hide();
$("#login").click( () => {
if( !checkForm(["username","pass"]) ) {
return;
}
$.post("/login", {
username: String( $("#username").val() ),
pass: String( $("#pass").val() )
}, (data) => {
localStorage.setItem('token', data.token);
window.location = "/map";
}).fail( () => {
window.location = "/login";
});
});
function checkForm(inputs) {
return inputs.every( i => checkInput(i) );
}
function checkInput(input) {
if($("#"+input).val() === "") {
$("#"+input).removeClass('is-valid');
$("#"+input).addClass('is-invalid');
return false;
}
$("#"+input).removeClass('is-invalid');
return true;
}
This diff is collapsed.
This diff is collapsed.
$("#conflict").hide();
$("#badPass").hide();
$("#register").click( () => {
if( checkForm(["username", "name", "pass", "rePass"]) && checkPassMatches()) {
$.post("/register", {
username: String( $("#username").val() ),
name: String( $("#name").val() ),
pass: String( $("#pass").val() )
}, () => { window.location = "/login"; }
).fail( (data) => {
if( data.statusText === "Conflict") {
unvalidate("username");
$("#conflict").show();
}
});
}
});
function checkForm(inputs) {
return inputs.every( i => checkInput(i) );
}
function checkPass() {
return $("#rePass").val() === $("#pass").val();
}
function checkInput(input) {
if($("#"+input).val() === "") {
unvalidate(input)
return false;
}
validate(input)
return true;
}
function unvalidate(input) {
$("#"+input).removeClass('form-control is-valid');
$("#"+input).addClass('form-control is-invalid');
}
function validate(input) {
$("#"+input).removeClass('form-control is-invalid');
$("#"+input).addClass('form-control is-valid');
}
$('#rePass').on('input', function() {
checkPassMatches();
});
function checkPassMatches() {
if( $("#pass").val() !== "" && !checkPass() ) {
$("#rePass").removeClass('form-control is-valid');
$("#rePass").addClass('form-control is-invalid');
$("#badPass").show();
return false;
} else {
$("#rePass").removeClass('form-control is-invalid');
$("#rePass").addClass('form-control is-valid');
$("#badPass").hide();
return true;
}
}
<mxfile host="Electron" modified="2020-01-05T15:35:04.617Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/11.3.0 Chrome/76.0.3809.139 Electron/6.0.7 Safari/537.36" etag="qJC-1U7w-5tIGQwfjJ-o" version="11.3.0" type="device" pages="1"><diagram id="Q6xCurVH4g-yG2zkMaES" name="Page-1">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</diagram></mxfile>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment