+4
-1
.rubocop.yml
+4
-1
.rubocop.yml
+16
-11
app.rb
+16
-11
app.rb
···
1
1
# frozen_string_literal: true
2
2
3
-
require 'irb'
4
3
require 'sinatra'
5
4
6
5
require_relative 'lib/database'
7
6
require_relative 'lib/data_access'
7
+
require_relative 'lib/services'
8
8
9
9
database = Database.new
10
10
data_access = DataAccess.new database
11
+
services = Services.new data_access
11
12
12
-
get '/' do
13
-
erb :index
14
-
end
13
+
def simple_get(path, view)= get(path) { erb view.to_sym }
15
14
16
-
get '/db-health-check' do
17
-
results = database.exec 'SELECT NOW() AS time'
18
-
results[0]['time']
15
+
# Simple routes
16
+
simple_get '/', :index
17
+
simple_get '/plants', :plants
18
+
get('/db-health-check') { database.healthcheck }
19
+
20
+
post '/new-plant' do
21
+
redirect '/plants' if services.plant.new_plant user_id: params['user_id'],
22
+
name: params['name'],
23
+
plant_type: params['plant_type']
19
24
end
20
25
21
26
post '/sign-up' do
22
-
return data_access.users.sign_up! params['email'] if params.key?('email')
27
+
return services.user.sign_up! params['email'] if params.key?('email')
23
28
24
29
status 400
25
30
'No email provided!'
26
31
end
27
32
28
33
post '/validate-code' do
29
-
data_access.users.validate_code? ip: request.env['REMOTE_ADDR'],
30
-
email: params['email'],
31
-
code: params['validation-code']
34
+
redirect '/plants' if services.user.validate_code? ip: request.env['REMOTE_ADDR'],
35
+
email: params['email'],
36
+
code: params['validation-code']
32
37
end
33
38
34
39
post '/send-email' do
+5
-1
bin/setup
+5
-1
bin/setup
+2
config.ru
+2
config.ru
+2
db/create_db.sql
+2
db/create_db.sql
+12
db/create_plants.sql
+12
db/create_plants.sql
···
1
+
CREATE TABLE IF NOT EXISTS plant_types (
2
+
id SERIAL PRIMARY KEY NOT NULL,
3
+
name VARCHAR(100) NOT NULL,
4
+
watering_frequency_in_minutes INT
5
+
);
6
+
7
+
CREATE TABLE IF NOT EXISTS plants (
8
+
id SERIAL PRIMARY KEY NOT NULL,
9
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
10
+
name VARCHAR(200),
11
+
plant_type_id INT REFERENCES plant_types(id) ON DELETE SET NULL
12
+
);
+5
db/create_users.sql
+5
db/create_users.sql
+7
db/create_validations.sql
+7
db/create_validations.sql
+3
-2
lib/data_access.rb
+3
-2
lib/data_access.rb
···
1
1
# frozen_string_literal: true
2
2
3
3
require_relative 'data_access/users_data_access'
4
+
require_relative 'data_access/validations_data_access'
4
5
5
6
# General class for all data_access classes
6
7
class DataAccess
7
-
attr_reader :database, :users
8
+
attr_reader :users, :validations
8
9
9
10
def initialize(database)
10
-
@database = database
11
11
@users = UsersDataAccess.new database
12
+
@validations = ValidationsDataAccess.new database
12
13
end
13
14
end
+70
lib/data_access/base_data_access.rb
+70
lib/data_access/base_data_access.rb
···
1
+
# frozen_string_literal: false
2
+
3
+
# Base data access layer
4
+
class BaseDataAccess
5
+
class << self
6
+
attr_accessor :table_name
7
+
end
8
+
9
+
def initialize(database)
10
+
@database = database
11
+
end
12
+
13
+
COLUMNS = [].freeze
14
+
15
+
def find_or_create(*, **params)
16
+
base_find(*, **params) do |result, params|
17
+
next result if result.one?
18
+
19
+
id = insert(**params)
20
+
21
+
params[:id] = id
22
+
23
+
find(*, **params)
24
+
end
25
+
end
26
+
27
+
# Do not pass block
28
+
def find(*, **params) = base_find(*, **params)
29
+
30
+
def insert(**params)
31
+
valid_params = params.select { |column| self.class::COLUMNS.include?(column) }
32
+
insert_statement = build_insert_statement(valid_params.keys)
33
+
database.exec(insert_statement, valid_params.values)[0]['id']
34
+
end
35
+
36
+
attr_reader :database
37
+
38
+
private
39
+
40
+
def base_find(choose: [:id], **params)
41
+
valid_choose = choose & self.class::COLUMNS # the intersection operator is great for things like this!
42
+
query_by = filter_params_by_columns params
43
+
query = build_find_query valid_choose, query_by
44
+
result = database.exec query, [*query_by.values]
45
+
return result unless block_given?
46
+
47
+
yield result, params
48
+
end
49
+
50
+
def build_insert_statement(columns)
51
+
"INSERT INTO #{self.class.table_name} (".tap do |sql|
52
+
sql << columns.join(', ')
53
+
sql << ') VALUES ('
54
+
sql << (1..columns.length).map { |pos| "$#{pos}" }.join(', ')
55
+
sql << ') RETURNING id'
56
+
end
57
+
end
58
+
59
+
def build_find_query(valid_choose, query_by)
60
+
'SELECT '.tap do |sql|
61
+
sql << valid_choose.join(', ')
62
+
sql << " FROM #{self.class.table_name} WHERE "
63
+
sql << query_by.map.with_index(1) { |(key), index| "#{key}=$#{index}" }.join(' AND ')
64
+
end
65
+
end
66
+
67
+
def filter_params_by_columns(params)
68
+
params.select { |column| self.class::COLUMNS.include?(column) }
69
+
end
70
+
end
+15
lib/data_access/plant_data_access.rb
+15
lib/data_access/plant_data_access.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require_relative 'base_data_access'
4
+
5
+
# Handles the data access for plants
6
+
class PlantDataAccess < BaseDataAccess
7
+
self.table_name = 'plants'
8
+
9
+
COLUMNS = %i[id user_id name plant_type_id].freeze
10
+
11
+
def insert(user_id:, name:, plant_type_id:)
12
+
database.exec 'INSERT INTO plants (user_id, name, plant_type_id) VALUES ($1, $2, $3)',
13
+
[user_id, name, plant_type_id]
14
+
end
15
+
end
+10
lib/data_access/plant_type_data_access.rb
+10
lib/data_access/plant_type_data_access.rb
+5
-24
lib/data_access/users_data_access.rb
+5
-24
lib/data_access/users_data_access.rb
···
1
1
# frozen_string_literal: true
2
2
3
-
require_relative '../databaseable'
3
+
require_relative 'base_data_access'
4
4
5
5
# Handles the data access for users
6
-
class UsersDataAccess
7
-
include Databaseable
6
+
class UsersDataAccess < BaseDataAccess
7
+
self.table_name = 'users'
8
8
9
-
COUNT_WITHIN_QUANTUM_SQL = <<~SQL
10
-
SELECT COUNT(*) from validations
11
-
WHERE user_id = $1 AND validated_at >= NOW() - INTERVAL '30 minutes' AND validated = false
12
-
SQL
9
+
COLUMNS = %i[id email access_code].freeze
13
10
14
-
def sign_up!(email)
15
-
raise 'Invalid email' unless /^[^@]+@[^.]+\.[^.]+$/.match? email
16
-
11
+
def insert(email:)
17
12
database.exec 'INSERT INTO users (email) VALUES ($1)', [email]
18
-
'success'
19
-
rescue PG::UniqueViolation
20
-
"An account with the email #{email} already exists"
21
-
end
22
-
23
-
def validate_code?(code:, email:, ip:)
24
-
user_id = database.exec('SELECT id FROM users WHERE email = $1', [email])[0]['id']
25
-
count_within_quantum = database.exec(COUNT_WITHIN_QUANTUM_SQL, [user_id])
26
-
raise 'Too many requests' if count_within_quantum[0]['count'].to_i >= 10
27
-
28
-
database.exec 'INSERT INTO validations (user_id, ip_address) VALUES ($1, $2)', [user_id, ip]
29
-
30
-
results = database.exec 'SELECT access_code FROM users WHERE id = $1', [user_id]
31
-
results[0]['code'] == code
32
13
end
33
14
end
+23
lib/data_access/validations_data_access.rb
+23
lib/data_access/validations_data_access.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require_relative 'base_data_access'
4
+
5
+
# Handles the data access for validations
6
+
class ValidationsDataAccess < BaseDataAccess
7
+
self.table_name = 'validations'
8
+
9
+
COLUMNS = %i[id user_id ip_address validated_at validated].freeze
10
+
11
+
COUNT_WITHIN_QUANTUM_SQL = <<~SQL
12
+
SELECT COUNT(*) from validations
13
+
WHERE user_id = $1 AND validated_at >= NOW() - INTERVAL '30 minutes' AND validated = false
14
+
SQL
15
+
16
+
def validations_within_time_quantum_for_user_id(user_id)
17
+
database.exec COUNT_WITHIN_QUANTUM_SQL, [user_id]
18
+
end
19
+
20
+
def insert(user_id:, ip_address:)
21
+
database.exec 'INSERT INTO validations (user_id, ip_address) VALUES ($1, $2)', [user_id, ip_address]
22
+
end
23
+
end
+4
lib/database.rb
+4
lib/database.rb
-9
lib/databaseable.rb
-9
lib/databaseable.rb
+14
lib/services.rb
+14
lib/services.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require_relative 'services/user_service'
4
+
require_relative 'services/validation_service'
5
+
6
+
# General class for all service classes
7
+
class Services
8
+
attr_reader :user, :validation
9
+
10
+
def initialize(data_access)
11
+
@user = UserService.new data_access
12
+
@validation = ValidationService.new data_access
13
+
end
14
+
end
+18
lib/services/base_service.rb
+18
lib/services/base_service.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require 'sinatra'
4
+
5
+
# Base services, initializes data access, services
6
+
class BaseService
7
+
def initialize(data_access)
8
+
@data_access = data_access
9
+
end
10
+
11
+
protected
12
+
13
+
attr_reader :data_access
14
+
15
+
def error!(message)
16
+
halt 500, message
17
+
end
18
+
end
+11
lib/services/plant_service.rb
+11
lib/services/plant_service.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require_relative 'base_service'
4
+
5
+
# Dealing with plant actions
6
+
class PlantService < BaseService
7
+
def new_plant(user_id:, name:, plant_type: nil)
8
+
plant_type_id = data_access.plant_types.find_or_create(name: plant_type) if plant_type
9
+
data_access.plants.insert user_id:, name:, plant_type_id:
10
+
end
11
+
end
+33
lib/services/user_service.rb
+33
lib/services/user_service.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require_relative 'base_service'
4
+
5
+
# Dealing with user's actions
6
+
class UserService < BaseService
7
+
# Signs a user up by email, returns a message to be sent back to the user
8
+
def sign_up!(email)
9
+
raise 'Invalid email' unless /^[^@]+@[^.]+\.[^.]+$/.match? email
10
+
11
+
data_access.users.insert email:
12
+
rescue PG::UniqueViolation
13
+
error! "An account with the email #{email} already exists"
14
+
end
15
+
16
+
# Validates a code to allow a user to sign in (or to check if a user can sign in) return true or false
17
+
def validate_code?(code:, email:, ip:)
18
+
user_id = find_id_from_email email
19
+
count_within_quantum = data_access.validations.validations_within_time_quantum_for_user_id(user_id)[0]['count'].to_i
20
+
raise 'Too many requests' if count_within_quantum >= 10
21
+
22
+
data_access.validations.insert(user_id:, ip:)
23
+
24
+
results = @data_access.users.find(choose: %i[access_code], id: user_id)
25
+
results[0]['access_code'] == code
26
+
end
27
+
28
+
private
29
+
30
+
def find_id_from_email(email)
31
+
data_access.users.find(choose: %i[id], email:)[0]['id']
32
+
end
33
+
end
+7
lib/services/validation_service.rb
+7
lib/services/validation_service.rb
+1
views/plants.erb
+1
views/plants.erb
···
1
+
<h1>Plants</h1>