An application that reminds you to water your plants

Start working on this application

+2
.gitignore
··· 1 + .env 2 + db/init.sql
+3
.rubocop.yml
··· 1 + AllCops: 2 + NewCops: enable 3 + TargetRubyVersion: 3.4
+13
Gemfile
··· 1 + # frozen_string_literal: true 2 + 3 + source 'https://rubygems.org' 4 + 5 + gem 'dotenv', '~> 3.1' 6 + gem 'pg', '~> 1.6' 7 + gem 'puma', '~> 7.1' 8 + gem 'rackup', '~> 2.2' 9 + gem 'sinatra', '~> 4.2' 10 + 11 + group :development do 12 + gem 'rubocop', '~> 1.81' 13 + end
+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
··· 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
··· 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
··· 1 + class DatabaseModel 2 + def initialize(database) 3 + @database = database 4 + end 5 + 6 + protected 7 + 8 + attr_reader :database 9 + end
+11
lib/json_parser.rb
··· 1 + # frozen_string_literal: true 2 + 3 + # Contains code related to JSON parsing functionality 4 + module JSONParser 5 + class << self 6 + def parse_request!(request) 7 + request.body.rewind 8 + JSON.parse request.body.read 9 + end 10 + end 11 + end
+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
··· 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
··· 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
··· 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>