+13
Gemfile
+13
Gemfile
+87
Gemfile.lock
+87
Gemfile.lock
···
1
+
GEM
2
+
remote: https://rubygems.org/
3
+
specs:
4
+
ast (2.4.3)
5
+
base64 (0.3.0)
6
+
dotenv (3.1.8)
7
+
json (2.15.1)
8
+
language_server-protocol (3.17.0.5)
9
+
lint_roller (1.1.0)
10
+
logger (1.7.0)
11
+
mustermann (3.0.4)
12
+
ruby2_keywords (~> 0.0.1)
13
+
nio4r (2.7.4)
14
+
parallel (1.27.0)
15
+
parser (3.3.9.0)
16
+
ast (~> 2.4.1)
17
+
racc
18
+
pg (1.6.2)
19
+
pg (1.6.2-aarch64-linux)
20
+
pg (1.6.2-aarch64-linux-musl)
21
+
pg (1.6.2-arm64-darwin)
22
+
pg (1.6.2-x86_64-darwin)
23
+
pg (1.6.2-x86_64-linux)
24
+
pg (1.6.2-x86_64-linux-musl)
25
+
prism (1.6.0)
26
+
puma (7.1.0)
27
+
nio4r (~> 2.0)
28
+
racc (1.8.1)
29
+
rack (3.2.3)
30
+
rack-protection (4.2.1)
31
+
base64 (>= 0.1.0)
32
+
logger (>= 1.6.0)
33
+
rack (>= 3.0.0, < 4)
34
+
rack-session (2.1.1)
35
+
base64 (>= 0.1.0)
36
+
rack (>= 3.0.0)
37
+
rackup (2.2.1)
38
+
rack (>= 3)
39
+
rainbow (3.1.1)
40
+
regexp_parser (2.11.3)
41
+
rubocop (1.81.6)
42
+
json (~> 2.3)
43
+
language_server-protocol (~> 3.17.0.2)
44
+
lint_roller (~> 1.1.0)
45
+
parallel (~> 1.10)
46
+
parser (>= 3.3.0.2)
47
+
rainbow (>= 2.2.2, < 4.0)
48
+
regexp_parser (>= 2.9.3, < 3.0)
49
+
rubocop-ast (>= 1.47.1, < 2.0)
50
+
ruby-progressbar (~> 1.7)
51
+
unicode-display_width (>= 2.4.0, < 4.0)
52
+
rubocop-ast (1.47.1)
53
+
parser (>= 3.3.7.2)
54
+
prism (~> 1.4)
55
+
ruby-progressbar (1.13.0)
56
+
ruby2_keywords (0.0.5)
57
+
sinatra (4.2.1)
58
+
logger (>= 1.6.0)
59
+
mustermann (~> 3.0)
60
+
rack (>= 3.0.0, < 4)
61
+
rack-protection (= 4.2.1)
62
+
rack-session (>= 2.0.0, < 3)
63
+
tilt (~> 2.0)
64
+
tilt (2.6.1)
65
+
unicode-display_width (3.2.0)
66
+
unicode-emoji (~> 4.1)
67
+
unicode-emoji (4.1.0)
68
+
69
+
PLATFORMS
70
+
aarch64-linux
71
+
aarch64-linux-musl
72
+
arm64-darwin
73
+
ruby
74
+
x86_64-darwin
75
+
x86_64-linux
76
+
x86_64-linux-musl
77
+
78
+
DEPENDENCIES
79
+
dotenv (~> 3.1)
80
+
pg (~> 1.6)
81
+
puma (~> 7.1)
82
+
rackup (~> 2.2)
83
+
rubocop (~> 1.81)
84
+
sinatra (~> 4.2)
85
+
86
+
BUNDLED WITH
87
+
2.6.9
+38
app.rb
+38
app.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require 'irb'
4
+
require 'sinatra'
5
+
6
+
require_relative 'lib/database'
7
+
require_relative 'lib/json_parser'
8
+
require_relative 'lib/users'
9
+
10
+
set :bind, '0.0.0.0'
11
+
12
+
database = Database.new
13
+
users = Users.new database
14
+
15
+
get '/' do
16
+
erb :index
17
+
end
18
+
19
+
get '/db-health-check' do
20
+
results = database.exec 'SELECT NOW() AS time'
21
+
results[0]['time']
22
+
end
23
+
24
+
post '/sign-up' do
25
+
user_data = JSONParser.parse_request! request
26
+
27
+
return 'No email provided!' unless user_data.key?('email')
28
+
29
+
users.sign_up! user_data['email']
30
+
'Success'
31
+
end
32
+
33
+
post '/validate-code' do
34
+
validation_data = JSONParser.parse_request! request
35
+
validation_data['IP'] = request.env['REMOTE_ADDR']
36
+
37
+
users.validate_code! validation_data
38
+
end
+27
lib/database.rb
+27
lib/database.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require 'dotenv/load'
4
+
require 'pg'
5
+
6
+
# This class represents basic database interactions
7
+
class Database
8
+
def initialize
9
+
@db_uri = URI.parse(ENV['DATABASE_URL']) if ENV.key? 'DATABASE_URL'
10
+
11
+
connect!
12
+
end
13
+
14
+
def exec(...) = @pg_conn.exec(...)
15
+
16
+
private
17
+
18
+
def connect!
19
+
@pg_conn = PG.connect(
20
+
host: @db_uri&.host || 'localhost',
21
+
port: @db_uri&.port || 5432,
22
+
dbname: ENV.fetch('POSTGRES_DB', nil),
23
+
user: ENV.fetch('POSTGRES_USER', nil),
24
+
password: ENV.fetch('POSTGRES_PASSWORD', nil)
25
+
)
26
+
end
27
+
end
+9
lib/database_model.rb
+9
lib/database_model.rb
+11
lib/json_parser.rb
+11
lib/json_parser.rb
+27
lib/users.rb
+27
lib/users.rb
···
1
+
# frozen_string_literal: true
2
+
3
+
require_relative 'database_model'
4
+
5
+
class Users < DatabaseModel
6
+
COUNT_WITHIN_QUANTUM_SQL = <<~SQL
7
+
SELECT COUNT(*) from validations
8
+
WHERE user_id = $1 AND validated_at >= DATEADD(minute, -30, GETDATE()) AND validated = false
9
+
SQL
10
+
11
+
def sign_up!(email)
12
+
raise 'Invalid email' unless /^[^@]+@[^.]+\.[^.]+$/.match? email
13
+
14
+
database.exec 'INSERT INTO users (email) VALUES ($1)', [email]
15
+
end
16
+
17
+
def validate_code?(data)
18
+
user_id = database.exec 'SELECT id FROM users WHERE email = $1', [data['email']]
19
+
count_within_quantum = database.exec(COUNT_WITHIN_QUANTUM_SQL, [user_id])
20
+
raise 'Too many requests' if count_within_quantum >= 10
21
+
22
+
database.exec 'INSERT INTO validations (user_id, ip_address) VALUES ($1, $2)', [user_id, data['IP']]
23
+
24
+
code = database.exec 'SELECT code FROM users WHERE user_id = $1', user_id
25
+
code == data['code']
26
+
end
27
+
end
+7
views/forms/signup.erb
+7
views/forms/signup.erb
···
1
+
<h3>Sign up</h3>
2
+
3
+
<form action="/signup" method="post">
4
+
<input type="email" name="email" placeholder="Input your email (e.g. internet.user@example.com)" />
5
+
<input type="password" minlength="8" name="password" placeholder="Password (minimum 8 characters)" />
6
+
<input type="submit" value="Sign up" />
7
+
</form>
+6
views/index.erb
+6
views/index.erb
···
1
+
<h1>Plant Reminder</h1>
2
+
<h3>About</h3>
3
+
<p>Plant Reminder is an open source tool built using <a href="https://ruby-lang.org">Ruby</a> with the <a href="https://sinatrarb.com">Sinatra</a> framework.</p>
4
+
<%= erb :'forms/signup' %>
5
+
<h3>How to use it</h3>
6
+
<p>Lorem ipsum dolar sit amet</p>
+16
views/layout.erb
+16
views/layout.erb
···
1
+
<!DOCTYPE html>
2
+
<html>
3
+
<head>
4
+
<meta charset="utf-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6
+
<meta name="color-scheme" content="light dark" />
7
+
<!-- Link to Pico via CDN for now -->
8
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.red.min.css" />
9
+
<title>Plant Reminder</title>
10
+
</head>
11
+
<body>
12
+
<main>
13
+
<%= yield %>
14
+
</main>
15
+
</body>
16
+
</html>