From 0793fd055a0cb0f7540a035a420af83d4bc62d0a Mon Sep 17 00:00:00 2001 From: Nick McDonnough Date: Fri, 12 Dec 2014 07:23:59 -0600 Subject: [PATCH 01/24] Initial commit --- .gitignore | 5 ++ Gemfile | 8 +++ Gemfile.lock | 54 +++++++++++++++++ config.ru | 3 + lib/chatitude.rb | 29 ++++++++++ lib/chatitude/repos/users_repo.rb | 21 +++++++ public/javascripts/chat.js | 0 server.rb | 96 +++++++++++++++++++++++++++++++ 8 files changed, 216 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 config.ru create mode 100644 lib/chatitude.rb create mode 100644 lib/chatitude/repos/users_repo.rb create mode 100644 public/javascripts/chat.js create mode 100644 server.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..90286207 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.bundle +vendor/bundle +*.swp +*.swo diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..b0b5facf --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem 'sinatra' +gem 'sinatra-contrib' +gem 'eventmachine' +gem 'pry-byebug' +gem 'thin' +gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..fd924b2a --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,54 @@ +GEM + remote: https://rubygems.org/ + specs: + backports (3.6.4) + byebug (3.5.1) + columnize (~> 0.8) + debugger-linecache (~> 1.2) + slop (~> 3.6) + coderay (1.1.0) + columnize (0.9.0) + daemons (1.1.9) + debugger-linecache (1.2.0) + eventmachine (1.0.3) + method_source (0.8.2) + multi_json (1.10.1) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (2.0.0) + byebug (~> 3.4) + pry (~> 0.10) + rack (1.5.2) + rack-protection (1.5.3) + rack + rack-test (0.6.2) + rack (>= 1.0) + sinatra (1.4.5) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + sinatra-contrib (1.4.2) + backports (>= 2.0) + multi_json + rack-protection + rack-test + sinatra (~> 1.4.0) + tilt (~> 1.3) + slop (3.6.0) + thin (1.6.3) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0) + rack (~> 1.0) + tilt (1.4.1) + +PLATFORMS + ruby + +DEPENDENCIES + eventmachine + pry-byebug + sinatra + sinatra-contrib + thin diff --git a/config.ru b/config.ru new file mode 100644 index 00000000..ad41ffe7 --- /dev/null +++ b/config.ru @@ -0,0 +1,3 @@ +require './server' + +run Chatitude::Server diff --git a/lib/chatitude.rb b/lib/chatitude.rb new file mode 100644 index 00000000..c80a518a --- /dev/null +++ b/lib/chatitude.rb @@ -0,0 +1,29 @@ +require_relative 'chatitude/repos/users_repo.rb' + +module Chatitude + def self.create_db_connection dbname + PG.connect(host: 'localhost', dbname: dbname) + end + + def self.clear_db db + db.exec <<-SQL + DELETE FROM users; + SQL + end + + def self.create_tables db + db.exec <<-SQL + CREATE TABLE IF NOT EXISTS users( + id SERIAL PRIMARY KEY, + username VARCHAR, + password VARCHAR + ); + SQL + end + + def self.drop_tables db + db.exec <<-SQL + DROP TABLE users; + SQL + end +end diff --git a/lib/chatitude/repos/users_repo.rb b/lib/chatitude/repos/users_repo.rb new file mode 100644 index 00000000..668611ed --- /dev/null +++ b/lib/chatitude/repos/users_repo.rb @@ -0,0 +1,21 @@ +module Chatitude + class UsersRepo + def self.find db, user_id + sql = %q[SELECT * FROM users WHERE id = $1] + result = db.exec(sql, [user_id]) + result.first + end + + def self.find_by_name db, username + sql = %q[SELECT * FROM users WHERE username = $1] + result = db.exec(sql, [username]) + result.first + end + + def self.save db, user_data + sql = %q[INSERT INTO users (username, password) VALUES ($1, $2) RETURNING *] + result = db.exec(sql, [user_data[:username], user_data[:password]]) + result.first + end + end +end diff --git a/public/javascripts/chat.js b/public/javascripts/chat.js new file mode 100644 index 00000000..e69de29b diff --git a/server.rb b/server.rb new file mode 100644 index 00000000..811363b3 --- /dev/null +++ b/server.rb @@ -0,0 +1,96 @@ +require 'sinatra' +require 'sinatra/reloader' +require 'eventmachine' +require 'json' +require 'pry-byebug' + +require './lib/chatitude' + +class Chatitude::Server < Sinatra::Application + configure do + set server: 'thin' + end + + helpers do + def db + Chatitude.create_db_connection 'blogtastic' + end + + def timestamp + Time.now.strftime("%H:%M:%S") + end + end + + connections = [] + + before do + if session[:user_id] + @current_user = Chatitude::UsersRepo.find db, session[:user_id] + end + end + + ############ MAIN ROUTES ############### + + get '/' do + erb :index + end + + ########### SESSION STUFF ############## + + get '/signup' do + end + + get '/signin' do + end + + post '/signup' do + if params[:password] == params[:password_confirmation] + user_data = {username: params[:username], password: params[:password]} + user = Chatitude::UsersRepo.save db, user_data + session[:user_id] = user['id'] + redirect to '/posts' # no redirect + end + end + + post '/signin' do + user = Chatitude::UsersRepo.find_by_name db, params[:username] + + if user && user['password'] == params[:password] + session[:user_id] = user['id'] + redirect to '/posts' # no redirect + end + end + + get '/logout' do + session.delete('user_id') + redirect to '/posts' + end + + ########################################## + # event stream stuff. + + # this endpoint is where the user goes in the + # browser to see incoming messages. + get '/chat', provides: 'text/event-stream' do + stream :keep_open do |out| + # EventMachine::PeriodicTimer.new(20) { out << '\0' } # keep connection open + def out.user; @current_user['id']; end + connections << out + out.callback { connections.delete(out) } + end + end + + # this endpoint is where messages are sent to. + post '/chat' do + sender = @current_user['username'] + message = params[:chat_message] + connections.each { |out| out << "#{timestamp} #{sender}: #{message}\n"} + end + + post '/chatping' do + if params[:ping] + conn = connections.find { |out| out.user == @current_user['id'] } + conn << "PONG" + end + end +end From 83ef6afd06a0246147edcc0996b694327bd5602a Mon Sep 17 00:00:00 2001 From: Nick McDonnough Date: Fri, 12 Dec 2014 07:35:23 -0600 Subject: [PATCH 02/24] Check that user is logged in before creating a connection for him --- server.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server.rb b/server.rb index 811363b3..9add0f7e 100644 --- a/server.rb +++ b/server.rb @@ -72,11 +72,14 @@ def timestamp # this endpoint is where the user goes in the # browser to see incoming messages. get '/chat', provides: 'text/event-stream' do - stream :keep_open do |out| - # EventMachine::PeriodicTimer.new(20) { out << '\0' } # keep connection open - def out.user; @current_user['id']; end - connections << out - out.callback { connections.delete(out) } + if @current_user + stream :keep_open do |out| + def out.user; @current_user['id']; end + connections << out + out.callback { connections.delete(out) } + end + else + # something to make them sign in end end From ae13f35f9e6dc977e1d990b41d62995012f7d4f2 Mon Sep 17 00:00:00 2001 From: Nick McDonnough Date: Fri, 12 Dec 2014 07:35:33 -0600 Subject: [PATCH 03/24] Add readme --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..36b6e284 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Chatitude + +A start on a chat server built using Sinatra and server sent events. The SSE necessitated the use of an evented web server so WEBrick was swapped out for Thin. + +## Notes + +The *client* closes connections after around 40 seconds and the only remedy I could find is to require the client to send a PING message periodically and have the server respond with a PONG message. The server response is what will actually keep the connection open. + +Chat users are identified by their ID which is stored in the session. Users have to sign up and have an account (username & password) to begin chatting. + +This line in the `get '/chat'` endpoint adds a method called `user` to the instance of Sinatra::Helpers::Stream so that they can be identified for keeping connections open and later for private messages. +``` +def out.user; @current_user['id']; end +``` From 4f39deb8050932fb2dd1549251e3dcde668cfc1a Mon Sep 17 00:00:00 2001 From: Nick McDonnough Date: Fri, 12 Dec 2014 09:53:34 -0600 Subject: [PATCH 04/24] Respond with json and clean up routes --- server.rb | 80 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/server.rb b/server.rb index 9add0f7e..a2f6c8b9 100644 --- a/server.rb +++ b/server.rb @@ -7,10 +7,13 @@ require './lib/chatitude' class Chatitude::Server < Sinatra::Application + + # use thin instead of webrick configure do set server: 'thin' end + # helpers for connecting to db and prerparing json responses helpers do def db Chatitude.create_db_connection 'blogtastic' @@ -19,51 +22,70 @@ def db def timestamp Time.now.strftime("%H:%M:%S") end + + def respond sender, message + { + :sender => sender, + :message => message, + :timestamp => timestamp + }.to_json + end + + def parse_message input + msg_pieces = input.split + data = {sender: @current_user['username']} + if msg_pieces.first == '/pm' + data.merge({ + :recipient => msg_pieces[1], + :message => msg_pieces[2..-1].join(' ') + }) + else + data.merge({ + :recipient => :all, + :message => input + }) + end + end end - - connections = [] - + + # run this before every endpoint to get the current user before do if session[:user_id] @current_user = Chatitude::UsersRepo.find db, session[:user_id] end end + # Array used to store event streams + connections = [] + + ############ MAIN ROUTES ############### get '/' do erb :index end - - ########### SESSION STUFF ############## - get '/signup' do - end - - get '/signin' do - end - post '/signup' do if params[:password] == params[:password_confirmation] user_data = {username: params[:username], password: params[:password]} user = Chatitude::UsersRepo.save db, user_data session[:user_id] = user['id'] - redirect to '/posts' # no redirect + {action: 'signup', success: true}.to_json end end - + post '/signin' do user = Chatitude::UsersRepo.find_by_name db, params[:username] if user && user['password'] == params[:password] session[:user_id] = user['id'] - redirect to '/posts' # no redirect + {action: 'signin', success: true}.to_json end end get '/logout' do session.delete('user_id') - redirect to '/posts' + redirect to '/' end ########################################## @@ -74,26 +96,32 @@ def timestamp get '/chat', provides: 'text/event-stream' do if @current_user stream :keep_open do |out| - def out.user; @current_user['id']; end + def out.user; @current_user; end connections << out out.callback { connections.delete(out) } end else - # something to make them sign in + redirect to 'sign' end end # this endpoint is where messages are sent to. post '/chat' do - sender = @current_user['username'] - message = params[:chat_message] - connections.each { |out| out << "#{timestamp} #{sender}: #{message}\n"} - end - - post '/chatping' do - if params[:ping] - conn = connections.find { |out| out.user == @current_user['id'] } - conn << "PONG" + sender = @current_user['username'] + input = parse_message params[:chat_message] + if input[:recipient] == :all + connections.each { |out| out << respond(sender, params[:chat_message]) } + else + rconn = connections.find { |out| out.user['username'] == input[:recipient] } + rconn << respond(input[:sender], input[:message]) if rconn end end + + # possibly not needed? + # post '/chatping' do + # if params[:ping] + # conn = connections.find { |out| out.user == @current_user['id'] } + # conn << "PONG" + # end + # end end From c959e59c875f5bf9ecbedeecf5ecae71d5a2d75a Mon Sep 17 00:00:00 2001 From: Nick McDonnough Date: Fri, 12 Dec 2014 09:55:03 -0600 Subject: [PATCH 05/24] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36b6e284..8e5038ac 100644 --- a/README.md +++ b/README.md @@ -10,5 +10,5 @@ Chat users are identified by their ID which is stored in the session. Users have This line in the `get '/chat'` endpoint adds a method called `user` to the instance of Sinatra::Helpers::Stream so that they can be identified for keeping connections open and later for private messages. ``` -def out.user; @current_user['id']; end +def out.user; @current_user; end ``` From 7962cee570ad7b52a1ac1688ef834161674852a4 Mon Sep 17 00:00:00 2001 From: Nick McDonnough Date: Fri, 12 Dec 2014 13:06:27 -0600 Subject: [PATCH 06/24] So many changes.... --- Gemfile | 1 - Gemfile.lock | 3 +- Rakefile | 45 +++++++++++++++++++++++++++ lib/chatitude.rb | 10 +++++- public/javascripts/chat.js | 0 server.rb | 63 ++++++++++++++++++------------------- views/index.erb | 64 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 Rakefile delete mode 100644 public/javascripts/chat.js create mode 100644 views/index.erb diff --git a/Gemfile b/Gemfile index b0b5facf..5d85a47d 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,6 @@ source 'https://rubygems.org' gem 'sinatra' gem 'sinatra-contrib' -gem 'eventmachine' gem 'pry-byebug' gem 'thin' gem 'pg' diff --git a/Gemfile.lock b/Gemfile.lock index fd924b2a..3d8682ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,7 @@ GEM eventmachine (1.0.3) method_source (0.8.2) multi_json (1.10.1) + pg (0.17.1) pry (0.10.1) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -47,7 +48,7 @@ PLATFORMS ruby DEPENDENCIES - eventmachine + pg pry-byebug sinatra sinatra-contrib diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..f82d6eb1 --- /dev/null +++ b/Rakefile @@ -0,0 +1,45 @@ +task :environment do + require './lib/chatitude' +end + +task :console => :environment do + require 'irb' + ARGV.clear + IRB.start +end + +namespace :db do + task :create do + `createdb chatitude` + puts 'Database \'chatitude\' created!' + end + + task :drop do + `dropdb chatitude` + puts 'Database \'chatitude\' dropped!' + end + + task :create_tables => :environment do + db = Chatitude.create_db_connection 'chatitude' + Chatitude.create_tables db + puts 'Created tables.' + end + + task :drop_tables => :environment do + db = Chatitude.create_db_connection 'chatitude' + Chatitude.drop_tables db + puts 'Dropped tables.' + end + + task :clear => :environment do + db = Chatitude.create_db_connection 'chatitude' + Chatitude.clear_tables db + puts 'Cleared tables.' + end + + task :seed => :environment do + db = Chatitude.create_db_connection 'chatitude' + Chatitude.seed_dummy_users db + puts 'Tables seeded.' + end +end diff --git a/lib/chatitude.rb b/lib/chatitude.rb index c80a518a..8583349e 100644 --- a/lib/chatitude.rb +++ b/lib/chatitude.rb @@ -1,3 +1,4 @@ +require 'pg' require_relative 'chatitude/repos/users_repo.rb' module Chatitude @@ -5,7 +6,7 @@ def self.create_db_connection dbname PG.connect(host: 'localhost', dbname: dbname) end - def self.clear_db db + def self.clear db db.exec <<-SQL DELETE FROM users; SQL @@ -26,4 +27,11 @@ def self.drop_tables db DROP TABLE users; SQL end + + def self.seed_dummy_users db + db.exec <<-SQL + INSERT INTO users (username, password) + VALUES ('nick','nick'), ('kate','kate'); + SQL + end end diff --git a/public/javascripts/chat.js b/public/javascripts/chat.js deleted file mode 100644 index e69de29b..00000000 diff --git a/server.rb b/server.rb index a2f6c8b9..7e339e10 100644 --- a/server.rb +++ b/server.rb @@ -1,6 +1,5 @@ require 'sinatra' require 'sinatra/reloader' -require 'eventmachine' require 'json' require 'pry-byebug' @@ -10,6 +9,7 @@ class Chatitude::Server < Sinatra::Application # use thin instead of webrick configure do + enable :sessions set server: 'thin' end @@ -20,7 +20,7 @@ def db end def timestamp - Time.now.strftime("%H:%M:%S") + Time.now.to_i end def respond sender, message @@ -50,14 +50,24 @@ def parse_message input # run this before every endpoint to get the current user before do + # this condition assign the current user if someone is logged in if session[:user_id] @current_user = Chatitude::UsersRepo.find db, session[:user_id] end + + # the next few lines are to allow cross domain requests + cors = { + "Access-Control-Allow-Origin" => "*", + "Access-Control-Allow-Methods" => "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers" => "Origin, X-Requested-With, Content-Type, Accept" + } + headers.merge! cors end # Array used to store event streams - connections = [] - + # connections = [] + + messages, message_count = [], 0 ############ MAIN ROUTES ############### @@ -85,43 +95,30 @@ def parse_message input get '/logout' do session.delete('user_id') - redirect to '/' + status 200 end ########################################## # event stream stuff. - # this endpoint is where the user goes in the - # browser to see incoming messages. - get '/chat', provides: 'text/event-stream' do - if @current_user - stream :keep_open do |out| - def out.user; @current_user; end - connections << out - out.callback { connections.delete(out) } - end + get '/chat' do + content_type 'application/json' + if params[:since] + messages.select { |m| params[:since] < m['time'] } else - redirect to 'sign' - end + messages.last 10 + end.to_json end - - # this endpoint is where messages are sent to. + post '/chat' do - sender = @current_user['username'] - input = parse_message params[:chat_message] - if input[:recipient] == :all - connections.each { |out| out << respond(sender, params[:chat_message]) } - else - rconn = connections.find { |out| out.user['username'] == input[:recipient] } - rconn << respond(input[:sender], input[:message]) if rconn - end + message_count += 1 + messages << { + user: @current_user['username'], + message: params[:chat_message], + time: timestamp, + id: message_count + } + status 200 end - # possibly not needed? - # post '/chatping' do - # if params[:ping] - # conn = connections.find { |out| out.user == @current_user['id'] } - # conn << "PONG" - # end - # end end diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 00000000..a950ce7e --- /dev/null +++ b/views/index.erb @@ -0,0 +1,64 @@ + + + + + + + + + + + +

+ + + + + + + + + + + + From db5178c64a2fcb1eebeee45bee152b22835d8c21 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 13:31:25 -0600 Subject: [PATCH 07/24] Whitespace --- server.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.rb b/server.rb index 7e339e10..17f108fa 100644 --- a/server.rb +++ b/server.rb @@ -6,13 +6,13 @@ require './lib/chatitude' class Chatitude::Server < Sinatra::Application - + # use thin instead of webrick configure do enable :sessions set server: 'thin' end - + # helpers for connecting to db and prerparing json responses helpers do def db @@ -66,7 +66,7 @@ def parse_message input # Array used to store event streams # connections = [] - + messages, message_count = [], 0 ############ MAIN ROUTES ############### From 7285ff65d1ebc214f2d3cd6da61c0cdef0cf6405 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 13:48:22 -0600 Subject: [PATCH 08/24] Fix CORS --- server.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/server.rb b/server.rb index 17f108fa..15c1ec93 100644 --- a/server.rb +++ b/server.rb @@ -56,12 +56,9 @@ def parse_message input end # the next few lines are to allow cross domain requests - cors = { - "Access-Control-Allow-Origin" => "*", - "Access-Control-Allow-Methods" => "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers" => "Origin, X-Requested-With, Content-Type, Accept" - } - headers.merge! cors + headers["Access-Control-Allow-Origin"] = "*" + headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept" end # Array used to store event streams From 1a14f0ec6ec65b16f4927263ed0eb54be524ffe7 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 13:48:57 -0600 Subject: [PATCH 09/24] Require login to post a message --- server.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server.rb b/server.rb index 15c1ec93..276dfb95 100644 --- a/server.rb +++ b/server.rb @@ -108,14 +108,18 @@ def parse_message input end post '/chat' do - message_count += 1 - messages << { - user: @current_user['username'], - message: params[:chat_message], - time: timestamp, - id: message_count - } - status 200 + if @current_user + message_count += 1 + messages << { + user: @current_user['username'], + message: params[:chat_message], + time: timestamp, + id: message_count + } + status 200 + else + status 403 + end end end From 31d730957e8c2a0cca763b7afab62ba137efaa07 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 14:21:13 -0600 Subject: [PATCH 10/24] Add rake to Gemfile --- Gemfile | 1 + Gemfile.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index 5d85a47d..81776ec3 100644 --- a/Gemfile +++ b/Gemfile @@ -5,3 +5,4 @@ gem 'sinatra-contrib' gem 'pry-byebug' gem 'thin' gem 'pg' +gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock index 3d8682ef..93d8cc90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,6 +26,7 @@ GEM rack rack-test (0.6.2) rack (>= 1.0) + rake (10.3.2) sinatra (1.4.5) rack (~> 1.4) rack-protection (~> 1.4) @@ -50,6 +51,7 @@ PLATFORMS DEPENDENCIES pg pry-byebug + rake sinatra sinatra-contrib thin From 3642f5326e6a853f14b2b6fd4c538dda306b5a64 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 14:33:48 -0600 Subject: [PATCH 11/24] Correct database name --- server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.rb b/server.rb index 276dfb95..12f8c579 100644 --- a/server.rb +++ b/server.rb @@ -16,7 +16,7 @@ class Chatitude::Server < Sinatra::Application # helpers for connecting to db and prerparing json responses helpers do def db - Chatitude.create_db_connection 'blogtastic' + Chatitude.create_db_connection 'chatitude' end def timestamp From 362c7b5415628c045d7792d85f7f8279ffce2945 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 15:43:42 -0600 Subject: [PATCH 12/24] Use api tokens --- lib/chatitude.rb | 9 ++++++++- lib/chatitude/repos/users_repo.rb | 24 ++++++++++++++++++++++++ server.rb | 10 ++++++---- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/lib/chatitude.rb b/lib/chatitude.rb index 8583349e..7b5779d2 100644 --- a/lib/chatitude.rb +++ b/lib/chatitude.rb @@ -9,6 +9,7 @@ def self.create_db_connection dbname def self.clear db db.exec <<-SQL DELETE FROM users; + DELETE FROM sessions; SQL end @@ -19,12 +20,18 @@ def self.create_tables db username VARCHAR, password VARCHAR ); + CREATE TABLE IF NOT EXISTS sessions( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users (id), + token TEXT UNIQUE + ); SQL end def self.drop_tables db db.exec <<-SQL - DROP TABLE users; + DROP TABLE IF EXISTS users; + DROP TABLE IF EXISTS sessions; SQL end diff --git a/lib/chatitude/repos/users_repo.rb b/lib/chatitude/repos/users_repo.rb index 668611ed..88314352 100644 --- a/lib/chatitude/repos/users_repo.rb +++ b/lib/chatitude/repos/users_repo.rb @@ -1,3 +1,5 @@ +require 'securerandom' + module Chatitude class UsersRepo def self.find db, user_id @@ -12,10 +14,32 @@ def self.find_by_name db, username result.first end + def self.find_by_token db, token + sql = %q[ + SELECT + u.id + , u.username + , u.password + FROM sessions s + JOIN users u + ON s.user_id = u.id + WHERE s.token = $1 + ] + result = db.exec(sql, [token]) + result.first + end + def self.save db, user_data sql = %q[INSERT INTO users (username, password) VALUES ($1, $2) RETURNING *] result = db.exec(sql, [user_data[:username], user_data[:password]]) result.first end + + def self.sign_in db, id + token = SecureRandom.hex(16) + sql = %q[INSERT INTO sessions (user_id, token) VALUES ($1, $2)] + result = db.exec(sql, [id, token]) + token + end end end diff --git a/server.rb b/server.rb index 12f8c579..0fe2dc56 100644 --- a/server.rb +++ b/server.rb @@ -51,8 +51,8 @@ def parse_message input # run this before every endpoint to get the current user before do # this condition assign the current user if someone is logged in - if session[:user_id] - @current_user = Chatitude::UsersRepo.find db, session[:user_id] + if params[:apiToken] + @current_user = Chatitude::UsersRepo.find_by_token db, params[:apiToken] end # the next few lines are to allow cross domain requests @@ -85,8 +85,10 @@ def parse_message input user = Chatitude::UsersRepo.find_by_name db, params[:username] if user && user['password'] == params[:password] - session[:user_id] = user['id'] - {action: 'signin', success: true}.to_json + token = Chatitude::UsersRepo.sign_in db, user['id'] + { apiToken: token }.to_json + else + status 401 end end From d428af2c63aa8b1fb9a9695f246d3dbd1f73001f Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 15:43:53 -0600 Subject: [PATCH 13/24] Provide errors on incorrect signup --- server.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server.rb b/server.rb index 0fe2dc56..b1479468 100644 --- a/server.rb +++ b/server.rb @@ -73,11 +73,22 @@ def parse_message input end post '/signup' do - if params[:password] == params[:password_confirmation] + errors = [] + if !params[:password] || params[:password] == '' + errors << 'blank_password' + end + if !params[:username] || params[:username] == '' + errors << 'blank_username' + end + + if errors.count == 0 user_data = {username: params[:username], password: params[:password]} user = Chatitude::UsersRepo.save db, user_data session[:user_id] = user['id'] - {action: 'signup', success: true}.to_json + status 200 + else + status 400 + { errors: errors }.to_json end end From 15bc6a35c2268747d52ce62d4bcc49ca47b508f6 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 15:44:05 -0600 Subject: [PATCH 14/24] Make routes slightly more restful --- server.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.rb b/server.rb index b1479468..8607d517 100644 --- a/server.rb +++ b/server.rb @@ -111,7 +111,7 @@ def parse_message input ########################################## # event stream stuff. - get '/chat' do + get '/chats' do content_type 'application/json' if params[:since] messages.select { |m| params[:since] < m['time'] } @@ -120,12 +120,12 @@ def parse_message input end.to_json end - post '/chat' do + post '/chats' do if @current_user message_count += 1 messages << { user: @current_user['username'], - message: params[:chat_message], + message: params[:message], time: timestamp, id: message_count } From 3e13295ecc319b8daa43f218e559f72ed62858d0 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Fri, 12 Dec 2014 15:53:08 -0600 Subject: [PATCH 15/24] Remove index endpoint --- server.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.rb b/server.rb index 8607d517..fa0fe860 100644 --- a/server.rb +++ b/server.rb @@ -68,9 +68,9 @@ def parse_message input ############ MAIN ROUTES ############### - get '/' do - erb :index - end + # get '/' do + # erb :index + # end post '/signup' do errors = [] From c4e21a94a97d863b4718a4118dbb13cf71dd4c47 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Sun, 14 Dec 2014 18:55:23 -0600 Subject: [PATCH 16/24] Add ruby 2.0 to Gemfile --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 81776ec3..53fdd760 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' +ruby '2.0.0' gem 'sinatra' gem 'sinatra-contrib' From 81605b02c272e2755b8389a0b1c70e8dbf7356e0 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Sun, 14 Dec 2014 18:57:24 -0600 Subject: [PATCH 17/24] Signout endpoint --- lib/chatitude/repos/users_repo.rb | 4 ++++ server.rb | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/chatitude/repos/users_repo.rb b/lib/chatitude/repos/users_repo.rb index 88314352..8102ca0e 100644 --- a/lib/chatitude/repos/users_repo.rb +++ b/lib/chatitude/repos/users_repo.rb @@ -41,5 +41,9 @@ def self.sign_in db, id result = db.exec(sql, [id, token]) token end + + def self.sign_out db, token + db.exec("DELETE FROM sessions WHERE token = $1", [token]) + end end end diff --git a/server.rb b/server.rb index fa0fe860..affa8c7f 100644 --- a/server.rb +++ b/server.rb @@ -103,8 +103,8 @@ def parse_message input end end - get '/logout' do - session.delete('user_id') + delete '/signout' do + Chatitude::UsersRepo.sign_out db, params[:apiToken] status 200 end From bd323b14972aef4effb25bec415cd806a2c7381f Mon Sep 17 00:00:00 2001 From: Gilbert Date: Sun, 14 Dec 2014 18:57:46 -0600 Subject: [PATCH 18/24] Set json content type for all endpoints --- server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.rb b/server.rb index affa8c7f..cd380d42 100644 --- a/server.rb +++ b/server.rb @@ -50,6 +50,7 @@ def parse_message input # run this before every endpoint to get the current user before do + content_type 'application/json' # this condition assign the current user if someone is logged in if params[:apiToken] @current_user = Chatitude::UsersRepo.find_by_token db, params[:apiToken] @@ -112,7 +113,6 @@ def parse_message input # event stream stuff. get '/chats' do - content_type 'application/json' if params[:since] messages.select { |m| params[:since] < m['time'] } else From 86c04d451198c805d750bf277b67237c8331d4af Mon Sep 17 00:00:00 2001 From: Gilbert Date: Sun, 14 Dec 2014 18:58:00 -0600 Subject: [PATCH 19/24] Validate new chat messages --- server.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server.rb b/server.rb index cd380d42..16e86d8c 100644 --- a/server.rb +++ b/server.rb @@ -122,16 +122,27 @@ def parse_message input post '/chats' do if @current_user + + msg = params[:message] + if msg.nil? || msg == '' + status 400 + return { errors: ["blank_message"] }.to_json + elsif messages.find {|m| m[:message] == msg } + status 400 + return { errors: ["message_already_exists"] }.to_json + end + message_count += 1 messages << { user: @current_user['username'], - message: params[:message], + message: msg, time: timestamp, id: message_count } status 200 else status 403 + { errors: "invalid_api_key" }.to_json end end From c6be3c738017b5384336415065beaa822ac05005 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Sun, 14 Dec 2014 18:58:18 -0600 Subject: [PATCH 20/24] Helpful index endpoint --- server.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/server.rb b/server.rb index 16e86d8c..6414475a 100644 --- a/server.rb +++ b/server.rb @@ -69,9 +69,22 @@ def parse_message input ############ MAIN ROUTES ############### - # get '/' do - # erb :index - # end + get '/' do + { + endpoints: [ + { name: "GET /chats", + description: "List all chats" }, + { name: "POST /signup", + description: "Sign up for an account" }, + { name: "POST /signin", + description: "Retrieve an API token for an existing account" }, + { name: "POST /chats", + description: "Create a new chat message" }, + { name: "DELETE /signout", + description: "Invalidate your API token" }, + ] + }.to_json + end post '/signup' do errors = [] From 06a7ea0c4459b003c888cdf6266825363970774a Mon Sep 17 00:00:00 2001 From: Gilbert Date: Mon, 15 Dec 2014 12:32:26 -0600 Subject: [PATCH 21/24] Enforce username uniqueness --- server.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server.rb b/server.rb index 6414475a..17815bf9 100644 --- a/server.rb +++ b/server.rb @@ -94,6 +94,9 @@ def parse_message input if !params[:username] || params[:username] == '' errors << 'blank_username' end + if Chatitude::UsersRepo.find_by_name(db, params[:username]) + errors << 'username_taken' + end if errors.count == 0 user_data = {username: params[:username], password: params[:password]} From 901ca10454b18f18f9e8487c4ca2e28e7538cd06 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Mon, 15 Dec 2014 12:34:01 -0600 Subject: [PATCH 22/24] Don't return blank JSON responses --- server.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server.rb b/server.rb index 17815bf9..6d49139a 100644 --- a/server.rb +++ b/server.rb @@ -103,6 +103,7 @@ def parse_message input user = Chatitude::UsersRepo.save db, user_data session[:user_id] = user['id'] status 200 + '{}' else status 400 { errors: errors }.to_json @@ -123,6 +124,7 @@ def parse_message input delete '/signout' do Chatitude::UsersRepo.sign_out db, params[:apiToken] status 200 + '{}' end ########################################## @@ -156,6 +158,7 @@ def parse_message input id: message_count } status 200 + '{}' else status 403 { errors: "invalid_api_key" }.to_json From f37754ab97cba9d78b85b8fb6928adc7397a56e9 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Wed, 17 Dec 2014 13:05:57 -0600 Subject: [PATCH 23/24] Add index page for demonstration --- public/index.html | 1 + server.rb | 16 +--------------- 2 files changed, 2 insertions(+), 15 deletions(-) create mode 100644 public/index.html diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..ab55abe5 --- /dev/null +++ b/public/index.html @@ -0,0 +1 @@ +

hi

diff --git a/server.rb b/server.rb index 6d49139a..8087d680 100644 --- a/server.rb +++ b/server.rb @@ -50,7 +50,6 @@ def parse_message input # run this before every endpoint to get the current user before do - content_type 'application/json' # this condition assign the current user if someone is logged in if params[:apiToken] @current_user = Chatitude::UsersRepo.find_by_token db, params[:apiToken] @@ -70,20 +69,7 @@ def parse_message input ############ MAIN ROUTES ############### get '/' do - { - endpoints: [ - { name: "GET /chats", - description: "List all chats" }, - { name: "POST /signup", - description: "Sign up for an account" }, - { name: "POST /signin", - description: "Retrieve an API token for an existing account" }, - { name: "POST /chats", - description: "Create a new chat message" }, - { name: "DELETE /signout", - description: "Invalidate your API token" }, - ] - }.to_json + send_file 'public/index.html' end post '/signup' do From 6b4f7114d250d79492ee029a26213f4836f43cdd Mon Sep 17 00:00:00 2001 From: Dudemullet Date: Thu, 1 Jan 2015 22:15:02 -0800 Subject: [PATCH 24/24] Adds correct retrieval of key and String to Float comparison for the since field --- server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.rb b/server.rb index 8087d680..3dc6ff8c 100644 --- a/server.rb +++ b/server.rb @@ -118,7 +118,7 @@ def parse_message input get '/chats' do if params[:since] - messages.select { |m| params[:since] < m['time'] } + messages.select { |m| params[:since].to_f < m[:time] } else messages.last 10 end.to_json