diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000000..c058df210d --- /dev/null +++ b/schema.sql @@ -0,0 +1,145 @@ +-- +-- Name: article; Type: TABLE; Schema: public; Owner: coco98 +-- + +CREATE TABLE article ( + id integer NOT NULL, + title text NOT NULL, + heading text NOT NULL, + date date NOT NULL, + content text NOT NULL +); + +-- +-- Name: article_id_seq; Type: SEQUENCE; Schema: public; Owner: coco98 +-- + +CREATE SEQUENCE article_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: comment; Type: TABLE; Schema: public; Owner: coco98 +-- + +CREATE TABLE comment ( + id integer NOT NULL, + article_id integer NOT NULL, + user_id integer NOT NULL, + comment text NOT NULL, + "timestamp" timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: comment_id_seq; Type: SEQUENCE; Schema: public; Owner: coco98 +-- + +CREATE SEQUENCE comment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user; Type: TABLE; Schema: public; Owner: coco98 +-- + +CREATE TABLE "user" ( + id integer NOT NULL, + username text NOT NULL, + password text NOT NULL +); + + +-- +-- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: coco98 +-- + +CREATE SEQUENCE user_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY article ALTER COLUMN id SET DEFAULT nextval('article_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY comment ALTER COLUMN id SET DEFAULT nextval('comment_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY "user" ALTER COLUMN id SET DEFAULT nextval('user_id_seq'::regclass); + +-- +-- Name: article_id; Type: CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY article + ADD CONSTRAINT article_id PRIMARY KEY (id); + + +-- +-- Name: article_title; Type: CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY article + ADD CONSTRAINT article_title UNIQUE (title); + + +-- +-- Name: comment_id; Type: CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY comment + ADD CONSTRAINT comment_id PRIMARY KEY (id); + + +-- +-- Name: user_id; Type: CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY "user" + ADD CONSTRAINT user_id PRIMARY KEY (id); + + +-- +-- Name: user_username; Type: CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY "user" + ADD CONSTRAINT user_username UNIQUE (username); + + +-- +-- Name: comment_article_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY comment + ADD CONSTRAINT comment_article_id_fkey FOREIGN KEY (article_id) REFERENCES article(id); + + +-- +-- Name: comment_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: coco98 +-- + +ALTER TABLE ONLY comment + ADD CONSTRAINT comment_user_id_fkey FOREIGN KEY (user_id) REFERENCES "user"(id); diff --git a/server.js b/server.js index 3d5cece3ab..63043c8831 100644 --- a/server.js +++ b/server.js @@ -1,20 +1,234 @@ var express = require('express'); var morgan = require('morgan'); var path = require('path'); +var Pool = require('pg').Pool; +var crypto = require('crypto'); +var bodyParser = require('body-parser'); +var session = require('express-session'); + +var config = { + user: 'coco98', + database: 'coco98', + host: 'db.imad.hasura-app.io', + port: '5432', + password: process.env.DB_PASSWORD +}; var app = express(); app.use(morgan('combined')); +app.use(bodyParser.json()); +app.use(session({ + secret: 'someRandomSecretValue', + cookie: { maxAge: 1000 * 60 * 60 * 24 * 30} +})); + +function createTemplate (data) { + var title = data.title; + var date = data.date; + var heading = data.heading; + var content = data.content; + + var htmlTemplate = ` + + + + ${title} + + + + + +
+
+ Home +
+
+

+ ${heading} +

+
+ ${date.toDateString()} +
+
+ ${content} +
+
+

Comments

+
+
+
+
Loading comments...
+
+
+ + + + `; + return htmlTemplate; +} app.get('/', function (req, res) { res.sendFile(path.join(__dirname, 'ui', 'index.html')); }); -app.get('/ui/style.css', function (req, res) { - res.sendFile(path.join(__dirname, 'ui', 'style.css')); + +function hash (input, salt) { + // How do we create a hash? + var hashed = crypto.pbkdf2Sync(input, salt, 10000, 512, 'sha512'); + return ["pbkdf2", "10000", salt, hashed.toString('hex')].join('$'); +} + + +app.get('/hash/:input', function(req, res) { + var hashedString = hash(req.params.input, 'this-is-some-random-string'); + res.send(hashedString); +}); + +app.post('/create-user', function (req, res) { + // username, password + // {"username": "tanmai", "password": "password"} + // JSON + var username = req.body.username; + var password = req.body.password; + var salt = crypto.randomBytes(128).toString('hex'); + var dbString = hash(password, salt); + pool.query('INSERT INTO "user" (username, password) VALUES ($1, $2)', [username, dbString], function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + res.send('User successfully created: ' + username); + } + }); +}); + +app.post('/login', function (req, res) { + var username = req.body.username; + var password = req.body.password; + + pool.query('SELECT * FROM "user" WHERE username = $1', [username], function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + if (result.rows.length === 0) { + res.status(403).send('username/password is invalid'); + } else { + // Match the password + var dbString = result.rows[0].password; + var salt = dbString.split('$')[2]; + var hashedPassword = hash(password, salt); // Creating a hash based on the password submitted and the original salt + if (hashedPassword === dbString) { + + // Set the session + req.session.auth = {userId: result.rows[0].id}; + // set cookie with a session id + // internally, on the server side, it maps the session id to an object + // { auth: {userId }} + + res.send('credentials correct!'); + + } else { + res.status(403).send('username/password is invalid'); + } + } + } + }); +}); + +app.get('/check-login', function (req, res) { + if (req.session && req.session.auth && req.session.auth.userId) { + // Load the user object + pool.query('SELECT * FROM "user" WHERE id = $1', [req.session.auth.userId], function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + res.send(result.rows[0].username); + } + }); + } else { + res.status(400).send('You are not logged in'); + } +}); + +app.get('/logout', function (req, res) { + delete req.session.auth; + res.send('Logged out!

Back to home'); +}); + +var pool = new Pool(config); + +app.get('/get-articles', function (req, res) { + // make a select request + // return a response with the results + pool.query('SELECT * FROM article ORDER BY date DESC', function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + res.send(JSON.stringify(result.rows)); + } + }); +}); + +app.get('/get-comments/:articleName', function (req, res) { + // make a select request + // return a response with the results + pool.query('SELECT comment.*, "user".username FROM article, comment, "user" WHERE article.title = $1 AND article.id = comment.article_id AND comment.user_id = "user".id ORDER BY comment.timestamp DESC', [req.params.articleName], function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + res.send(JSON.stringify(result.rows)); + } + }); +}); + +app.post('/submit-comment/:articleName', function (req, res) { + // Check if the user is logged in + if (req.session && req.session.auth && req.session.auth.userId) { + // First check if the article exists and get the article-id + pool.query('SELECT * from article where title = $1', [req.params.articleName], function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + if (result.rows.length === 0) { + res.status(400).send('Article not found'); + } else { + var articleId = result.rows[0].id; + // Now insert the right comment for this article + pool.query( + "INSERT INTO comment (comment, article_id, user_id) VALUES ($1, $2, $3)", + [req.body.comment, articleId, req.session.auth.userId], + function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + res.status(200).send('Comment inserted!') + } + }); + } + } + }); + } else { + res.status(403).send('Only logged in users can comment'); + } +}); + +app.get('/articles/:articleName', function (req, res) { + // SELECT * FROM article WHERE title = '\'; DELETE WHERE a = \'asdf' + pool.query("SELECT * FROM article WHERE title = $1", [req.params.articleName], function (err, result) { + if (err) { + res.status(500).send(err.toString()); + } else { + if (result.rows.length === 0) { + res.status(404).send('Article not found'); + } else { + var articleData = result.rows[0]; + res.send(createTemplate(articleData)); + } + } + }); }); -app.get('/ui/madi.png', function (req, res) { - res.sendFile(path.join(__dirname, 'ui', 'madi.png')); +app.get('/ui/:fileName', function (req, res) { + res.sendFile(path.join(__dirname, 'ui', req.params.fileName)); }); diff --git a/ui/article.js b/ui/article.js new file mode 100644 index 0000000000..d86ab239f9 --- /dev/null +++ b/ui/article.js @@ -0,0 +1,100 @@ +// Eg: coco98.imad.hasura-app.io/articles/article-one will result in article-one +var currentArticleTitle = window.location.pathname.split('/')[2]; + +function loadCommentForm () { + var commentFormHtml = ` +
Submit a comment
+ +
+ +
+ `; + document.getElementById('comment_form').innerHTML = commentFormHtml; + + // Submit username/password to login + var submit = document.getElementById('submit'); + submit.onclick = function () { + // Create a request object + var request = new XMLHttpRequest(); + + // Capture the response and store it in a variable + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + // Take some action + if (request.status === 200) { + // clear the form & reload all the comments + document.getElementById('comment_text').value = ''; + loadComments(); + } else { + alert('Error! Could not submit comment'); + } + submit.value = 'Submit'; + } + }; + + // Make the request + var comment = document.getElementById('comment_text').value; + request.open('POST', '/submit-comment/' + currentArticleTitle, true); + request.setRequestHeader('Content-Type', 'application/json'); + request.send(JSON.stringify({comment: comment})); + submit.value = 'Submitting...'; + + }; +} + +function loadLogin () { + // Check if the user is already logged in + var request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status === 200) { + loadCommentForm(this.responseText); + } + } + }; + + request.open('GET', '/check-login', true); + request.send(null); +} + +function escapeHTML (text) +{ + var $text = document.createTextNode(text); + var $div = document.createElement('div'); + $div.appendChild($text); + return $div.innerHTML; +} + +function loadComments () { + // Check if the user is already logged in + var request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + var comments = document.getElementById('comments'); + if (request.status === 200) { + var content = ''; + var commentsData = JSON.parse(this.responseText); + for (var i=0; i< commentsData.length; i++) { + var time = new Date(commentsData[i].timestamp); + content += `
+

${escapeHTML(commentsData[i].comment)}

+
+ ${commentsData[i].username} - ${time.toLocaleTimeString()} on ${time.toLocaleDateString()} +
+
`; + } + comments.innerHTML = content; + } else { + comments.innerHTML('Oops! Could not load comments!'); + } + } + }; + + request.open('GET', '/get-comments/' + currentArticleTitle, true); + request.send(null); +} + + +// The first thing to do is to check if the user is logged in! +loadLogin(); +loadComments(); \ No newline at end of file diff --git a/ui/index.html b/ui/index.html index 5a28644db4..66b071da62 100644 --- a/ui/index.html +++ b/ui/index.html @@ -4,12 +4,26 @@ -
- -
-
-
- Hi! I am your webapp. +
+
+ +
+

About Me

+
+ Hi. My name is Tanmai Gopal
+ I work at Hasura +
+
+
+
Loading login status...
+
+
+

My Articles

+
+
Loading articles...
+
+
diff --git a/ui/main.js b/ui/main.js index 4cf7ed58de..57a5718493 100644 --- a/ui/main.js +++ b/ui/main.js @@ -1 +1,139 @@ -console.log('Loaded!'); + +function loadLoginForm () { + var loginHtml = ` +

Login/Register to unlock awesome features

+ + +

+ + + `; + document.getElementById('login_area').innerHTML = loginHtml; + + // Submit username/password to login + var submit = document.getElementById('login_btn'); + submit.onclick = function () { + // Create a request object + var request = new XMLHttpRequest(); + + // Capture the response and store it in a variable + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + // Take some action + if (request.status === 200) { + submit.value = 'Sucess!'; + } else if (request.status === 403) { + submit.value = 'Invalid credentials. Try again?'; + } else if (request.status === 500) { + alert('Something went wrong on the server'); + submit.value = 'Login'; + } else { + alert('Something went wrong on the server'); + submit.value = 'Login'; + } + loadLogin(); + } + // Not done yet + }; + + // Make the request + var username = document.getElementById('username').value; + var password = document.getElementById('password').value; + console.log(username); + console.log(password); + request.open('POST', '/login', true); + request.setRequestHeader('Content-Type', 'application/json'); + request.send(JSON.stringify({username: username, password: password})); + submit.value = 'Logging in...'; + + }; + + var register = document.getElementById('register_btn'); + register.onclick = function () { + // Create a request object + var request = new XMLHttpRequest(); + + // Capture the response and store it in a variable + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + // Take some action + if (request.status === 200) { + alert('User created successfully'); + register.value = 'Registered!'; + } else { + alert('Could not register the user'); + register.value = 'Register'; + } + } + }; + + // Make the request + var username = document.getElementById('username').value; + var password = document.getElementById('password').value; + console.log(username); + console.log(password); + request.open('POST', '/create-user', true); + request.setRequestHeader('Content-Type', 'application/json'); + request.send(JSON.stringify({username: username, password: password})); + register.value = 'Registering...'; + + }; +} + +function loadLoggedInUser (username) { + var loginArea = document.getElementById('login_area'); + loginArea.innerHTML = ` +

Hi ${username}

+ Logout + `; +} + +function loadLogin () { + // Check if the user is already logged in + var request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status === 200) { + loadLoggedInUser(this.responseText); + } else { + loadLoginForm(); + } + } + }; + + request.open('GET', '/check-login', true); + request.send(null); +} + +function loadArticles () { + // Check if the user is already logged in + var request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + var articles = document.getElementById('articles'); + if (request.status === 200) { + var content = '" + articles.innerHTML = content; + } else { + articles.innerHTML('Oops! Could not load all articles!') + } + } + }; + + request.open('GET', '/get-articles', true); + request.send(null); +} + + +// The first thing to do is to check if the user is logged in! +loadLogin(); + +// Now this is something that we could have directly done on the server-side using templating too! +loadArticles(); \ No newline at end of file diff --git a/ui/style.css b/ui/style.css index 71a5f6acc1..301bec1463 100644 --- a/ui/style.css +++ b/ui/style.css @@ -1,7 +1,6 @@ body { font-family: sans-serif; - background-color: lightgrey; - margin-top: 75px; + margin-top: 60px; } .center { @@ -20,3 +19,28 @@ body { height: 200px; } +.container { + max-width: 800px; + margin: 0 auto; + color: #4a4a4a; + font-family: sans-serif; + padding-left: 20px; + padding-right: 20px; +} + +.comment { + margin-left: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #ccc; +} + +.commenter { + font-style: italic; + font-size: 0.8em; +} + +#comment_form { + margin-left: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #ccc; +}