Compare commits

...

16 Commits

Author SHA1 Message Date
Arthur POULET 35b69ef33a
Loosen on dependencies version
continuous-integration/drone/push Build is passing Details
2022-07-28 00:47:13 +02:00
Arthur POULET 46d14c2917
Update dependencies (security)
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2022-07-28 00:14:35 +02:00
Arthur POULET 3d607ade56 deps: execute bundle update 2022-06-20 23:48:15 +02:00
Arthur POULET 08a2729ce6
Merge remote-tracking branch 'origin/develop' into develop
continuous-integration/drone/push Build is passing Details
2022-04-23 11:30:35 +02:00
Arthur POULET d4de167df2
Merge remote-tracking branch 'origin/master'
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
2022-04-23 11:28:44 +02:00
Arthur POULET 29e78b7f52
cookies: add a duration for cookies 2022-04-23 11:27:55 +02:00
Arthur POULET 5f8afb008e drone: update ruby image
continuous-integration/drone/push Build is passing Details
2022-03-27 17:27:39 +02:00
Arthur POULET b9bc73b3b8 recalls: add a red pill to remind recalls to do
closes #61
2022-03-27 17:23:31 +02:00
Arthur POULET 8efe5d3ab3
offset: fix offset generator for yesterday 2022-03-19 11:13:47 +01:00
Arthur POULET db9c5cd7a4 prefs: add show full date checkbox
* factorize user prefs cookies
* add checkboxe
2022-02-28 23:47:14 +01:00
Arthur POULET 76da1fd1a8 systems: fix UserHelper inclusion 2022-02-28 22:56:26 +01:00
Arthur POULET 1f44afb893 layout: add the full date (today/yesterday) 2022-02-28 22:52:57 +01:00
Arthur POULET 0485f8131d
prefs: add offset as a user pref 2022-02-26 16:26:46 +01:00
Arthur POULET ac29e5bb41 preferences: add a page to edit user prefs
closes #63
2022-01-11 05:25:39 +01:00
Arthur POULET 690d41d33f csv: improve csv headers with category 2022-01-11 05:06:47 +01:00
Arthur POULET fef976ed5c csv: add csv export in /about 2022-01-05 23:02:25 +01:00
18 changed files with 287 additions and 104 deletions

View File

@ -3,7 +3,7 @@ name: default
steps:
- name: test
image: ruby:3.0
image: ruby:3.1
environment:
LIFEPEX_DB: "sqlite://test.db"
LIFEPEX_ENV: "test"

28
Gemfile
View File

@ -3,34 +3,34 @@
source "https://rubygems.org"
# web
gem "puma", "~> 5.3"
gem "sinatra", "~> 2.1"
gem "sinatra-contrib", "~> 2.1"
gem "slim", "~> 4.1"
gem "puma", "~> 5"
gem "sinatra", "~> 2"
gem "sinatra-contrib", "~> 2"
gem "slim", "~> 4"
# database
gem "sequel", "~> 5.43"
gem "sequel", "~> 5"
# you # comment the drivers you don't want
gem "sqlite3", "~> 1.4"
gem "sqlite3", "~> 1"
# gem "pg", "~> 1.2"
# security
gem "jwt", "~> 2.2"
gem "bcrypt", "~> 3.1"
gem "rack_csrf", "~> 2.6"
gem "jwt", "~> 2"
gem "bcrypt", "~> 3"
gem "rack_csrf", "~> 2"
# api tools
gem "doc_my_routes"
# debug and helpers
gem "colorize", "~> 0.8.1"
gem "activesupport", "= 6.1.3.1"
gem "colorize", "~> 0.8"
gem "activesupport", "~> 6"
# tests
group :test do
gem "pry", "~> 0.14.1"
gem "rack-test", "~> 1.1", require: false
gem "pry"
gem "rack-test", "~> 1", require: false
# gem "simplecov", "~> 0.21.2", require: false
end
gem "dotenv", "~> 2.7"
gem "dotenv", "~> 2"

View File

@ -1,83 +1,81 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.3.1)
activesupport (6.1.6.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
bcrypt (3.1.16)
bcrypt (3.1.18)
coderay (1.1.3)
colorize (0.8.1)
concurrent-ruby (1.1.9)
concurrent-ruby (1.1.10)
doc_my_routes (0.13.0)
dotenv (2.7.6)
i18n (1.8.10)
dotenv (2.8.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jwt (2.2.3)
jwt (2.4.1)
method_source (1.0.0)
minitest (5.14.4)
minitest (5.16.2)
multi_json (1.15.0)
mustermann (1.1.1)
mustermann (2.0.2)
ruby2_keywords (~> 0.0.1)
nio4r (2.5.7)
pg (1.2.3)
nio4r (2.5.8)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
puma (5.3.2)
puma (5.6.4)
nio4r (~> 2.0)
rack (2.2.3)
rack-protection (2.1.0)
rack (2.2.4)
rack-protection (2.2.2)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack_csrf (2.6.0)
rack (>= 1.1.0)
ruby2_keywords (0.0.4)
sequel (5.45.0)
sinatra (2.1.0)
mustermann (~> 1.0)
ruby2_keywords (0.0.5)
sequel (5.58.0)
sinatra (2.2.2)
mustermann (~> 2.0)
rack (~> 2.2)
rack-protection (= 2.1.0)
rack-protection (= 2.2.2)
tilt (~> 2.0)
sinatra-contrib (2.1.0)
sinatra-contrib (2.2.2)
multi_json
mustermann (~> 1.0)
rack-protection (= 2.1.0)
sinatra (= 2.1.0)
mustermann (~> 2.0)
rack-protection (= 2.2.2)
sinatra (= 2.2.2)
tilt (~> 2.0)
slim (4.1.0)
temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1)
sqlite3 (1.4.2)
sqlite3 (1.4.4)
temple (0.8.2)
tilt (2.0.10)
tzinfo (2.0.4)
tilt (2.0.11)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
zeitwerk (2.4.2)
zeitwerk (2.6.0)
PLATFORMS
x86_64-linux
DEPENDENCIES
activesupport (= 6.1.3.1)
bcrypt (~> 3.1)
colorize (~> 0.8.1)
activesupport (~> 6)
bcrypt (~> 3)
colorize (~> 0.8)
doc_my_routes
dotenv (~> 2.7)
jwt (~> 2.2)
pg (~> 1.2)
pry (~> 0.14.1)
puma (~> 5.3)
rack-test (~> 1.1)
rack_csrf (~> 2.6)
sequel (~> 5.43)
sinatra (~> 2.1)
sinatra-contrib (~> 2.1)
slim (~> 4.1)
sqlite3 (~> 1.4)
dotenv (~> 2)
jwt (~> 2)
pry
puma (~> 5)
rack-test (~> 1)
rack_csrf (~> 2)
sequel (~> 5)
sinatra (~> 2)
sinatra-contrib (~> 2)
slim (~> 4)
sqlite3 (~> 1)
BUNDLED WITH
2.2.16

View File

@ -95,3 +95,9 @@ LIFEPEX_ENV=test rake db:migrate
```
Then if you want to run the test, simply type `rake test` (you will need the startup env variable to be set first).
### Debug
Some debug options can be enabled with the env variable
`LIFEPEX_ENV=debug`

View File

@ -70,6 +70,10 @@ h2 {
height: 45px;
}
.smaller {
font-size: x-small !important;
}
.highcharts-figure, .highcharts-data-table table {
min-width: 360px;
max-width: 800px;

View File

@ -6,4 +6,18 @@ class LifePex::User < Sequel::Model(:users)
def password=(clear_password)
self.hashed_password = BCrypt::Password.create(clear_password)
end
def recalls_not_validated(cached: true)
@recalls_not_validated = nil if !cached
pexs = LifePex::Pex.setup_user_pexs(user_id: id, user_pexs: user_pexs)
@recalls_not_validated ||= recalls.filter do |recall|
pex = pexs[recall[:pex_id]]
validated = pex[:count_by_date].filter do |date, _|
date >= Date.today - recall[:span_duration]
end.values.sum
validated < recall[:repeated]
end
end
end

View File

@ -4,9 +4,9 @@ class LifePex::UserPex < Sequel::Model(:user_pexs)
def self.last_inserted_at(user_id)
LifePex::UserPex
.join(:pexs, :id => :pex_id)
.where(Sequel[:user_pexs][:user_id] => user_id)
.group(:pex_id)
.select_append{max(created_at).as(:last_inserted_at)}
# .join(:pexs, :id => :pex_id)
# .where(Sequel[:user_pexs][:user_id] => user_id)
# .group(:pex_id)
# .select_append{max(created_at).as(:last_inserted_at)}
end
end

View File

@ -1,11 +1,33 @@
class LifePex::Systems::AuthSystem < Sinatra::Base
helpers Sinatra::Cookies
include JSON::API
include LifePex::UsersHelper
def setup_user_cookie!(user_id)
response.set_cookie(
"auth",
{
value: JWT.encode({ "user_id" => user_id }, LifePex::SECRET),
expires: Time.now + 2.days,
path: "/",
})
end
def renew_user_cookie!
response.set_cookie(
"auth",
{
value: cookies["auth"],
expires: Time.now + 2.days,
path: "/",
})
end
def user_id_decoded(cookies = nil)
cookies = cookies() if cookies.nil?
begin
decoded = JWT.decode(cookies["auth"], LifePex::SECRET)
renew_user_cookie!
decoded[0]["user_id"]
rescue => err
STDERR.puts "user_id_decoded: #{err}"

View File

@ -1,32 +1,14 @@
require "date"
require "active_support/all"
require_relative "./auth.rb"
require_relative "./csrf.rb"
require_relative "../utils/users.rb"
class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
include JSON::API
include LifePex::Systems::CrlfHelper
def my_user_pexs(cookies, date = nil)
params = {
user_id: user_id_decoded(cookies),
}
params[:created_at] = date unless date.nil?
LifePex::UserPex.where(params).all()
end
def get_user_date
date = cookies["date"]
if date && Date.respond_to?(date)
Date.send date
elsif date == "yesterday"
Date.today - 1
else
Date.today
end
end
get "/today", auth: [] do
cookies.set "date", { value: "today", httponly: false }
cookies.set "date", { value: "now", httponly: false }
redirect "/"
end

View File

@ -25,7 +25,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
produces 'application/json'
status_codes [200]
parameter :pluck, required: false, type: 'string', in: 'query', description: 'improve performance by only fetching one field'
get '', auth: [], provides: 'json' do
get '', auth: [], provides: "json" do
pexs = current_user.pexs
pexs = pexs.select(json_params["pluck"]) if json_params["pluck"]
pexs.to_json

View File

@ -3,7 +3,6 @@ require_relative "./auth.rb"
require_relative "./csrf.rb"
class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
include JSON::API
include LifePex::Systems::CrlfHelper
DEFAULT_PEXS_FOR_NEW_USERS = YAML.load_file "config/default_pexs_for_new_users.yaml"
@ -12,14 +11,10 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
slim :login
end
def setup_user_cookie!(user_id)
cookies["auth"] = JWT.encode({ "user_id" => user_id }, LifePex::SECRET)
end
def login(params)
user = LifePex::User.where(username: params["username"]).first
if user && BCrypt::Password.new(user[:hashed_password]) == params["password"]
cookies["auth"] = JWT.encode({ "user_id" => user[:id] }, LifePex::SECRET)
setup_user_cookie!(user[:id])
user
else
nil
@ -28,7 +23,6 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
post "/login", provides: 'html' do
if user = login(params)
cookies["auth"] = JWT.encode({ "user_id" => user[:id] }, LifePex::SECRET)
redirect "/"
else
slim :login, locals: { flash: { danger: "Failed to login" } }
@ -89,6 +83,40 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
slim :about
end
get "/preferences" do
show_preferences
end
UserPreferenceCookie = Struct.new(:cookie, :allow_blank, :convert, :html, :description)
USER_PREFERENCES = {
"inputRecapDays" => UserPreferenceCookie.new("recap_days", false, :to_i, { type: "text" }, "Amount of days to show in the recap"),
"inputLateDayOffset" => UserPreferenceCookie.new("late_day_offset", false, :to_f, { type: "number", min: "0", max: "24", step: "0.1" }, "Offset for today (so a few hours after midnight is still today)"),
"inputShowFullDate" => UserPreferenceCookie.new("show_full_date", true, :to_s, { type: "checkbox" }, "Show or hide the date of the current tab"),
}
post "/preferences" do
USER_PREFERENCES.each do |param_name, upc|
current_param = params[param_name]
next if !upc.allow_blank && current_param.blank?
cookies[upc.cookie] =
case upc.convert
when Symbol
current_param.send(upc.convert)
when Proc
upc.convert.call(current_param)
else
current_param
end
end
show_preferences
end
def show_preferences
slim :preferences
end
extend DocMyRoutes::Annotatable
register Sinatra::Namespace
namespace '/api/user/v1' do

View File

@ -1,14 +1,57 @@
require_relative "./auth.rb"
require_relative "./api_response"
require "csv"
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
include JSON::API
include LifePex::Systems::ApiResponse
set :protection, :except => [:frame_options, :json_csrf]
extend DocMyRoutes::Annotatable # included by PexSystem
register Sinatra::Namespace # included by PexSystem
namespace "/api/user-pex/v1" do
summary "Export every single user pex and pex"
produces "application/json,application/csv"
status_codes [200]
# parameter :id, required: true, type: "integer", in: "path"
# parameter :date, required: true, type: "string", in: "path"
get "/export", auth: [], provides: %w(json application/csv) do
if accept? "application/csv"
export_csv
else
pexs = LifePex::Pex.where(user_id: current_user_id).map{ |pex| [pex, pex.user_pexs] }.to_h
api_response({ pexs: pexs, entity_type: "pexs" })
end
end
summary "Export every single user pex and pex as CSV"
produces "application/csv"
status_codes [200]
# parameter :id, required: true, type: "integer", in: "path"
# parameter :date, required: true, type: "string", in: "path"
get "/export.csv", auth: [], provides: %w(application/csv) do
export_csv
end
private def export_csv
pexs = LifePex::Pex.where(user_id: current_user_id).order(:id).all
pexs_names = pexs.map { |pex| "[#{pex.category}] #{pex.name}" }
pexs_ids = pexs.map(&:id)
user_pexs_by_date = LifePex::UserPex
.where(Sequel.qualify(:user_pexs, :user_id) => current_user_id)
.order_by(:created_at).all.group_by(&:created_at)
csv_output = CSV.generate do |csv|
csv << ["date", *pexs_names]
user_pexs_by_date.each do |date, user_pexs|
amount_by_column = user_pexs.group_by(&:pex_id).transform_values { |v| v.size }
user_pex_amount_by_date_all_column = pexs_ids.map { |id| amount_by_column[id] || 0 }
csv << [date, *user_pex_amount_by_date_all_column]
end
end
content_type "application/csv"
csv_output
end
namespace "/pexs" do
summary "Get the amount of user_pex for a given day"
@ -90,5 +133,6 @@ class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
end
end
include LifePex::Systems::ApiList
end

View File

@ -1,3 +1,5 @@
require "active_support/all"
module JSON::API
def json_params
begin
@ -10,18 +12,26 @@ module JSON::API
end
end
def date_input_convertor(date)
if date && Date.respond_to?(date)
Date.send date
elsif date == "yesterday"
Date.today - 1
else
begin
Date.parse date
rescue => _
Date.today
DATE_GENERATOR = {
"yesterday" => ->() { DateTime.now - 24.hours },
"now" => ->() { DateTime.now },
"today" => ->() { DateTime.now },
nil => ->() { DateTime.now },
}
# @param [String] date: either a date iso formatted or a word to be sent to DateTime
# @param [Float] offset: an amount of hours to remove from the date, useful for setting the start of the day hours after midnight
def date_input_convertor(date = "now", offset = 0.0)
base_time =
if DATE_GENERATOR.key?(date)
DATE_GENERATOR[date].call()
else
begin
DateTime.parse date
rescue => _
DateTime.now
end
end
end
(base_time - offset.hours).to_date
end
# params:

15
src/utils/users.rb Normal file
View File

@ -0,0 +1,15 @@
module LifePex::UsersHelper
def my_user_pexs(cookies, date = nil)
params = {
user_id: user_id_decoded(cookies),
}
params[:created_at] = date unless date.nil?
LifePex::UserPex.where(params).all()
end
def get_user_date
date = cookies["date"]
offset = cookies["late_day_offset"].to_f
date_input_convertor(date, offset)
end
end

View File

@ -17,7 +17,10 @@
I engage my honor to do never read or modify personnal data you may have put on
the server, and do my best to ensure its security.
You should look at the code source if you want to audit it.
br
h2
| GDPR
p
| In case you want to take a look at your data or want to delete it, I may in the futur
provide a feature for do it yourself.
Meanwhile you can still drop me an issue or a message on &nbsp;
@ -26,6 +29,8 @@
| &nbsp;
i
| #lifepex.
a.btn.btn-success(href="/api/user-pex/v1/export.csv")
| Export as CSV
h2
| Service version

View File

@ -20,9 +20,13 @@ html lang="en"
- if cookies["date"] == "yesterday"
a.navbar-brand href="/"
| Yesterday
- if cookies["show_full_date"] == "on"
.smaller= get_user_date
- else
a.navbar-brand href="/"
| Today
- if cookies["show_full_date"] == "on"
.smaller= get_user_date
button.navbar-toggler type="button" data-bs-toggle="collapse" data-bs-target="#navbar-collapser" aria-controls="navbar-collapser" aria-expanded="false" aria-label="Toggle navigation"
span.navbar-toggler-icon/
.collapse.navbar-collapse#navbar-collapser
@ -35,7 +39,11 @@ html lang="en"
li.nav-item
a.btn.btn-lg.btn-dark href="/yesterday" Yesterday
li.nav-item
a.btn.btn-lg.btn-dark href="/recap" Recap
a.btn.btn-lg.btn-dark.position-relative href="/recap"
| Recap
- if (recalls_count = current_user.recalls_not_validated.count) > 0
span.position-absolute.top-0.start-100.translate-middle.badge.rounded-pill.bg-danger
= recalls_count
li.nav-item
a.btn.btn-lg.btn-dark href="/achievements" Achievements
li.nav-item
@ -44,6 +52,8 @@ html lang="en"
a.btn.btn-lg.btn-dark href="/password" Change password
li.nav-item
a.btn.btn-lg.btn-dark href="/about" About lifepex
li.nav-item
a.btn.btn-lg.btn-dark href="/preferences" Profil preference
li.nav-item
a.btn.btn-lg.btn-dark href="/?filter_hidden=false" Show hidden
li.nav-item

View File

@ -0,0 +1,45 @@
.container
h1
| Profil preferences
h2
| Recap
form(method="POST")
== csrf_tag
/-
- USER_PREFERENCES.each do |param_name, upc|
- if upc.html[:type] == "checkbox"
.form-group.form-check.form-switch
label.col-sm-12.form-check-label for=param_name
strong=upc.description
- if cookies[upc.cookie] == "on"
input.form-check-input *upc.html name=param_name checked="on" /
- else
input.form-check-input *upc.html name=param_name /
- else
.form-group.row
label.col-sm-12.col-form-label for=param_name
strong=upc.description
input.form-control.form-control-lg *upc.html name=param_name value=cookies[upc.cookie] /
.form-group.row
input.btn.btn-lg.btn-block type="submit" value="Update"
- if LifePex::APP_ENV == "debug"
h2
| Debug
p
table
- cookies.each do |k, v|
tr
td= "cookies.#{k}"
td= v
tr
td
| get_user_date
td= get_user_date
h2
| Private data export
a.btn.btn-success(href="/api/user-pex/v1/export.csv")
| Export as CSV

View File

@ -26,7 +26,7 @@ h1 Recap
== csrf_tag
.form-group.row
label.col-form-label for="inputDaysAgo" Load how many days since today ?
input#inputDaysAgo.form-control.form-control-lg name="days_ago" type="number" min="3" max="365" step="1" value=(params["days_ago"] || "60") /
input#inputDaysAgo.form-control.form-control-lg name="days_ago" type="number" min="3" max="365" step="1" value=(params["days_ago"] || cookies["recap_days"] || "60") /
.form-group.row
p
input.btn.btn-lg.btn-block type="submit" value="Reload"