Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
35b69ef33a | |||
46d14c2917 | |||
3d607ade56 | |||
08a2729ce6 | |||
d4de167df2 | |||
29e78b7f52 | |||
5f8afb008e | |||
b9bc73b3b8 | |||
8efe5d3ab3 | |||
db9c5cd7a4 | |||
76da1fd1a8 | |||
1f44afb893 | |||
0485f8131d | |||
|
ac29e5bb41 | ||
|
690d41d33f | ||
|
fef976ed5c | ||
c2b5dd96b2 | |||
5a24cdb6b7 | |||
ab708456ed | |||
f2ad52586f | |||
d1fae9bf63 | |||
fb02993fa3 | |||
b36249ae76 | |||
2559c8c7a7 | |||
fd23535924 | |||
0e26243315 | |||
9315c422a0 | |||
08c895f3a5 | |||
52a8d4b5fe | |||
71e9d7c745 | |||
e3377979ed | |||
6f0877264f | |||
b9bd6674f6 | |||
51c513e358 | |||
fd71187d53 | |||
aa479693a3 | |||
3523add226 | |||
f6a951f704 | |||
531756e3b6 | |||
7eb4b680a5 | |||
3d7028e291 | |||
0f25ed78e0 | |||
612cee76a0 | |||
a9652a63a9 | |||
46d2d3c8d1 | |||
f350c56e9c | |||
1b4fac518e | |||
9cef2c7a59 | |||
9972bff604 | |||
6572a40a85 | |||
7c6d0307a0 | |||
d8dcd0f1e5 | |||
0a89fe1c36 | |||
a2eeff353a | |||
ff1853e01c | |||
548172b55f | |||
15a16e0110 | |||
ef61f05607 | |||
93a7c2dcf9 | |||
575e8450a4 | |||
a41c22b211 |
13
.drone.yml
Normal file
13
.drone.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: ruby:3.1
|
||||||
|
environment:
|
||||||
|
LIFEPEX_DB: "sqlite://test.db"
|
||||||
|
LIFEPEX_ENV: "test"
|
||||||
|
commands:
|
||||||
|
- bundle install --jobs=1 --retry=1
|
||||||
|
- rake db:migrate
|
||||||
|
- rake test
|
5
.env.sample
Normal file
5
.env.sample
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
LIFEPEX_DB=sqlite://sqlite.db
|
||||||
|
LIFEPEX_BIND=127.0.0.1
|
||||||
|
LIFEPEX_BASE_URL=
|
||||||
|
LIFEPEX_SECRET=
|
||||||
|
PORT=
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
*.db
|
*.db
|
||||||
|
coverage
|
||||||
|
.env.*local
|
||||||
|
|
37
Gemfile
37
Gemfile
|
@ -3,25 +3,34 @@
|
||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
# web
|
# web
|
||||||
gem "puma", "~> 5.3"
|
gem "puma", "~> 5"
|
||||||
gem "sinatra", "~> 2.1"
|
gem "sinatra", "~> 2"
|
||||||
gem "sinatra-contrib", "~> 2.1"
|
gem "sinatra-contrib", "~> 2"
|
||||||
gem "slim", "~> 4.1"
|
gem "slim", "~> 4"
|
||||||
|
|
||||||
# database
|
# database
|
||||||
# you # comment what you don't want
|
gem "sequel", "~> 5"
|
||||||
gem "sequel", "~> 5.43"
|
# you # comment the drivers you don't want
|
||||||
gem "sqlite3", "~> 1.4"
|
gem "sqlite3", "~> 1"
|
||||||
gem "pg", "~> 1.2"
|
# gem "pg", "~> 1.2"
|
||||||
|
|
||||||
# security
|
# security
|
||||||
gem "jwt", "~> 2.2"
|
gem "jwt", "~> 2"
|
||||||
gem "bcrypt", "~> 3.1"
|
gem "bcrypt", "~> 3"
|
||||||
gem "rack_csrf", "~> 2.6"
|
gem "rack_csrf", "~> 2"
|
||||||
|
|
||||||
# api tools
|
# api tools
|
||||||
gem "doc_my_routes"
|
gem "doc_my_routes"
|
||||||
|
|
||||||
# debug tools
|
# debug and helpers
|
||||||
gem "pry", "~> 0.14.1"
|
gem "colorize", "~> 0.8"
|
||||||
gem "colorize", "~> 0.8.1"
|
gem "activesupport", "~> 6"
|
||||||
|
|
||||||
|
# tests
|
||||||
|
group :test do
|
||||||
|
gem "pry"
|
||||||
|
gem "rack-test", "~> 1", require: false
|
||||||
|
# gem "simplecov", "~> 0.21.2", require: false
|
||||||
|
end
|
||||||
|
|
||||||
|
gem "dotenv", "~> 2"
|
||||||
|
|
79
Gemfile.lock
79
Gemfile.lock
|
@ -1,64 +1,81 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
bcrypt (3.1.16)
|
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.18)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
colorize (0.8.1)
|
colorize (0.8.1)
|
||||||
|
concurrent-ruby (1.1.10)
|
||||||
doc_my_routes (0.13.0)
|
doc_my_routes (0.13.0)
|
||||||
jwt (2.2.3)
|
dotenv (2.8.1)
|
||||||
|
i18n (1.12.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
jwt (2.4.1)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
|
minitest (5.16.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
mustermann (1.1.1)
|
mustermann (2.0.2)
|
||||||
ruby2_keywords (~> 0.0.1)
|
ruby2_keywords (~> 0.0.1)
|
||||||
nio4r (2.5.7)
|
nio4r (2.5.8)
|
||||||
pg (1.2.3)
|
|
||||||
pry (0.14.1)
|
pry (0.14.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
puma (5.3.2)
|
puma (5.6.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
rack (2.2.3)
|
rack (2.2.4)
|
||||||
rack-protection (2.1.0)
|
rack-protection (2.2.2)
|
||||||
rack
|
rack
|
||||||
|
rack-test (1.1.0)
|
||||||
|
rack (>= 1.0, < 3)
|
||||||
rack_csrf (2.6.0)
|
rack_csrf (2.6.0)
|
||||||
rack (>= 1.1.0)
|
rack (>= 1.1.0)
|
||||||
ruby2_keywords (0.0.4)
|
ruby2_keywords (0.0.5)
|
||||||
sequel (5.43.0)
|
sequel (5.58.0)
|
||||||
sinatra (2.1.0)
|
sinatra (2.2.2)
|
||||||
mustermann (~> 1.0)
|
mustermann (~> 2.0)
|
||||||
rack (~> 2.2)
|
rack (~> 2.2)
|
||||||
rack-protection (= 2.1.0)
|
rack-protection (= 2.2.2)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
sinatra-contrib (2.1.0)
|
sinatra-contrib (2.2.2)
|
||||||
multi_json
|
multi_json
|
||||||
mustermann (~> 1.0)
|
mustermann (~> 2.0)
|
||||||
rack-protection (= 2.1.0)
|
rack-protection (= 2.2.2)
|
||||||
sinatra (= 2.1.0)
|
sinatra (= 2.2.2)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
slim (4.1.0)
|
slim (4.1.0)
|
||||||
temple (>= 0.7.6, < 0.9)
|
temple (>= 0.7.6, < 0.9)
|
||||||
tilt (>= 2.0.6, < 2.1)
|
tilt (>= 2.0.6, < 2.1)
|
||||||
sqlite3 (1.4.2)
|
sqlite3 (1.4.4)
|
||||||
temple (0.8.2)
|
temple (0.8.2)
|
||||||
tilt (2.0.10)
|
tilt (2.0.11)
|
||||||
|
tzinfo (2.0.5)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
zeitwerk (2.6.0)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bcrypt (~> 3.1)
|
activesupport (~> 6)
|
||||||
colorize (~> 0.8.1)
|
bcrypt (~> 3)
|
||||||
|
colorize (~> 0.8)
|
||||||
doc_my_routes
|
doc_my_routes
|
||||||
jwt (~> 2.2)
|
dotenv (~> 2)
|
||||||
pg (~> 1.2)
|
jwt (~> 2)
|
||||||
pry (~> 0.14.1)
|
pry
|
||||||
puma (~> 5.3)
|
puma (~> 5)
|
||||||
rack_csrf (~> 2.6)
|
rack-test (~> 1)
|
||||||
sequel (~> 5.43)
|
rack_csrf (~> 2)
|
||||||
sinatra (~> 2.1)
|
sequel (~> 5)
|
||||||
sinatra-contrib (~> 2.1)
|
sinatra (~> 2)
|
||||||
slim (~> 4.1)
|
sinatra-contrib (~> 2)
|
||||||
sqlite3 (~> 1.4)
|
slim (~> 4)
|
||||||
|
sqlite3 (~> 1)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.16
|
2.2.16
|
||||||
|
|
111
README.md
111
README.md
|
@ -1,60 +1,103 @@
|
||||||
# LifePex
|
# LifePex
|
||||||
|
|
||||||
## Dependencies
|
[![Build Status](https://drone.sceptique.eu/api/badges/Sceptique/LifePex/status.svg)](https://drone.sceptique.eu/Sceptique/LifePex)
|
||||||
|
|
||||||
* git
|
## Install the software on a server
|
||||||
* ruby 3
|
|
||||||
* sqlite or postgresql
|
|
||||||
|
|
||||||
## Setup
|
### Install the dependencies
|
||||||
|
|
||||||
|
You should have the following systems installed: `git` `ruby 3 AND gems` `sqlite OR postgresql`
|
||||||
|
Git is required to clone the project and fetch the tags in order to expose them in the
|
||||||
|
webapp.
|
||||||
|
|
||||||
|
Then make sure you have installed bundler `gem install bundler` if not already done.
|
||||||
|
|
||||||
|
Then install the ruby dependencies.
|
||||||
|
|
||||||
```
|
```
|
||||||
# Install dependencies
|
|
||||||
bundle install
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
# SQLITE
|
### Configuration setup
|
||||||
./init/database.rb sqlite://sqlite.db
|
|
||||||
|
|
||||||
# POSTGRESQL
|
A sample file contains all the variables you will need in order to start the server.
|
||||||
|
This is the `.env.sample` file. You should copy it under the name `.env.local`.
|
||||||
|
|
||||||
|
* LIFEPEX_DB: is an url to your database. It may include a user/password
|
||||||
|
* LIFEPEX_BIND: what your socket will listen to (should probably be 127.0.0.1 or 0.0.0.0)
|
||||||
|
* LIFEPEX_BASE_URL: base url for the whole app (for local usage you can leave if empty)
|
||||||
|
* LIFEPEX_SECRET: you must put randomness in this variable to secure tokens and cookies
|
||||||
|
* LIFEPEX_ENV: may be empty, test, or production to avoid showing the stacktrace when there is a bug
|
||||||
|
|
||||||
|
### Database setup
|
||||||
|
|
||||||
|
The recommanded way is simply to init the database you configured with
|
||||||
|
|
||||||
|
```
|
||||||
|
./init/database.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have postgresql, you may want to create a specific user/password for this.
|
||||||
|
Since I suck with SGDB administration, this is a sample of what you may do.
|
||||||
|
|
||||||
|
**TODO: show how to create a simple attribute with full permission on this database**
|
||||||
|
|
||||||
|
```
|
||||||
psql -U postgres postgres -c "CREATE USER root WITH PASSWORD 'toor' SUPERUSER;"
|
psql -U postgres postgres -c "CREATE USER root WITH PASSWORD 'toor' SUPERUSER;"
|
||||||
psql -U postgres postgres -c "CREATE DATABASE life_pex"
|
psql -U postgres postgres -c "CREATE DATABASE life_pex"
|
||||||
./init/database.rb postgres://root:toor@localhost/life_pex
|
echo LIFEPEX_DB=postgres://root:toor@localhost/life_pex >> .env.local
|
||||||
|
rake db:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
## Start
|
### Start
|
||||||
|
|
||||||
|
* configuration: check
|
||||||
|
* setup dependencies: check
|
||||||
|
* setup database: check
|
||||||
|
|
||||||
|
You can simply start the application.
|
||||||
|
|
||||||
```
|
```
|
||||||
export LIFEPEX_SECRET="put something random here"
|
|
||||||
export LIFEPEX_BASE_URL="https://mydomain/base"
|
|
||||||
export LIFEPEX_DB="sqlite://sqlite.db"
|
|
||||||
# you may also want to use postgres or something
|
|
||||||
# export LIFEPEX_DB="postgres://root:toor@localhost/life_pex"
|
|
||||||
# export LIFEPEX_BIND="0.0.0.0" # you may want to bind against something else than 127.0.01
|
|
||||||
./src/app.rb
|
./src/app.rb
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate documentation
|
### Service, update, etc.
|
||||||
|
|
||||||
|
You may want to take a look at the `scripts/` directory which contains tons of config helper, for nginx, systemd, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
![recap image](https://git.sceptique.eu/attachments/ce7c2fab-bd1b-43fc-b10a-21b2c953e2c2)
|
||||||
|
![today image](https://git.sceptique.eu/attachments/ecd7a36d-eac7-40aa-b973-c6ddbac87f51)
|
||||||
|
|
||||||
|
## Developers
|
||||||
|
|
||||||
|
### Generate documentation
|
||||||
|
|
||||||
```
|
```
|
||||||
./init/doc.rb
|
./init/doc.rb
|
||||||
xdg-open ./public/doc/index.html
|
xdg-open ./public/doc/index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
### Testing
|
||||||
|
|
||||||
* [x] Register an account
|
Generate first a specific configuration file
|
||||||
* [x] Reset password
|
|
||||||
* [x] Add pex models
|
|
||||||
* [x] Add point each day to each pex model
|
|
||||||
* [x] Make a nice smooth level function
|
|
||||||
* [x] Create a nice CSS template to make it dark and flashy and cyberpunk kikoo
|
|
||||||
* [x] Protect privacy with private pex models
|
|
||||||
* [x] Import CSV script
|
|
||||||
* [x] Make a basic recap graph
|
|
||||||
* [x] Make a niice recap graph
|
|
||||||
* [x] Add an easy and quick way to edit pex
|
|
||||||
* [x] Add advanced pex with variable value
|
|
||||||
* [ ] Improve security (token validity limit, random seed warning, ...)
|
|
||||||
|
|
||||||
![recap image](https://git.sceptique.eu/attachments/ce7c2fab-bd1b-43fc-b10a-21b2c953e2c2)
|
```
|
||||||
![today image](https://git.sceptique.eu/attachments/ecd7a36d-eac7-40aa-b973-c6ddbac87f51)
|
cp .env.local .env.test.local
|
||||||
|
editor .env.test.local # don't forget to set a new database !!!
|
||||||
|
```
|
||||||
|
|
||||||
|
Then init the database
|
||||||
|
|
||||||
|
```
|
||||||
|
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`
|
||||||
|
|
21
Rakefile
Normal file
21
Rakefile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require "rake"
|
||||||
|
require "rake/testtask"
|
||||||
|
|
||||||
|
Rake::TestTask.new do |t|
|
||||||
|
t.pattern = "test/*_test.rb"
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace "db" do
|
||||||
|
desc "Migrate the database to the lasted schema"
|
||||||
|
task "migrate" do
|
||||||
|
require_relative "./init/migrate_db"
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Reset all tables, schema, data"
|
||||||
|
task "reset" do
|
||||||
|
require_relative "./init/load_env"
|
||||||
|
DB.tables.each {|t| DB.drop_table t }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
task default: :test
|
13
init/load_env.rb
Normal file
13
init/load_env.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
require "sequel"
|
||||||
|
require "colorize"
|
||||||
|
|
||||||
|
require_relative "../src/utils/env"
|
||||||
|
require_relative "../src/utils/semver"
|
||||||
|
|
||||||
|
load_dotenv
|
||||||
|
DB = Sequel.connect ENV["LIFEPEX_DB"]
|
||||||
|
|
||||||
|
if ENV["environment"] == "debug"
|
||||||
|
require "pry"
|
||||||
|
binding.pry
|
||||||
|
end
|
|
@ -1,38 +1,30 @@
|
||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
require "sequel"
|
require_relative "./load_env"
|
||||||
require "colorize"
|
|
||||||
require_relative "../src/utils/semver.rb"
|
|
||||||
|
|
||||||
DB = Sequel.connect ARGV[0]
|
|
||||||
|
|
||||||
if ARGV[1] == "debug"
|
|
||||||
require "pry"
|
|
||||||
binding.pry
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_version
|
def current_version
|
||||||
DB[:meta].first[:version] rescue 0
|
DB[:meta].first[:version] rescue 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def migrate(version, &block)
|
def migrate(version, message = nil, &block)
|
||||||
puts "Check migration #{version}"
|
puts "Check migration #{version}".on_blue
|
||||||
if current_version < version
|
if current_version < version
|
||||||
puts "Migrate #{version}".blue
|
puts "Migrate #{version}".blue
|
||||||
begin
|
begin
|
||||||
yield
|
yield
|
||||||
DB[:meta].update(version: version)
|
DB[:meta].update(version: version)
|
||||||
puts "Successfuly set version #{version}"
|
puts "Successfuly set version #{version}".green
|
||||||
|
puts message.green if message
|
||||||
rescue => err
|
rescue => err
|
||||||
puts err.message.red
|
puts err.message.on_red
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
puts "Already migrated #{version}".blue
|
puts "Already migrated #{version}".yellow
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 1 do
|
migrate 1, "Initialized database" do
|
||||||
DB.create_table :meta do
|
DB.create_table :meta do
|
||||||
primary_key :id
|
primary_key :id
|
||||||
Int :version
|
Int :version
|
||||||
|
@ -57,33 +49,29 @@ migrate 1 do
|
||||||
String :username
|
String :username
|
||||||
String :hashed_password
|
String :hashed_password
|
||||||
end rescue puts "users already exists".yellow
|
end rescue puts "users already exists".yellow
|
||||||
puts "Initialized database".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 2 do
|
migrate 2, "Add pex categories" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
add_column :category, String
|
add_column :category, String
|
||||||
end rescue puts "pexs.category already exists".yellow
|
end rescue puts "pexs.category already exists".yellow
|
||||||
puts "Migrated categories".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 3 do
|
migrate 3, "Add pex user belonging" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
add_column :user_id, :Int
|
add_column :user_id, :Int
|
||||||
end rescue puts "pexs.user_id already exists".yellow
|
end rescue puts "pexs.user_id already exists".yellow
|
||||||
DB[:pexs].update(user_id: 1)
|
DB[:pexs].update(user_id: 1)
|
||||||
puts "Migrated pex belonging to user".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 4 do
|
migrate 4, "Fix pex category default behavior" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
set_column_default(:category, '')
|
set_column_default(:category, '')
|
||||||
end rescue puts "pexs.category default already exists".yellow
|
end rescue puts "pexs.category default already exists".yellow
|
||||||
DB[:pexs].where(category: nil).update(category: '')
|
DB[:pexs].where(category: nil).update(category: '')
|
||||||
puts "Migrated pex default category".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 5 do
|
migrate 5, "Add meta schema" do
|
||||||
DB.alter_table :meta do
|
DB.alter_table :meta do
|
||||||
add_column :code_version, String
|
add_column :code_version, String
|
||||||
end rescue puts "meta.code_version already exists".yellow
|
end rescue puts "meta.code_version already exists".yellow
|
||||||
|
@ -96,18 +84,20 @@ migrate 6 do
|
||||||
end rescue puts "meta.code_date already exists".yellow
|
end rescue puts "meta.code_date already exists".yellow
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 7 do
|
migrate 7, "Fix pex initial category" do
|
||||||
DB[:pexs].each { |pex| DB[:pexs].where(id: pex[:id]).update(category: pex[:category].to_s.downcase) }
|
DB[:pexs].each do |pex|
|
||||||
|
DB[:pexs].where(id: pex[:id]).update(category: pex[:category].to_s.downcase)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 8 do
|
migrate 8, "Add default hidden for pexs" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
add_column :hidden, TrueClass
|
add_column :hidden, TrueClass
|
||||||
end rescue puts "pex.hidden already exists".yellow
|
end rescue puts "pex.hidden already exists".yellow
|
||||||
DB[:pexs].each { |pex| DB[:pexs].where(id: pex[:id]).update(hidden: false) }
|
DB[:pexs].each { |pex| DB[:pexs].where(id: pex[:id]).update(hidden: false) }
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 9 do
|
migrate 9, "Add achievements" do
|
||||||
DB.create_table :achievements do
|
DB.create_table :achievements do
|
||||||
primary_key :id
|
primary_key :id
|
||||||
Int :user_id
|
Int :user_id
|
||||||
|
@ -118,14 +108,25 @@ migrate 9 do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
migrate 10 do
|
migrate 10, "Add generic flag to pexs, for bookmarking" do
|
||||||
DB.alter_table :pexs do
|
DB.alter_table :pexs do
|
||||||
add_column :flag, :String
|
add_column :flag, :String
|
||||||
end rescue puts "pexs.flag already exists".yellow
|
end rescue puts "pexs.flag already exists".yellow
|
||||||
puts "Migrated pex flag for bookmarks".green
|
|
||||||
end
|
end
|
||||||
|
|
||||||
puts "End migration".green
|
migrate 11, "Add recalls" do
|
||||||
|
DB.create_table :recalls do
|
||||||
|
primary_key :id
|
||||||
|
Int :user_id
|
||||||
|
Int :pex_id
|
||||||
|
String :name
|
||||||
|
Int :span_duration
|
||||||
|
Int :repeated
|
||||||
|
end
|
||||||
|
puts "Migrated recalls".green
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "End migration".on_blue
|
||||||
CODE_VERSION = `git tag`.split("\n").map{ |str| Utils::Semver.new(str) }.sort.last.to_s
|
CODE_VERSION = `git tag`.split("\n").map{ |str| Utils::Semver.new(str) }.sort.last.to_s
|
||||||
CODE_DATE=`git show #{CODE_VERSION} --pretty="format:%as"`.split("\n").first
|
CODE_DATE=`git show #{CODE_VERSION} --pretty="format:%as"`.split("\n").first
|
||||||
DB[:meta].update(code_version: CODE_VERSION)
|
DB[:meta].update(code_version: CODE_VERSION)
|
|
@ -70,6 +70,10 @@ h2 {
|
||||||
height: 45px;
|
height: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smaller {
|
||||||
|
font-size: x-small !important;
|
||||||
|
}
|
||||||
|
|
||||||
.highcharts-figure, .highcharts-data-table table {
|
.highcharts-figure, .highcharts-data-table table {
|
||||||
min-width: 360px;
|
min-width: 360px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|
34
public/js/ajax.js
Normal file
34
public/js/ajax.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
async function ajax({
|
||||||
|
method = "GET",
|
||||||
|
url = ".",
|
||||||
|
body = undefined,
|
||||||
|
headers = {},
|
||||||
|
on_success = () => {},
|
||||||
|
on_failure = () => {},
|
||||||
|
}) {
|
||||||
|
const xhttp = new XMLHttpRequest();
|
||||||
|
const return_on_sent = new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
xhttp.onreadystatechange = () => {
|
||||||
|
if (xhttp.readyState == 4) {
|
||||||
|
const response_status = String(xhttp.status);
|
||||||
|
if (/2\d\d/.test(response_status)) {
|
||||||
|
resolve(on_success(xhttp.responseText, xhttp));
|
||||||
|
} else {
|
||||||
|
resolve(on_failure(xhttp.responseText, xhttp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhttp.open(method, url, true);
|
||||||
|
Object.keys(headers).forEach((header_key) => {
|
||||||
|
xhttp.setRequestHeader(header_key, headers[header_key]);
|
||||||
|
});
|
||||||
|
xhttp.send(body);
|
||||||
|
|
||||||
|
return return_on_sent;
|
||||||
|
}
|
11
public/js/error.js
Normal file
11
public/js/error.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
function flashError(message) {
|
||||||
|
const new_flash_error = document.createElement('p');
|
||||||
|
new_flash_error.classList.add("alert");
|
||||||
|
new_flash_error.classList.add("alert-danger");
|
||||||
|
new_flash_error.classList.add("alert-dismissible");
|
||||||
|
new_flash_error.classList.add("fade");
|
||||||
|
new_flash_error.classList.add("show");
|
||||||
|
new_flash_error.setAttribute("role", "alert");
|
||||||
|
new_flash_error.innerHTML = `${message} <button class=\"btn-close\" type=\"button\" data-bs-dismiss=\"alert\" aria-label=\"Close\" />`;
|
||||||
|
const flash = document.querySelectorAll('.flash')[0].appendChild(new_flash_error);
|
||||||
|
}
|
|
@ -17,16 +17,99 @@ function toggle(node) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", (_event) => {
|
Array.toObject = function (arr) {
|
||||||
const togglers = document.querySelectorAll('.pex-editor-toggler');
|
return arr.reduce((base, current) => {
|
||||||
|
base[current[0]] = current[1];
|
||||||
|
return base;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
togglers.forEach((t) => {
|
function __map__(cb = (e) => e) {
|
||||||
const name = t.attributes.name.value;
|
const arr = [];
|
||||||
const pex_editor = document.querySelector(`.pex-editor[name="${name}"]`);
|
for (i = 0; i < this.length; i++) {
|
||||||
hide(pex_editor);
|
arr.push(cb(this[i]));
|
||||||
show(t);
|
}
|
||||||
t.addEventListener("click", (event) => {
|
return arr;
|
||||||
toggle(pex_editor);
|
}
|
||||||
});
|
|
||||||
|
HTMLCollection.prototype.map = __map__;
|
||||||
|
NodeList.prototype.map = __map__;
|
||||||
|
NamedNodeMap.prototype.map = __map__;
|
||||||
|
HTMLInputElement.prototype.attributesObject = function () {
|
||||||
|
return Array.toObject(this.attributes.map((attr) => [attr.name, attr.value]));
|
||||||
|
};
|
||||||
|
HTMLFormElement.prototype.attributesObject = function () {
|
||||||
|
return Array.toObject(this.attributes.map((attr) => [attr.name, attr.value]));
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseCookie(str) {
|
||||||
|
return str
|
||||||
|
.split(";")
|
||||||
|
.map((v) => v.split("="))
|
||||||
|
.reduce((acc, v) => {
|
||||||
|
if (v.length == 2) acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function userpexValidation(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const attributes = event.target.attributesObject();
|
||||||
|
|
||||||
|
const fields = event.target.childNodes.map().filter(e => e.nodeName == 'INPUT');
|
||||||
|
const fields_attributes = Array.toObject(fields.map(attr => [attr.name, attr.value]));
|
||||||
|
const date = parseCookie(document.cookie).date || new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
ajax({
|
||||||
|
method: fields_attributes["type"] == "-" ? "DELETE" : "POST",
|
||||||
|
url: `/api/user-pex/v1/pexs/${fields_attributes["id"]}/validation`,
|
||||||
|
body: JSON.stringify({ date: date, force_count_total: true }),
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||||
|
on_success: (body) => {
|
||||||
|
// === Sample how to make dynamic partial replacement ===
|
||||||
|
// const target_line = event.target.parentNode.parentElement;
|
||||||
|
// ajax({
|
||||||
|
// method: 'GET',
|
||||||
|
// url: `/api/pex/v2/pexs/${fields_attributes["id"]}/more`,
|
||||||
|
// headers: { Accept: "text/html;*/*" },
|
||||||
|
// on_success: (body) => {
|
||||||
|
// target_line.innerHTML = body;
|
||||||
|
// const toggler = target_line.querySelector('.pex-editor-toggler');
|
||||||
|
// setupPexEditorToggler(toggler);
|
||||||
|
// },
|
||||||
|
// on_failure: (body, req) => {
|
||||||
|
// flashError("Error JS#0002 while rendering...");
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
const json_body = JSON.parse(body);
|
||||||
|
const text_target = event.target.parentNode.parentElement.childNodes[3];
|
||||||
|
text_target.textContent = String(json_body.count_total);
|
||||||
|
const should_hide_decrease_button = json_body.count_total == 0;
|
||||||
|
event.target.parentNode.parentNode.childNodes[2].childNodes[0].hidden = should_hide_decrease_button;
|
||||||
|
},
|
||||||
|
on_failure: (body, req) => {
|
||||||
|
flashError("Error JS#0001 while validating...");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupPexEditorToggler(toggler) {
|
||||||
|
const name = toggler.attributes.name.value;
|
||||||
|
const pex_editor = document.querySelector(`.pex-editor[name="${name}"]`);
|
||||||
|
hide(pex_editor);
|
||||||
|
show(toggler);
|
||||||
|
toggler.addEventListener("click", (event) => {
|
||||||
|
toggle(pex_editor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", (_event) => {
|
||||||
|
const editor_togglers = document.querySelectorAll('.pex-editor-toggler');
|
||||||
|
editor_togglers.forEach(setupPexEditorToggler);
|
||||||
|
|
||||||
|
const userpexvalidation0 = document.querySelectorAll('.userpexvalidationvalue').map().filter(tag => tag.textContent == "0");
|
||||||
|
userpexvalidation0.forEach((tag) => {
|
||||||
|
tag.parentNode.parentNode.childNodes[2].childNodes[0].hidden = true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
function setupChart(...cumuls) {
|
function setupChart({ all_dates }, ...cumuls) {
|
||||||
const chart = Highcharts.chart('recap-xp-container', {
|
const chart = Highcharts.chart('recap-xp-container', {
|
||||||
chart: {
|
chart: {
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
|
@ -7,24 +7,53 @@ function setupChart(...cumuls) {
|
||||||
text: '',
|
text: '',
|
||||||
color: '#55f5f5',
|
color: '#55f5f5',
|
||||||
},
|
},
|
||||||
series: cumuls.map(cumul => ({
|
xAxis: {
|
||||||
|
categories: all_dates,
|
||||||
|
},
|
||||||
|
series: cumuls.map((cumul, idx) => ({
|
||||||
// color: '#55f5f5',
|
// color: '#55f5f5',
|
||||||
|
visible: idx != 0,
|
||||||
name: cumul.name,
|
name: cumul.name,
|
||||||
data: cumul.data,
|
data: cumul.data,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
function objectToQueryParam(obj) {
|
||||||
const xhttp = new XMLHttpRequest();
|
return Object.entries(obj).map(tuple => tuple.join("=")).join("&");
|
||||||
xhttp.onreadystatechange = () => {
|
}
|
||||||
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
|
||||||
const json_output = JSON.parse(xhttp.responseText);
|
|
||||||
setupChart(...json_output.pex_tables);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhttp.open("GET", "/api/pex/v1/recap", true);
|
function urlWithQueryParams(base, object_with_params) {
|
||||||
xhttp.setRequestHeader("Accept", "application/json");
|
if (Object.keys(object_with_params).length > 0) {
|
||||||
xhttp.send();
|
return `${base}?${objectToQueryParam(object_with_params)}`;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestChart() {
|
||||||
|
let url = "/api/pex/v1/recap";
|
||||||
|
const days_ago = document.querySelector("#inputDaysAgo").value;
|
||||||
|
if (days_ago.length > 0) {
|
||||||
|
url = urlWithQueryParams(url, { days_ago });
|
||||||
|
}
|
||||||
|
ajax({
|
||||||
|
method: "GET",
|
||||||
|
url: url,
|
||||||
|
body: null,
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
on_success: (body) => {
|
||||||
|
const json_output = JSON.parse(body);
|
||||||
|
setupChart({ all_dates: json_output.all_dates }, ...json_output.pex_tables);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async function () {
|
||||||
|
requestChart();
|
||||||
|
|
||||||
|
const form = document.querySelector(".recap-xp form#reloader");
|
||||||
|
form.addEventListener("submit", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
requestChart();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
export LIFEPEX_SECRET=update_life_pex
|
# Your env should already be setup before you run this
|
||||||
|
|
||||||
cd /srv/LifePex/
|
cd /srv/LifePex/
|
||||||
git checkout master
|
git checkout master
|
||||||
git fetch origin
|
git fetch origin
|
||||||
git reset --hard origin/master
|
git reset --hard origin/master --tags
|
||||||
bundle install
|
bundle install
|
||||||
./init/database.rb sqlite://sqlite.db
|
./init/database.rb
|
||||||
mkdir -p ./public/doc
|
mkdir -p ./public/doc
|
||||||
./init/doc.rb
|
./init/doc.rb
|
||||||
sudo systemctl restart lifepex
|
sudo systemctl restart lifepex
|
||||||
|
|
|
@ -39,23 +39,33 @@ class LifePex::AchievementDSL::Success
|
||||||
# end
|
# end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def static_cast_parameter_value(value: "", type: :String)
|
||||||
|
if type == :Integer
|
||||||
|
Integer(value)
|
||||||
|
elsif type == :Number || type == :Float
|
||||||
|
Float(value)
|
||||||
|
elsif type == :ArrayOfString
|
||||||
|
value.split(',')
|
||||||
|
elsif type == :ArrayOfInteger
|
||||||
|
raise "Invalid ArrayOfInteger" unless value =~ /^((\d+){1}?,?)*$/
|
||||||
|
value.split(',').map(&:to_i)
|
||||||
|
elsif type == :ArrayOfNumber
|
||||||
|
raise "Invalid ArrayOfNumber" unless value =~ /^((\d+(\.\d+)?){1}?,?)*$/
|
||||||
|
value.split(',').map(&:to_f)
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def format_parameters_hash(parameters)
|
def format_parameters_hash(parameters)
|
||||||
parameters.map do |key, value|
|
parameters.map do |key, value|
|
||||||
parameter = @parameters.find { |param| param.name == key }
|
parameter = @parameters.find { |param| param.name == key }
|
||||||
raise RuntimeError.new "No #{key} parameter found" if parameter.nil?
|
raise RuntimeError.new "No #{key} parameter found" if parameter.nil?
|
||||||
typed_value = if parameter.type == :Integer
|
begin
|
||||||
value.to_i
|
typed_value = static_cast_parameter_value(value: value, type: parameter.type)
|
||||||
elsif parameter.type == :Number
|
rescue => err
|
||||||
value.to_f
|
raise "Invalid value \"#{value}\" for type #{parameter.type} of parameter \"#{parameter.name}\""
|
||||||
elsif parameter.type == :Array
|
end
|
||||||
value.split(',')
|
|
||||||
elsif parameter.type == :ArrayOfNumber
|
|
||||||
value.split(',').map(&:to_f)
|
|
||||||
elsif parameter.type == :ArrayOfInteger
|
|
||||||
value.split(',').map(&:to_i)
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
[key, typed_value]
|
[key, typed_value]
|
||||||
end.to_h
|
end.to_h
|
||||||
end
|
end
|
||||||
|
|
112
src/app.rb
112
src/app.rb
|
@ -1,66 +1,75 @@
|
||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
require "pry"
|
module LifePex
|
||||||
# require "sinatra"
|
end
|
||||||
|
|
||||||
|
require "sinatra/base" # web
|
||||||
require "sinatra/cookies"
|
require "sinatra/cookies"
|
||||||
require "sinatra/base"
|
|
||||||
require "sinatra/namespace"
|
|
||||||
require "slim"
|
require "slim"
|
||||||
require "bcrypt"
|
require "sinatra/namespace" # api
|
||||||
|
require "doc_my_routes" # api doc
|
||||||
|
require "bcrypt" # security
|
||||||
require "jwt"
|
require "jwt"
|
||||||
require "sequel"
|
|
||||||
require "json"
|
|
||||||
require "doc_my_routes"
|
|
||||||
require "rack/csrf"
|
require "rack/csrf"
|
||||||
require "securerandom"
|
require "securerandom"
|
||||||
|
require "sequel" # db
|
||||||
|
require "json" # helpers
|
||||||
|
require "active_support"
|
||||||
|
require "active_support/core_ext"
|
||||||
|
require "pry" # debug
|
||||||
|
|
||||||
require_relative "./utils/url.rb"
|
# Load environment variables
|
||||||
|
require_relative "./utils/env"
|
||||||
module LifePex
|
module LifePex
|
||||||
DB = Sequel.connect ARGV[0] || ENV["LIFEPEX_DB"] || "sqlite://sqlite.db"
|
APP_ENV = load_dotenv
|
||||||
BASE_URL = ARGV[1] || ENV["LIFEPEX_BASE_URL"] || "http://localhost:4567"
|
end
|
||||||
SECRET = ENV.fetch('LIFEPEX_SECRET') do
|
|
||||||
STDERR.puts "WARNING: LIFEPEX_SECRET will be randomly generated as a fallback"
|
|
||||||
SecureRandom.hex(64)
|
|
||||||
end
|
|
||||||
if SECRET.size < 8
|
|
||||||
STDERR.puts "ERROR: Your secret is NOT very secret, fix this (at least 8 hexadigits entropy)"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
|
# Initialize framework
|
||||||
|
require_relative "./utils/url.rb"
|
||||||
|
require_relative "./utils/boot_framework"
|
||||||
|
LifePex::BootFramework.boot_application do
|
||||||
|
harshly_need_env "LIFEPEX_DB"
|
||||||
|
kindly_ask_env "LIFEPEX_BASE_URL"
|
||||||
|
kindly_ask_env "LIFEPEX_SECRET" do
|
||||||
|
warning "\"LIFEPEX_SECRET\" will be randomly generated as a fallback"
|
||||||
|
ENV["LIFEPEX_SECRET"] = SecureRandom.hex(64)
|
||||||
|
end
|
||||||
|
harshly_do env["LIFEPEX_SECRET"].size < 8 do
|
||||||
|
error "Your secret is NOT very secret, fix this (at least 8 hexadigits entropy)"
|
||||||
|
end
|
||||||
|
end.finish!
|
||||||
|
|
||||||
|
# Setup module variables
|
||||||
|
module LifePex
|
||||||
|
DB = Sequel.connect ENV["LIFEPEX_DB"]
|
||||||
|
BASE_URL = ENV["LIFEPEX_BASE_URL"] || "http://localhost:4567"
|
||||||
|
SECRET = ENV["LIFEPEX_SECRET"]
|
||||||
CODE_VERSION = DB[:meta].first[:code_version]
|
CODE_VERSION = DB[:meta].first[:code_version]
|
||||||
CODE_DATE = DB[:meta].first[:code_date]
|
CODE_DATE = DB[:meta].first[:code_date]
|
||||||
|
|
||||||
include LifePex::Utils::Url
|
include LifePex::Utils::Url
|
||||||
|
|
||||||
module Systems
|
module Systems
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require_relative "./utils/json_api.rb"
|
# Then we load all the systems
|
||||||
require_relative "./utils/string.rb"
|
|
||||||
|
Dir[File.join(__dir__, 'utils', '*.rb')].each { |file| require file }
|
||||||
|
|
||||||
require_relative "./achievements/dsl.rb"
|
require_relative "./achievements/dsl.rb"
|
||||||
require_relative "./achievements/achievement.rb"
|
require_relative "./achievements/achievement.rb"
|
||||||
|
|
||||||
require_relative "./models/level.rb"
|
Dir[File.join(__dir__, 'models', '*.rb')].each { |file| require file }
|
||||||
require_relative "./models/user.rb"
|
Dir[File.join(__dir__, 'systems', '*.rb')].each { |file| require file }
|
||||||
require_relative "./models/pex.rb"
|
|
||||||
require_relative "./models/user_pex.rb"
|
|
||||||
require_relative "./models/achievement.rb"
|
|
||||||
|
|
||||||
require_relative "./systems/csrf.rb"
|
|
||||||
require_relative "./systems/api_list.rb"
|
|
||||||
require_relative "./systems/auth.rb"
|
|
||||||
require_relative "./systems/user.rb"
|
|
||||||
require_relative "./systems/pex.rb"
|
|
||||||
require_relative "./systems/pex2.rb"
|
|
||||||
require_relative "./systems/achievement.rb"
|
|
||||||
|
|
||||||
|
# Static file serving in this file because it is overkill to create a file for this
|
||||||
class LifePex::Systems::PublicSystem < Sinatra::Base
|
class LifePex::Systems::PublicSystem < Sinatra::Base
|
||||||
set :public_folder, 'public'
|
set :public_folder, "public"
|
||||||
end
|
end
|
||||||
|
|
||||||
class LifePex::Systems::BaseSystem < Sinatra::Base
|
# Main app
|
||||||
|
class LifePex::App < Sinatra::Base
|
||||||
DocMyRoutes.configure do |config|
|
DocMyRoutes.configure do |config|
|
||||||
config.title = "LifePex"
|
config.title = "LifePex"
|
||||||
config.description = "LifePex JSON REST API documentation"
|
config.description = "LifePex JSON REST API documentation"
|
||||||
|
@ -69,14 +78,23 @@ class LifePex::Systems::BaseSystem < Sinatra::Base
|
||||||
set :session_secret, LifePex::SECRET
|
set :session_secret, LifePex::SECRET
|
||||||
enable :sessions
|
enable :sessions
|
||||||
|
|
||||||
use Rack::Csrf, :skip => ['*:/api/*']
|
# use Rack::Csrf, skip: ["POST:/api/*"], :raise => (LifePex::APP_ENV != "production") if LifePex::APP_ENV != "test"
|
||||||
|
set :protection, :except => :json_csrf if LifePex::APP_ENV != "test"
|
||||||
|
|
||||||
use LifePex::Systems::AuthSystem
|
LifePex::Systems.constants
|
||||||
use LifePex::Systems::PublicSystem
|
.filter { |system| system.to_s =~ /System$/ }
|
||||||
use LifePex::Systems::UserSystem
|
.each { |system|
|
||||||
use LifePex::Systems::PexSystem
|
use LifePex::Systems.const_get(system)
|
||||||
use LifePex::Systems::Pex2System
|
puts "Loaded #{system.to_s.green}"
|
||||||
use LifePex::Systems::Achievement
|
}
|
||||||
|
|
||||||
|
# use LifePex::Systems::AuthSystem
|
||||||
|
# use LifePex::Systems::PublicSystem
|
||||||
|
# use LifePex::Systems::UserSystem
|
||||||
|
# use LifePex::Systems::PexSystem
|
||||||
|
# use LifePex::Systems::Pex2System
|
||||||
|
# use LifePex::Systems::AchievementSystem
|
||||||
|
# use LifePex::Systems::UserPexSystem
|
||||||
|
|
||||||
include JSON::API
|
include JSON::API
|
||||||
not_found do
|
not_found do
|
||||||
|
@ -87,10 +105,14 @@ class LifePex::Systems::BaseSystem < Sinatra::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/meta/v1", provides: 'json' do
|
get "/api/meta/v1", provides: "json" do
|
||||||
LifePex::Systems::ApiList.get_all_api_routes.to_json
|
LifePex::Systems::ApiList.get_all_api_routes.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
set :bind, ENV["LIFEPEX_BIND"] || "127.0.0.1"
|
set :bind, ENV["LIFEPEX_BIND"] || "127.0.0.1"
|
||||||
|
|
||||||
|
set :environment, LifePex::APP_ENV
|
||||||
|
ENV["RACK_ENV"] = LifePex::APP_ENV
|
||||||
|
|
||||||
run! if app_file == $0
|
run! if app_file == $0
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
class LifePex::Pex < Sequel::Model(LifePex::DB[:pexs].order(:category, :name))
|
class LifePex::Pex < Sequel::Model(LifePex::DB[:pexs].order(:category, :name))
|
||||||
one_to_many :user_pexs
|
one_to_many :user_pexs
|
||||||
many_to_one :user
|
many_to_one :user
|
||||||
|
one_to_many :recalls
|
||||||
|
|
||||||
# note: wont work on #update
|
# note: wont work on #update
|
||||||
def before_validation
|
def before_validation
|
||||||
|
|
4
src/models/recall.rb
Normal file
4
src/models/recall.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class LifePex::Recall < Sequel::Model(LifePex::DB[:recalls].order(:name))
|
||||||
|
many_to_one :user
|
||||||
|
many_to_one :pex
|
||||||
|
end
|
|
@ -1,4 +1,23 @@
|
||||||
class LifePex::User < Sequel::Model(:users)
|
class LifePex::User < Sequel::Model(:users)
|
||||||
one_to_many :user_pexs
|
one_to_many :user_pexs
|
||||||
one_to_many :pexs
|
one_to_many :pexs
|
||||||
|
one_to_many :recalls
|
||||||
|
|
||||||
|
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
|
end
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
class LifePex::UserPex < Sequel::Model(:user_pexs)
|
class LifePex::UserPex < Sequel::Model(:user_pexs)
|
||||||
many_to_one :user
|
many_to_one :user
|
||||||
many_to_one :pex
|
many_to_one :pex
|
||||||
|
|
||||||
|
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)}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
class LifePex::Systems::Achievement < LifePex::Systems::AuthSystem
|
require_relative "./csrf.rb"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
|
||||||
|
class LifePex::Systems::AchievementSystem < LifePex::Systems::AuthSystem
|
||||||
include LifePex::Systems::CrlfHelper
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
get "/achievements", auth: [] do
|
get "/achievements", auth: [] do
|
||||||
|
@ -13,16 +16,21 @@ class LifePex::Systems::Achievement < LifePex::Systems::AuthSystem
|
||||||
|
|
||||||
post "/achievements", auth: [] do
|
post "/achievements", auth: [] do
|
||||||
success = LifePex::AchievementDSL::Includer::SUCCESS_INDEX[params["successName"]]
|
success = LifePex::AchievementDSL::Includer::SUCCESS_INDEX[params["successName"]]
|
||||||
parameters_json = success.format_parameters_hash(params["inputParams"]).to_json
|
begin
|
||||||
achievement = LifePex::Achievement.new(
|
parameters_json = success.format_parameters_hash(params["inputParams"]).to_json
|
||||||
user_id: current_user_id,
|
achievement = LifePex::Achievement.new(
|
||||||
success_name: params["successName"],
|
user_id: current_user_id,
|
||||||
name: params["inputName"],
|
success_name: params["successName"],
|
||||||
icon: params["inputMedal"],
|
name: params["inputName"],
|
||||||
parameters_json: parameters_json,
|
icon: params["inputMedal"],
|
||||||
).save
|
parameters_json: parameters_json,
|
||||||
achievements = LifePex::Achievement.where(user_id: current_user_id).all
|
).save
|
||||||
slim :achievements, locals: { achievements: achievements, flash: { success: "Added achievements '#{achievement.name}" } }
|
achievements = LifePex::Achievement.where(user_id: current_user_id).all
|
||||||
|
slim :achievements, locals: { achievements: achievements, flash: { success: "Added achievements '#{achievement.name}" } }
|
||||||
|
rescue => err
|
||||||
|
success = LifePex::AchievementDSL::Includer::SUCCESS_INDEX[params["successName"]]
|
||||||
|
slim :achievement_form, locals: { success: success, flash: { danger: err.message } }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
post "/achievements/delete", auth: [] do
|
post "/achievements/delete", auth: [] do
|
||||||
|
|
22
src/systems/api_response.rb
Normal file
22
src/systems/api_response.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module LifePex::Systems::ApiResponse
|
||||||
|
def api_response(any)
|
||||||
|
content_type 'application/json'
|
||||||
|
any.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_response_entity(message = nil, entity_type = nil, entity = nil, **more)
|
||||||
|
api_response({
|
||||||
|
"message" => message,
|
||||||
|
"entity_type" => entity_type,
|
||||||
|
entity_type => entity,
|
||||||
|
**more,
|
||||||
|
}.compact)
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_error(status = 500, message = "Internal error", **more)
|
||||||
|
halt(status, {
|
||||||
|
message => message,
|
||||||
|
**more,
|
||||||
|
}.to_json)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,33 @@
|
||||||
class LifePex::Systems::AuthSystem < Sinatra::Base
|
class LifePex::Systems::AuthSystem < Sinatra::Base
|
||||||
helpers Sinatra::Cookies
|
helpers Sinatra::Cookies
|
||||||
include JSON::API
|
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)
|
def user_id_decoded(cookies = nil)
|
||||||
cookies = cookies() if cookies.nil?
|
cookies = cookies() if cookies.nil?
|
||||||
begin
|
begin
|
||||||
decoded = JWT.decode(cookies["auth"], LifePex::SECRET)
|
decoded = JWT.decode(cookies["auth"], LifePex::SECRET)
|
||||||
|
renew_user_cookie!
|
||||||
decoded[0]["user_id"]
|
decoded[0]["user_id"]
|
||||||
rescue => err
|
rescue => err
|
||||||
STDERR.puts "user_id_decoded: #{err}"
|
STDERR.puts "user_id_decoded: #{err}"
|
||||||
|
@ -37,7 +59,7 @@ class LifePex::Systems::AuthSystem < Sinatra::Base
|
||||||
condition do
|
condition do
|
||||||
unless logged_in?
|
unless logged_in?
|
||||||
if accept_json?
|
if accept_json?
|
||||||
halt 303, { message: 'You need to /api/v1/register an account and /api/v1/login to get a cookie first' }.to_json
|
halt 401, { message: 'You need to POST /api/user/v1/register to register an account and POST /api/user/v1/login to get a cookie first' }.to_json
|
||||||
else
|
else
|
||||||
redirect "/login", 303
|
redirect "/login", 303
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,35 +1,19 @@
|
||||||
require "date"
|
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
|
class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
include JSON::API
|
|
||||||
include LifePex::Systems::CrlfHelper
|
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
|
get "/today", auth: [] do
|
||||||
cookies["date"] = "today"
|
cookies.set "date", { value: "now", httponly: false }
|
||||||
redirect "/"
|
redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/yesterday", auth: [] do
|
get "/yesterday", auth: [] do
|
||||||
cookies["date"] = "yesterday"
|
cookies.set "date", { value: "yesterday", httponly: false }
|
||||||
redirect "/"
|
redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,13 +21,18 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
user_pexs = my_user_pexs(cookies, get_user_date)
|
user_pexs = my_user_pexs(cookies, get_user_date)
|
||||||
user_pexs_amounts = user_pexs.group_by { |user_pex| user_pex[:pex_id] }
|
user_pexs_amounts = user_pexs.group_by { |user_pex| user_pex[:pex_id] }
|
||||||
pexs = LifePex::Pex.where(user_id: current_user_id)
|
pexs = LifePex::Pex.where(user_id: current_user_id)
|
||||||
|
|
||||||
pexs = pexs.where(hidden: false) if filter_hidden
|
pexs = pexs.where(hidden: false) if filter_hidden
|
||||||
pexs = pexs.all
|
pexs = pexs.all
|
||||||
|
|
||||||
|
user_pexs_last_inserted_at = LifePex::UserPex.last_inserted_at(current_user_id).all
|
||||||
|
|
||||||
pex_by_models = pexs.map do |pex|
|
pex_by_models = pexs.map do |pex|
|
||||||
{
|
{
|
||||||
**pex.values,
|
pex: pex,
|
||||||
user_pexs: {
|
user_pexs: {
|
||||||
amount: (user_pexs_amounts[pex[:id]] || []).length
|
at_date: (user_pexs_amounts[pex[:id]] || []).length,
|
||||||
|
last_inserted_at: (user_pexs_last_inserted_at.find{ |up| up[:pex_id] == pex[:id] } || {})[:last_inserted_at],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -69,12 +58,22 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/pexs", auth: [] do
|
get "/pexs", auth: [] do
|
||||||
slim :pex_form
|
categories = LifePex::Pex
|
||||||
|
.select(:category)
|
||||||
|
.where(user_id: current_user_id)
|
||||||
|
.distinct
|
||||||
|
.pluck(:category)
|
||||||
|
slim :pex_form, locals: { categories: categories }
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/pexs/update", auth: [] do
|
get "/pexs/update", auth: [] do
|
||||||
pex = LifePex::Pex.where(id: params["id"]).first
|
pex = LifePex::Pex.where(id: params["id"]).first
|
||||||
slim :pex_form, locals: { pex: pex, submit_name: "update" }
|
categories = LifePex::Pex
|
||||||
|
.select(:category)
|
||||||
|
.where(user_id: current_user_id)
|
||||||
|
.distinct
|
||||||
|
.pluck(:category)
|
||||||
|
slim :pex_form, locals: { pex: pex, categories: categories, submit_name: "update" }
|
||||||
end
|
end
|
||||||
|
|
||||||
post "/pexs", auth: [] do
|
post "/pexs", auth: [] do
|
||||||
|
@ -116,19 +115,28 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_recap_infos(cookies)
|
def get_recap_infos(params, cookies)
|
||||||
user_pexs = my_user_pexs(cookies)
|
user_pexs = my_user_pexs(cookies)
|
||||||
pexs = LifePex::Pex.setup_user_pexs(user_id: current_user_id, user_pexs: user_pexs)
|
pexs = LifePex::Pex.setup_user_pexs(user_id: current_user_id, user_pexs: user_pexs)
|
||||||
total_xp = pexs.values.sum { |up| up[:total_by_date].values.sum }
|
total_xp = pexs.values.sum { |up| up[:total_by_date].values.sum }
|
||||||
level = LifePex::Level.new total_xp
|
level = LifePex::Level.new total_xp
|
||||||
all_dates = user_pexs.map(&:created_at).uniq
|
|
||||||
all_category = pexs.values.map(&:category).uniq
|
all_category = pexs.values.map(&:category).uniq
|
||||||
|
|
||||||
|
start_date = 60.days.ago.to_date
|
||||||
|
if params["days_ago"]
|
||||||
|
start_date = params["days_ago"].to_i.days.ago.to_date
|
||||||
|
elsif params["since_first_jan"]
|
||||||
|
year = Date.today.year
|
||||||
|
first_jan = Date.new(year, 1, 1)
|
||||||
|
start_date = first_jan
|
||||||
|
end
|
||||||
|
all_dates = (start_date..(Date.today)).to_a
|
||||||
|
|
||||||
pex_table_by_date = all_dates.map { |date| [date, 0] }.to_h
|
pex_table_by_date = all_dates.map { |date| [date, 0] }.to_h
|
||||||
pexs.values.each do |pex|
|
pexs.values.each do |pex|
|
||||||
pex[:total_by_date].each do |date, total|
|
pex[:total_by_date].each do |date, total|
|
||||||
# pex_table_by_date[date] ||= 0
|
# pex_table_by_date[date] ||= 0
|
||||||
pex_table_by_date[date] += total
|
pex_table_by_date[date] += total if pex_table_by_date[date]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -144,7 +152,7 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
category = pex[:category]
|
category = pex[:category]
|
||||||
# pex_table_by_category_and_date[category] ||= {}
|
# pex_table_by_category_and_date[category] ||= {}
|
||||||
# pex_table_by_category_and_date[category][date] ||= 0
|
# pex_table_by_category_and_date[category][date] ||= 0
|
||||||
pex_table_by_category_and_date[category][date] += total
|
pex_table_by_category_and_date[category][date] += total if pex_table_by_category_and_date[category][date]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -159,6 +167,15 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
}
|
}
|
||||||
end.unshift(highchart_total)
|
end.unshift(highchart_total)
|
||||||
|
|
||||||
|
recalls = LifePex::Recall.where(user_id: current_user_id).all
|
||||||
|
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
|
||||||
|
|
||||||
all_setup = LifePex::Achievement.where(user_id: current_user_id).map(&:to_dsl_setup)
|
all_setup = LifePex::Achievement.where(user_id: current_user_id).map(&:to_dsl_setup)
|
||||||
medals = all_setup.filter do |success|
|
medals = all_setup.filter do |success|
|
||||||
success.call(user_pexs, pexs.values)
|
success.call(user_pexs, pexs.values)
|
||||||
|
@ -166,13 +183,16 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
|
|
||||||
{
|
{
|
||||||
pex_tables: highchart_tables,
|
pex_tables: highchart_tables,
|
||||||
|
start_date: start_date,
|
||||||
|
all_dates: all_dates,
|
||||||
level: level,
|
level: level,
|
||||||
medals: medals,
|
medals: medals,
|
||||||
|
recalls_not_validated: recalls_not_validated,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/recap", auth: [], provides: 'html' do
|
get "/recap", auth: [], provides: 'html' do
|
||||||
recap_infos = get_recap_infos(cookies)
|
recap_infos = get_recap_infos(params, cookies)
|
||||||
slim :recap, locals: recap_infos
|
slim :recap, locals: recap_infos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -183,9 +203,10 @@ class LifePex::Systems::PexSystem < LifePex::Systems::AuthSystem
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
get "/recap", auth: [], provides: 'json' do
|
get "/recap", auth: [], provides: 'json' do
|
||||||
recap_infos = get_recap_infos(cookies)
|
recap_infos = get_recap_infos(params, cookies)
|
||||||
{
|
{
|
||||||
pex_tables: recap_infos[:pex_tables],
|
pex_tables: recap_infos[:pex_tables],
|
||||||
|
all_dates: recap_infos[:all_dates],
|
||||||
level: recap_infos[:level].to_h,
|
level: recap_infos[:level].to_h,
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ require_relative "./pex.rb"
|
||||||
|
|
||||||
class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
# include JSON::API # included by PexSystem
|
# include JSON::API # included by PexSystem
|
||||||
|
include LifePex::Systems::ApiResponse
|
||||||
|
|
||||||
# extend DocMyRoutes::Annotatable # included by PexSystem
|
# extend DocMyRoutes::Annotatable # included by PexSystem
|
||||||
# register Sinatra::Namespace # included by PexSystem
|
# register Sinatra::Namespace # included by PexSystem
|
||||||
|
@ -24,7 +25,7 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :pluck, required: false, type: 'string', in: 'query', description: 'improve performance by only fetching one field'
|
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 = current_user.pexs
|
||||||
pexs = pexs.select(json_params["pluck"]) if json_params["pluck"]
|
pexs = pexs.select(json_params["pluck"]) if json_params["pluck"]
|
||||||
pexs.to_json
|
pexs.to_json
|
||||||
|
@ -37,10 +38,15 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
parameter :category, required: true, type: 'string', in: 'body'
|
parameter :category, required: true, type: 'string', in: 'body'
|
||||||
parameter :amount, required: true, type: 'number', in: 'body'
|
parameter :amount, required: true, type: 'number', in: 'body'
|
||||||
parameter :auth, required: true, type: 'string', in: 'cookies'
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
post "/", auth: [], provides: 'json' do
|
post '', auth: [], provides: 'json' do
|
||||||
raise RunetimeError.new "\"name\" is required" unless json_params["name"]
|
halt 400, "\"name\" is required" unless json_params["name"]
|
||||||
raise RunetimeError.new "\"category\" is required" unless json_params["category"]
|
halt 400, "\"category\" is required" unless json_params["category"]
|
||||||
raise RunetimeError.new "\"amount\" is required" unless json_params["amount"]
|
halt 400, "\"amount\" is required" unless json_params["amount"]
|
||||||
|
halt 409, "conflict with existing pex with the same characteristics" if LifePex::Pex.find(
|
||||||
|
name: json_params['name'],
|
||||||
|
category: json_params['category'],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
pex_id = LifePex::Pex.insert(
|
pex_id = LifePex::Pex.insert(
|
||||||
name: json_params['name'],
|
name: json_params['name'],
|
||||||
category: json_params['category'],
|
category: json_params['category'],
|
||||||
|
@ -53,54 +59,148 @@ class LifePex::Systems::Pex2System < LifePex::Systems::PexSystem
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
summary 'Update an existing pex'
|
||||||
|
produces 'application/json'
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, require: true, type: 'integer', in: 'path'
|
||||||
|
parameter :name, required: false, type: 'string', in: 'body'
|
||||||
|
parameter :category, required: false, type: 'string', in: 'body'
|
||||||
|
parameter :amount, required: false, type: 'number', in: 'body'
|
||||||
|
parameter :auth, required: true, ype: 'string', in: 'cookies'
|
||||||
|
put '/:id', auth: [], provides: 'json' do
|
||||||
|
pex = LifePex::Pex.find(
|
||||||
|
id: params["id"],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
pex.name = json_params['name'] unless json_params['name'].nil?
|
||||||
|
pex.category = json_params['category'] unless json_params['category'].nil?
|
||||||
|
pex.amount = json_params['amount'] unless json_params['amount'].nil?
|
||||||
|
pex.save
|
||||||
|
{
|
||||||
|
message: "entity updated",
|
||||||
|
pex: pex,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
summary 'Delete an existing pex'
|
||||||
|
produces 'application/json'
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, require: true, type: 'integer', in: 'path'
|
||||||
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
|
delete '/:id', auth: [], provides: 'json' do
|
||||||
|
pex = LifePex::Pex.find(
|
||||||
|
id: params["id"],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
pex.destroy
|
||||||
|
{
|
||||||
|
message: "entity deleted",
|
||||||
|
pex: pex,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
summary 'Show the state of a pex'
|
summary 'Show the state of a pex'
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :id, required: true, type: 'string', in: 'path'
|
parameter :id, required: true, type: 'string', in: 'path'
|
||||||
get "/:id", auth: [], provides: 'json' do
|
get '/:id', auth: [], provides: 'json' do
|
||||||
|
pex = LifePex::Pex.find(
|
||||||
|
id: params["id"],
|
||||||
|
user_id: current_user.id,
|
||||||
|
)
|
||||||
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
{
|
{
|
||||||
pex: LifePex::Pex.find(
|
pex: pex,
|
||||||
id: params["id"],
|
|
||||||
user_id: current_user.id,
|
|
||||||
),
|
|
||||||
}.to_json
|
}.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
summary 'Show the state of a pex and load advanced infos about it'
|
summary 'Show the state of a pex and load advanced infos about it'
|
||||||
produces 'application/json'
|
produces 'application/json,text/html'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :id, required: true, type: 'string', in: 'path'
|
parameter :id, required: true, type: 'string', in: 'path'
|
||||||
get "/:id/more", auth: [], provides: 'json' do
|
get '/:id/more', auth: [], provides: 'json' do
|
||||||
pex = LifePex::Pex.find(
|
pex = LifePex::Pex.find(
|
||||||
id: params["id"],
|
id: params["id"],
|
||||||
user_id: current_user_id,
|
user_id: current_user_id,
|
||||||
)
|
)
|
||||||
{
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
|
user_pexs = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex.id
|
||||||
|
).select(:created_at).all
|
||||||
|
date = date_input_convertor(json_params["date"])
|
||||||
|
user_pexs_by_date = user_pexs.group_by(&:created_at).transform_values { |group| group.size }
|
||||||
|
user_pexs_at_date = user_pexs_by_date[date]
|
||||||
|
|
||||||
|
pex_with_more = {
|
||||||
pex: pex,
|
pex: pex,
|
||||||
user_pexs: LifePex::UserPex.where(user_id: current_user_id, pex_id: pex.id).select(:created_at).all.group_by(&:created_at).transform_values { |group| group.size }
|
user_pexs: {
|
||||||
}.to_json
|
by_date: user_pexs_by_date,
|
||||||
|
at_date: user_pexs_at_date,
|
||||||
|
total: user_pexs.size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
given_content_type do
|
||||||
|
json pex_with_more.to_json
|
||||||
|
html slim_partial("pex_row", locals: { pex_with_amount: pex_with_more })
|
||||||
|
default do
|
||||||
|
api_error(400, message: "No valid accepted content found.")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
summary 'Get the amount of a pex each day since first occurence since 1 january'
|
summary 'Get the amount of a pex each day since first occurence since a given date'
|
||||||
notes ""
|
notes ''
|
||||||
produces 'application/json'
|
produces 'application/json'
|
||||||
status_codes [200]
|
status_codes [200]
|
||||||
parameter :id, required: true, type: 'integer', in: 'path'
|
parameter :id, required: true, type: 'integer', in: 'path'
|
||||||
parameter :auth, required: true, type: 'string', in: 'cookies'
|
parameter :auth, required: true, type: 'string', in: 'cookies'
|
||||||
post "/:id/recap", auth: [], provides: 'json' do
|
parameter :days_ago, required: false, type: 'integer', in: 'body', description: "default: 30, limit the amount of results to the n last days included"
|
||||||
user_id = user_id_decoded
|
parameter :since_first_jan, required: false, type: 'boolean', in: 'body', description: "default: false, if no days_ago defined, limit to all dates after (included) 1st jan of the current year"
|
||||||
|
post '/:id/recap', auth: [], provides: 'json' do
|
||||||
pex_id = params["id"]
|
pex_id = params["id"]
|
||||||
year = Date.today.year
|
pex = LifePex::Pex.find(id: pex_id, user_id: current_user_id)
|
||||||
first_jan = Date.new(year, 1, 1)
|
halt 404, { message: "No pex #{params["id"]}" }.to_json if pex.nil?
|
||||||
pex = LifePex::Pex.find(id: pex_id)
|
|
||||||
user_pexs = LifePex::UserPex.where {
|
start_date = 30.days.ago
|
||||||
self.user_id == user_id && self.pex_id == pex_id && created_at > first_jan
|
if params["days_ago"]
|
||||||
}.first
|
start_date = params["days_ago"].to_i.days.ago
|
||||||
|
elsif params["since_first_jan"]
|
||||||
|
year = Date.today.year
|
||||||
|
first_jan = Date.new(year, 1, 1)
|
||||||
|
start_date = first_jan
|
||||||
|
end
|
||||||
|
|
||||||
|
user_pexs = LifePex::UserPex.where { # TODO query that
|
||||||
|
self.user_id == current_user_id && self.pex_id == pex_id && created_at >= start_date
|
||||||
|
}
|
||||||
{ pex: pex, user_pexs: user_pexs }.to_json
|
{ pex: pex, user_pexs: user_pexs }.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace '/infos' do
|
||||||
|
|
||||||
|
summary 'Get for all existing pex the last date a user_pex has been added'
|
||||||
|
produces 'application/json'
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, required: true, type: 'string', in: 'path'
|
||||||
|
get '/last-inserts', auth: [], provides: 'json' do
|
||||||
|
pexs = LifePex::UserPex.last_inserted_at(current_user.id)
|
||||||
|
pexs.map { |pex|
|
||||||
|
{
|
||||||
|
pex_id: pex[:pex_id],
|
||||||
|
name: pex[:name],
|
||||||
|
last_inserted_at: pex[:last_inserted_at],
|
||||||
|
}
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
include LifePex::Systems::ApiList
|
include LifePex::Systems::ApiList
|
||||||
end
|
end
|
||||||
|
|
47
src/systems/recall.rb
Normal file
47
src/systems/recall.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
require_relative "./csrf.rb"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
|
||||||
|
class LifePex::Systems::RecallSystem < LifePex::Systems::AuthSystem
|
||||||
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
|
get "/recalls", auth: [] do
|
||||||
|
recalls = current_user.recalls
|
||||||
|
slim :recalls, locals: { recalls: recalls }
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/recalls/new", auth: [] do
|
||||||
|
pexs = current_user.pexs
|
||||||
|
slim :recall_form, locals: { pexs: pexs }
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/recalls", auth: [] do
|
||||||
|
flash = {}
|
||||||
|
if !LifePex::Pex.where(user_id: current_user_id, id: params["pex_id"]).first
|
||||||
|
flash[:danger] = "Invalid pex"
|
||||||
|
else
|
||||||
|
recall = LifePex::Recall.new(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: params["pex_id"],
|
||||||
|
name: params["name"],
|
||||||
|
span_duration: params["span_duration"],
|
||||||
|
repeated: params["repeated"],
|
||||||
|
).save
|
||||||
|
flash[:success] = "Recall #{recall.name} created"
|
||||||
|
end
|
||||||
|
recalls = current_user.recalls
|
||||||
|
slim :recalls, locals: { recalls: recalls, flash: flash }
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/recalls/delete", auth: [] do
|
||||||
|
recall = LifePex::Recall.where(user_id: current_user_id, id: params["id"]).first
|
||||||
|
flash = {}
|
||||||
|
if recall
|
||||||
|
recall.destroy
|
||||||
|
flash[:success] = "Recall #{recall.name} removed"
|
||||||
|
else
|
||||||
|
flash[:danger] = "Recall do not exists"
|
||||||
|
end
|
||||||
|
recalls = current_user.recalls
|
||||||
|
slim :recalls, locals: { recalls: recalls, flash: flash }
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,8 @@
|
||||||
require "yaml"
|
require "yaml"
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
require_relative "./csrf.rb"
|
||||||
|
|
||||||
class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
include JSON::API
|
|
||||||
include LifePex::Systems::CrlfHelper
|
include LifePex::Systems::CrlfHelper
|
||||||
|
|
||||||
DEFAULT_PEXS_FOR_NEW_USERS = YAML.load_file "config/default_pexs_for_new_users.yaml"
|
DEFAULT_PEXS_FOR_NEW_USERS = YAML.load_file "config/default_pexs_for_new_users.yaml"
|
||||||
|
@ -10,14 +11,10 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
slim :login
|
slim :login
|
||||||
end
|
end
|
||||||
|
|
||||||
def setup_user_cookie!(user_id)
|
|
||||||
cookies["auth"] = JWT.encode({ "user_id" => user_id }, LifePex::SECRET)
|
|
||||||
end
|
|
||||||
|
|
||||||
def login(params)
|
def login(params)
|
||||||
user = LifePex::User.where(username: params["username"]).first
|
user = LifePex::User.where(username: params["username"]).first
|
||||||
if user && BCrypt::Password.new(user[:hashed_password]) == params["password"]
|
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
|
user
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
|
@ -26,7 +23,6 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
|
|
||||||
post "/login", provides: 'html' do
|
post "/login", provides: 'html' do
|
||||||
if user = login(params)
|
if user = login(params)
|
||||||
cookies["auth"] = JWT.encode({ "user_id" => user[:id] }, LifePex::SECRET)
|
|
||||||
redirect "/"
|
redirect "/"
|
||||||
else
|
else
|
||||||
slim :login, locals: { flash: { danger: "Failed to login" } }
|
slim :login, locals: { flash: { danger: "Failed to login" } }
|
||||||
|
@ -60,7 +56,7 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/logout", auth: [] do
|
post "/logout", auth: [] do
|
||||||
cookies.delete "auth"
|
cookies.delete "auth"
|
||||||
slim :logout, locals: { flash: { success: "Logged out" } }
|
slim :logout, locals: { flash: { success: "Logged out" } }
|
||||||
end
|
end
|
||||||
|
@ -87,6 +83,40 @@ class LifePex::Systems::UserSystem < LifePex::Systems::AuthSystem
|
||||||
slim :about
|
slim :about
|
||||||
end
|
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
|
extend DocMyRoutes::Annotatable
|
||||||
register Sinatra::Namespace
|
register Sinatra::Namespace
|
||||||
namespace '/api/user/v1' do
|
namespace '/api/user/v1' do
|
||||||
|
|
138
src/systems/user_pex.rb
Normal file
138
src/systems/user_pex.rb
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
require_relative "./auth.rb"
|
||||||
|
require_relative "./api_response"
|
||||||
|
require "csv"
|
||||||
|
|
||||||
|
class LifePex::Systems::UserPexSystem < LifePex::Systems::AuthSystem
|
||||||
|
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"
|
||||||
|
produces "application/json"
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
parameter :date, required: true, type: "string", in: "path"
|
||||||
|
get "/:pex_id/amount/by-date/:date", auth: [], provides: "json" do
|
||||||
|
pex_id = params["pex_id"]
|
||||||
|
date = params["date"]
|
||||||
|
count = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).count
|
||||||
|
api_response({ count: count, entity_type: "user_pex" })
|
||||||
|
end
|
||||||
|
|
||||||
|
summary "Create a new user_pex for a given day"
|
||||||
|
produces "application/json"
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
parameter :date, required: false, type: "string", in: "body"
|
||||||
|
parameter :force_count_total, required: false, type: "bool", in: "body", description: "if true, force the server to computes the total amount of validation compatible after the operation"
|
||||||
|
post "/:pex_id/validation", auth: [], provides: "json" do
|
||||||
|
pex_id = params["pex_id"]
|
||||||
|
date = date_input_convertor(json_params["date"])
|
||||||
|
api_error(403, "You may not have created or access to this pex id") if LifePex::Pex.where(id: pex_id).select(:user_id).first&.user_id != current_user_id
|
||||||
|
user_pex = LifePex::UserPex.new(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).save
|
||||||
|
count_total = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).count if json_params["force_count_total"] == true
|
||||||
|
api_response_entity(
|
||||||
|
"Successfuly added one user_pex",
|
||||||
|
"user_pex",
|
||||||
|
user_pex,
|
||||||
|
count_total: count_total,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
summary "Remove an existing user_pex for a given day"
|
||||||
|
produces "application/json"
|
||||||
|
status_codes [200]
|
||||||
|
parameter :id, required: true, type: "integer", in: "path"
|
||||||
|
parameter :date, required: false, type: "string", in: "body"
|
||||||
|
parameter :force_count_total, required: false, type: "bool", in: "body", description: "if true, force the server to computes the total amount of validation compatible after the operation"
|
||||||
|
delete "/:pex_id/validation", auth: [], provides: "json" do
|
||||||
|
pex_id = params["pex_id"]
|
||||||
|
date = date_input_convertor(json_params["date"])
|
||||||
|
user_pex = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).select(:id).first
|
||||||
|
if user_pex
|
||||||
|
user_pex.destroy
|
||||||
|
count_total = LifePex::UserPex.where(
|
||||||
|
user_id: current_user_id,
|
||||||
|
pex_id: pex_id,
|
||||||
|
created_at: date,
|
||||||
|
).count if json_params["force_count_total"] == true
|
||||||
|
api_response({
|
||||||
|
message: "Successfuly destroyed 1 user_pex",
|
||||||
|
entity_type: "user_pex",
|
||||||
|
count_destroyed: 1,
|
||||||
|
count_total: count_total,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
api_error(404, "Nothing to destroy")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
include LifePex::Systems::ApiList
|
||||||
|
end
|
79
src/utils/boot_framework.rb
Normal file
79
src/utils/boot_framework.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
require "colorize"
|
||||||
|
|
||||||
|
# @example
|
||||||
|
# LifePex::BootFrameWork::Application.new(ENV).boot do
|
||||||
|
# harshly_need_env "VARIABLE_NAME1"
|
||||||
|
# kindly_ask_env "VARIABLE_NAME2" { do_something_if_failure() }
|
||||||
|
# harshly_do true
|
||||||
|
# kindly_do env["SOME_OTHER_VARIABLE"] { error "this should not happen :(((" }
|
||||||
|
# end.finish!
|
||||||
|
module LifePex::BootFramework
|
||||||
|
class Application
|
||||||
|
attr_reader :env, :stop
|
||||||
|
|
||||||
|
def initialize(env)
|
||||||
|
@env = env
|
||||||
|
@stop = false
|
||||||
|
end
|
||||||
|
|
||||||
|
private def message(string)
|
||||||
|
STDERR.puts string
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(string)
|
||||||
|
message "Error: #{string}".red
|
||||||
|
end
|
||||||
|
|
||||||
|
def warning(string)
|
||||||
|
message "Warning: #{string}".yellow
|
||||||
|
end
|
||||||
|
|
||||||
|
def boot(&block)
|
||||||
|
instance_eval(&block)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def finish!
|
||||||
|
if @stop
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def will_stop!
|
||||||
|
@stop = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def harshly_need_env(env_variable, &block)
|
||||||
|
@env.fetch env_variable do
|
||||||
|
error "\"#{env_variable}\" is a required ENV variable. Provide it.".red
|
||||||
|
will_stop!
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def kindly_ask_env(env_variable, &block)
|
||||||
|
@env.fetch env_variable do
|
||||||
|
warning "\"#{env_variable}\" is a prefered ENV variable. Provide it if possible.".yellow
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def harshly_do(test, &block)
|
||||||
|
if test
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
will_stop!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def kindly_do(test, &block)
|
||||||
|
if test
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.boot_application(&block)
|
||||||
|
Application.new(ENV).boot(&block)
|
||||||
|
end
|
||||||
|
end
|
11
src/utils/env.rb
Normal file
11
src/utils/env.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
require "dotenv"
|
||||||
|
|
||||||
|
def get_env
|
||||||
|
ENV.fetch("LIFEPEX_ENV") { "development" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_dotenv(app_env = nil)
|
||||||
|
app_env ||= get_env
|
||||||
|
Dotenv.load(".env.#{app_env}.local", ".env.local", ".env")
|
||||||
|
app_env
|
||||||
|
end
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "active_support/all"
|
||||||
|
|
||||||
module JSON::API
|
module JSON::API
|
||||||
def json_params
|
def json_params
|
||||||
begin
|
begin
|
||||||
|
@ -10,7 +12,103 @@ module JSON::API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
(base_time - offset.hours).to_date
|
||||||
|
end
|
||||||
|
|
||||||
|
# params:
|
||||||
|
# - mimes: example: %w(application/json text/json)
|
||||||
|
def accept?(*mimes)
|
||||||
|
@request_accept_str ||= request.accept.map(&:to_str)
|
||||||
|
@request_accept_str.intersection(mimes).size > 0
|
||||||
|
end
|
||||||
|
|
||||||
def accept_json?
|
def accept_json?
|
||||||
request.accept.any? { |a| a.entry == 'application/json' }
|
accept? 'application/json'
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept_html?
|
||||||
|
accept? 'text/html'
|
||||||
|
end
|
||||||
|
|
||||||
|
def slim_partial(partial, locals: {})
|
||||||
|
Slim::Template.new(
|
||||||
|
"src/views/partials/#{partial}.slim",
|
||||||
|
# layout: false,
|
||||||
|
).render(self, locals)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# render do
|
||||||
|
# json "{ ... }"
|
||||||
|
# html "< ... >"
|
||||||
|
# end
|
||||||
|
def given_content_type(&block)
|
||||||
|
raise "Invalid DSL call to JSON::API.render without block" unless block_given?
|
||||||
|
Render.new(self, &block).complete_rendering
|
||||||
|
end
|
||||||
|
|
||||||
|
class Render
|
||||||
|
delegate :slim,
|
||||||
|
:slim_partial,
|
||||||
|
# :api_error,
|
||||||
|
# :api_response,
|
||||||
|
# :render,
|
||||||
|
to: :@context
|
||||||
|
attr_reader :context
|
||||||
|
|
||||||
|
def initialize(context, &block)
|
||||||
|
@render = nil
|
||||||
|
@context = context
|
||||||
|
instance_eval(&block) if block_given?
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_something_if_accept(acceptance, output, &block)
|
||||||
|
if block_given?
|
||||||
|
context.instance_eval(&block)
|
||||||
|
else
|
||||||
|
@render = output
|
||||||
|
end if @context.send("accept_#{acceptance}?")
|
||||||
|
end
|
||||||
|
|
||||||
|
def json(output = nil, &block)
|
||||||
|
render_something_if_accept :json, output, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def html(output = nil, &block)
|
||||||
|
render_something_if_accept :html, output, &block
|
||||||
|
end
|
||||||
|
|
||||||
|
def default(output = nil, &block)
|
||||||
|
if block_given?
|
||||||
|
@render_default_block = block
|
||||||
|
else
|
||||||
|
@render_default = output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete_rendering
|
||||||
|
@render ||
|
||||||
|
(@render_default_block.is_a?(Proc) && @context.instance_eval(&@render_default_block)) ||
|
||||||
|
@render_default
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
15
src/utils/users.rb
Normal file
15
src/utils/users.rb
Normal 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
|
|
@ -17,7 +17,10 @@
|
||||||
I engage my honor to do never read or modify personnal data you may have put on
|
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.
|
the server, and do my best to ensure its security.
|
||||||
You should look at the code source if you want to audit it.
|
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
|
| 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.
|
provide a feature for do it yourself.
|
||||||
Meanwhile you can still drop me an issue or a message on
|
Meanwhile you can still drop me an issue or a message on
|
||||||
|
@ -26,6 +29,8 @@
|
||||||
|
|
|
|
||||||
i
|
i
|
||||||
| #lifepex.
|
| #lifepex.
|
||||||
|
a.btn.btn-success(href="/api/user-pex/v1/export.csv")
|
||||||
|
| Export as CSV
|
||||||
|
|
||||||
h2
|
h2
|
||||||
| Service version
|
| Service version
|
||||||
|
|
|
@ -3,66 +3,19 @@
|
||||||
table.table
|
table.table
|
||||||
tbody
|
tbody
|
||||||
- pex_by_models.each.with_index do |pex_by_model, index|
|
- pex_by_models.each.with_index do |pex_by_model, index|
|
||||||
- if pex_by_model[:category] && pex_by_model[:category] != pex_by_models[index - 1][:category]
|
- if pex_by_model[:pex][:category] != pex_by_models[index - 1][:pex][:category]
|
||||||
tr.full-row-sep
|
tr.full-row-sep
|
||||||
td.col-12(colspan=6)
|
td.col-12(colspan=6)
|
||||||
- if pex_by_model[:category].empty?
|
- if pex_by_model[:pex][:category].empty?
|
||||||
| Base
|
| Base
|
||||||
- else
|
- else
|
||||||
= pex_by_model[:category].capitalize
|
= pex_by_model[:pex][:category].capitalize
|
||||||
- @row_class = ""
|
- @row_class = ""
|
||||||
- if pex_by_model[:hidden]
|
- if pex_by_model[:pex][:hidden]
|
||||||
- @row_class = "bg-warning"
|
- @row_class = "bg-warning"
|
||||||
- if pex_by_model[:flag] == "bookmarked"
|
- if pex_by_model[:pex][:flag] == "bookmarked"
|
||||||
- @row_class = "bg-bookmarked"
|
- @row_class = "bg-bookmarked"
|
||||||
tr.full-row(class=@row_class)
|
== slim :"partials/pex_row", locals: { pex_with_amount: pex_by_model }
|
||||||
td.col-1
|
|
||||||
.btn.pex-editor-toggler(name=pex_by_model[:id] style="display: none;")
|
|
||||||
| ¤
|
|
||||||
.pex-editor(name=name=pex_by_model[:id])
|
|
||||||
form method="POST" action="/pexs/bookmark"
|
|
||||||
== csrf_tag
|
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
|
||||||
button.btn.btn-success type="submit"
|
|
||||||
- if pex_by_model[:flag] == "bookmarked"
|
|
||||||
| unfav
|
|
||||||
- else
|
|
||||||
| fav
|
|
||||||
form method="POST" action="/pexs/delete"
|
|
||||||
== csrf_tag
|
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
|
||||||
input type="hidden" name="type" value="-"
|
|
||||||
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permanently REMOVE \'#{pex_by_model[:name]}\' and points ?')"
|
|
||||||
| x
|
|
||||||
form method="POST" action="/pexs/hide"
|
|
||||||
== csrf_tag
|
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
|
||||||
- if pex_by_model[:hidden]
|
|
||||||
button.btn.btn-success type="submit"
|
|
||||||
| >>
|
|
||||||
- else
|
|
||||||
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to disable \'#{pex_by_model[:name]}\' (do not affect points) ?')"
|
|
||||||
| <<
|
|
||||||
a.btn.btn-warning href="/pexs/update?id=#{pex_by_model[:id]}"
|
|
||||||
| u
|
|
||||||
td.col-8=pex_by_model[:name]
|
|
||||||
td.col-1.center
|
|
||||||
- if pex_by_model[:user_pexs][:amount] > 0
|
|
||||||
form method="POST" action="/"
|
|
||||||
== csrf_tag
|
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
|
||||||
input type="hidden" name="type" value="-"
|
|
||||||
button.btn.force-1-col type="submit" style="display: block;"
|
|
||||||
| -
|
|
||||||
td.col-1.center
|
|
||||||
=pex_by_model[:user_pexs][:amount]
|
|
||||||
td.col-1.center
|
|
||||||
form method="POST" action="/"
|
|
||||||
== csrf_tag
|
|
||||||
input type="hidden" name="id" value=pex_by_model[:id]
|
|
||||||
input type="hidden" name="type" value="+"
|
|
||||||
button.btn.force-1-col type="submit" style="display: block;"
|
|
||||||
| +
|
|
||||||
|
|
||||||
form method="GET" action="/pexs"
|
form method="GET" action="/pexs"
|
||||||
== csrf_tag
|
== csrf_tag
|
||||||
|
|
|
@ -2,7 +2,8 @@ doctype html
|
||||||
html lang="en"
|
html lang="en"
|
||||||
head
|
head
|
||||||
/! Required meta tags
|
/! Required meta tags
|
||||||
title Life Pex
|
title
|
||||||
|
| Life Pex
|
||||||
meta charset="utf-8" /
|
meta charset="utf-8" /
|
||||||
meta content="width=device-width, initial-scale=1" name="viewport" /
|
meta content="width=device-width, initial-scale=1" name="viewport" /
|
||||||
/! Bootstrap CSS
|
/! Bootstrap CSS
|
||||||
|
@ -19,9 +20,13 @@ html lang="en"
|
||||||
- if cookies["date"] == "yesterday"
|
- if cookies["date"] == "yesterday"
|
||||||
a.navbar-brand href="/"
|
a.navbar-brand href="/"
|
||||||
| Yesterday
|
| Yesterday
|
||||||
|
- if cookies["show_full_date"] == "on"
|
||||||
|
.smaller= get_user_date
|
||||||
- else
|
- else
|
||||||
a.navbar-brand href="/"
|
a.navbar-brand href="/"
|
||||||
| Today
|
| 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"
|
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/
|
span.navbar-toggler-icon/
|
||||||
.collapse.navbar-collapse#navbar-collapser
|
.collapse.navbar-collapse#navbar-collapser
|
||||||
|
@ -34,28 +39,27 @@ html lang="en"
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/yesterday" Yesterday
|
a.btn.btn-lg.btn-dark href="/yesterday" Yesterday
|
||||||
li.nav-item
|
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
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/achievements" Achievements
|
a.btn.btn-lg.btn-dark href="/achievements" Achievements
|
||||||
|
li.nav-item
|
||||||
|
a.btn.btn-lg.btn-dark href="/recalls" Recalls
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/password" Change password
|
a.btn.btn-lg.btn-dark href="/password" Change password
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
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
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/?filter_hidden=false" Show hidden
|
a.btn.btn-lg.btn-dark href="/?filter_hidden=false" Show hidden
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/logout" Logout
|
form method="POST" action="/logout"
|
||||||
|
input.btn.btn-lg.btn-dark type="submit" value="Logout"
|
||||||
|
|
||||||
/ li.nav-item.dropdown
|
|
||||||
/ a.btn.btn-lg.btn-dark.nav-link.dropdown-toggle#nav-drop-more href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"
|
|
||||||
/ | More
|
|
||||||
/ ul.dropdown-menu aria-labelledby="nav-drop-more"
|
|
||||||
/ li
|
|
||||||
/ a.btn.btn-lg.btn-dark href="/password" Change password
|
|
||||||
/ li
|
|
||||||
/ a.btn.btn-lg.btn-dark href="/about" About lifepex
|
|
||||||
/ li
|
|
||||||
/ a.btn.btn-lg.btn-dark href="/logout" Logout
|
|
||||||
- else
|
- else
|
||||||
li.nav-item
|
li.nav-item
|
||||||
a.btn.btn-lg.btn-dark href="/login" Login
|
a.btn.btn-lg.btn-dark href="/login" Login
|
||||||
|
@ -65,7 +69,7 @@ html lang="en"
|
||||||
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
a.btn.btn-lg.btn-dark href="/about" About lifepex
|
||||||
|
|
||||||
|
|
||||||
.flash
|
#flash.flash
|
||||||
- if defined? flash
|
- if defined? flash
|
||||||
- flash.each do |flash_name, flash_message|
|
- flash.each do |flash_name, flash_message|
|
||||||
.alert.alert-dismissible.fade.show role="alert" class="alert-#{flash_name}"
|
.alert.alert-dismissible.fade.show role="alert" class="alert-#{flash_name}"
|
||||||
|
@ -81,3 +85,5 @@ html lang="en"
|
||||||
/ script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"
|
/ script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"
|
||||||
script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
|
script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"
|
||||||
script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"
|
script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"
|
||||||
|
script src="/js/error.js"
|
||||||
|
script src="/js/ajax.js"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
h1 Sign in with your account
|
h1 Sign in with your account
|
||||||
form.col-md-6 method="POST" action="/login"
|
form#login.col-md-6 method="POST" action="/login"
|
||||||
== csrf_tag
|
== csrf_tag
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-2.col-form-label for="inputUsername" Username
|
label.col-sm-2.col-form-label for="inputUsername" Username
|
||||||
|
|
53
src/views/partials/pex_row.slim
Normal file
53
src/views/partials/pex_row.slim
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
tr.full-row(class=@row_class)
|
||||||
|
td.col-1
|
||||||
|
.btn.pex-editor-toggler(name=pex_with_amount[:pex][:id] style="display: none;")
|
||||||
|
| ¤
|
||||||
|
.pex-editor(name=name=pex_with_amount[:pex][:id])
|
||||||
|
form method="POST" action="/pexs/bookmark"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
button.btn.btn-success type="submit"
|
||||||
|
- if pex_with_amount[:pex][:flag] == "bookmarked"
|
||||||
|
| unfav
|
||||||
|
- else
|
||||||
|
| fav
|
||||||
|
form method="POST" action="/pexs/delete"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
input type="hidden" name="type" value="-"
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permanently REMOVE \'#{pex_with_amount[:pex][:name]}\' and points ?')"
|
||||||
|
| x
|
||||||
|
form method="POST" action="/pexs/hide"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
- if pex_with_amount[:pex][:hidden]
|
||||||
|
button.btn.btn-success type="submit"
|
||||||
|
| >>
|
||||||
|
- else
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to disable \'#{pex_with_amount[:pex][:name]}\' (do not affect points) ?')"
|
||||||
|
| <<
|
||||||
|
a.btn.btn-warning href="/pexs/update?id=#{pex_with_amount[:pex][:id]}"
|
||||||
|
| u
|
||||||
|
td.col-8
|
||||||
|
.row
|
||||||
|
.col-8
|
||||||
|
| #{pex_with_amount[:pex][:name]}
|
||||||
|
.col-4
|
||||||
|
.small=pex_with_amount[:user_pexs][:last_inserted_at]
|
||||||
|
td.col-1.center
|
||||||
|
form.userpexvalidation.userpexvalidationdecrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
input type="hidden" name="type" value="-"
|
||||||
|
button.btn.force-1-col type="submit" style="display: block;"
|
||||||
|
| -
|
||||||
|
td.col-1.center
|
||||||
|
.userpexvalidation.userpexvalidationvalue
|
||||||
|
=pex_with_amount[:user_pexs][:at_date]
|
||||||
|
td.col-1.center
|
||||||
|
form.userpexvalidation.userpexvalidationincrease method="POST" action="/" onsubmit="return userpexValidation(event)"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=pex_with_amount[:pex][:id]
|
||||||
|
input type="hidden" name="type" value="+"
|
||||||
|
button.btn.force-1-col type="submit" style="display: block;"
|
||||||
|
| +
|
|
@ -15,7 +15,10 @@ main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
input#inputUsername.form-control.form-control-lg name="name" type="text" value=pex&.name /
|
input#inputUsername.form-control.form-control-lg name="name" type="text" value=pex&.name /
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-12.col-form-label for="inputCategory" Category
|
label.col-sm-12.col-form-label for="inputCategory" Category
|
||||||
input#inputCategory.form-control.form-control-lg name="category" type="text" placeholder="sport" value=pex&.category /
|
input#inputCategory.form-control.form-control-lg name="category" type="text" placeholder="sport" list="categoryList" value=pex&.category /
|
||||||
|
datalist#categoryList
|
||||||
|
- categories.each do |category|
|
||||||
|
option value=category
|
||||||
.form-group.row
|
.form-group.row
|
||||||
label.col-sm-12.col-form-label for="inputAmount" Xp Amount by check
|
label.col-sm-12.col-form-label for="inputAmount" Xp Amount by check
|
||||||
input#inputAmount.form-control.form-control-lg name="amount" type="integer" min="-50" max="50" value=(pex&.amount||"1") /
|
input#inputAmount.form-control.form-control-lg name="amount" type="integer" min="-50" max="50" value=(pex&.amount||"1") /
|
||||||
|
|
45
src/views/preferences.slim
Normal file
45
src/views/preferences.slim
Normal 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
|
||||||
|
|
33
src/views/recall_form.slim
Normal file
33
src/views/recall_form.slim
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
main.col-md-9.ms-sm-auto.col-lg-10.px-md-4
|
||||||
|
h1
|
||||||
|
| Add a new Recall
|
||||||
|
form.col-md-6 method="POST" action="/recalls"
|
||||||
|
== csrf_tag
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="name"
|
||||||
|
strong
|
||||||
|
| Name
|
||||||
|
input#name.form-control.form-control-lg name="name" type="text" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="span_duration"
|
||||||
|
strong
|
||||||
|
| Span duration
|
||||||
|
input#span_duration.form-control.form-control-lg name="span_duration" type="number" value="7" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="repeated"
|
||||||
|
strong
|
||||||
|
| Repeated
|
||||||
|
input#inputRepeated.form-control.form-control-lg name="repeated" type="number" value="1" /
|
||||||
|
.form-group.row
|
||||||
|
label.col-sm-12.col-form-label for="pex_id"
|
||||||
|
strong
|
||||||
|
| PexId
|
||||||
|
select.form-select.form-control-lg arial-label="Pex Id" name="pex_id"
|
||||||
|
- pexs.each.with_index do |pex, pex_idx|
|
||||||
|
- if pex_idx == 0
|
||||||
|
option value=pex[:id] selected="true" =pex[:name]
|
||||||
|
- else
|
||||||
|
option value=pex[:id] =pex[:name]
|
||||||
|
|
||||||
|
.form-group.row
|
||||||
|
input.btn.btn-lg.btn-block type="submit" value="create"
|
25
src/views/recalls.slim
Normal file
25
src/views/recalls.slim
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
h1
|
||||||
|
| Current recalls
|
||||||
|
|
||||||
|
a.btn.btn-dark href="/recalls/new"
|
||||||
|
| Add a new recall
|
||||||
|
|
||||||
|
table.table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th Name
|
||||||
|
th Span duration
|
||||||
|
th Repeated
|
||||||
|
th
|
||||||
|
tbody
|
||||||
|
- recalls.each do |recall|
|
||||||
|
tr
|
||||||
|
td=recall.name
|
||||||
|
td=recall.span_duration
|
||||||
|
td=recall.repeated
|
||||||
|
td
|
||||||
|
form method="POST" action="/recalls/delete"
|
||||||
|
== csrf_tag
|
||||||
|
input type="hidden" name="id" value=recall[:id]
|
||||||
|
button.btn.btn-danger type="submit" onclick="return confirm('Confirm to permanently REMOVE \'#{recall[:name]}\' ?')"
|
||||||
|
| x
|
|
@ -2,6 +2,17 @@ script src="https://code.highcharts.com/highcharts.js"
|
||||||
|
|
||||||
h1 Recap
|
h1 Recap
|
||||||
|
|
||||||
|
.recap-recalls
|
||||||
|
- if !recalls_not_validated.empty?
|
||||||
|
p
|
||||||
|
| You have a warning to consider!
|
||||||
|
|
||||||
|
h2
|
||||||
|
- recalls_not_validated.each do |recall|
|
||||||
|
span.badge.bg-danger
|
||||||
|
| #{recall[:name]}
|
||||||
|
br/
|
||||||
|
|
||||||
.recap-level
|
.recap-level
|
||||||
span.badge.bg-primary
|
span.badge.bg-primary
|
||||||
| Level #{level.current_level.round}
|
| Level #{level.current_level.round}
|
||||||
|
@ -10,6 +21,15 @@ h1 Recap
|
||||||
| #{level.xp_from_current_level.round} / #{level.xp_for_complete_level.round}
|
| #{level.xp_from_current_level.round} / #{level.xp_for_complete_level.round}
|
||||||
|
|
||||||
.recap-xp
|
.recap-xp
|
||||||
|
.float-end
|
||||||
|
form#reloader.col-md-6 method="GET"
|
||||||
|
== 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"] || cookies["recap_days"] || "60") /
|
||||||
|
.form-group.row
|
||||||
|
p
|
||||||
|
input.btn.btn-lg.btn-block type="submit" value="Reload"
|
||||||
#recap-xp-container style="width:100%; height:400px;"
|
#recap-xp-container style="width:100%; height:400px;"
|
||||||
|
|
||||||
.recap-success
|
.recap-success
|
||||||
|
|
82
test/base.rb
Normal file
82
test/base.rb
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
ENV["LIFEPEX_ENV"] = "test"
|
||||||
|
require "yaml"
|
||||||
|
require_relative "./fixtures_reader"
|
||||||
|
require_relative "../src/app"
|
||||||
|
|
||||||
|
require "rack/test"
|
||||||
|
require "test/unit"
|
||||||
|
|
||||||
|
class LifePexTest < Test::Unit::TestCase
|
||||||
|
include Rack::Test::Methods
|
||||||
|
|
||||||
|
def app
|
||||||
|
LifePex::App
|
||||||
|
end
|
||||||
|
|
||||||
|
%i(get head).each do |verb|
|
||||||
|
define_method "api_#{verb}" do |uri, **headers|
|
||||||
|
self.send(verb, uri, headers.merge({ "CONTENT_TYPE" => "application/json" }))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i(post put patch delete).each do |verb|
|
||||||
|
define_method "api_#{verb}" do |uri, body = {}, **headers|
|
||||||
|
header "Accept", "application/json"
|
||||||
|
header "Content-Type", "application/json"
|
||||||
|
self.send(verb, uri, body.to_json, headers.merge({
|
||||||
|
"CONTENT_TYPE" => "application/json",
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def login_as(username = "test", password = "test")
|
||||||
|
api_post '/api/user/v1/login', { username: username, password: password }
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if every key of `minimal_data` is contained in `full_data`
|
||||||
|
def assert_hash_include(minimal_data, full_data)
|
||||||
|
full_data = full_data.stringify_keys
|
||||||
|
minimal_data.stringify_keys.each do |key, value|
|
||||||
|
assert_equal value, full_data[key], "#{key} should contains <#{value}>, got <#{full_data[key]}>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# parse the body as a json
|
||||||
|
def api_reponse_body
|
||||||
|
JSON.parse last_response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if the body contains a message
|
||||||
|
def assert_message_exist
|
||||||
|
assert_not_empty api_reponse_body.dig "message"
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if the entity.user_id belongs to the user who sent the last req
|
||||||
|
def assert_entity_owned(entity)
|
||||||
|
user_id = JWT.decode(last_request.cookies["auth"], nil, false).first["user_id"]
|
||||||
|
assert_equal user_id.to_s, entity["user_id"].to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
# status may be a Regexp: the assertion with test if they match
|
||||||
|
# status may be a String: the assertion with test if the param includes the response status
|
||||||
|
# status may be another thing: the assertion with test equality
|
||||||
|
def assert_status(status)
|
||||||
|
if status.is_a?(Regexp)
|
||||||
|
assert_match status, last_response.status.to_s
|
||||||
|
elsif status.is_a?(String)
|
||||||
|
assert_equal status, last_response.status.to_s[0..(status.size)]
|
||||||
|
else
|
||||||
|
assert_equal status, last_response.status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_client_error(status = /^4\d\d/)
|
||||||
|
refute last_response.ok?
|
||||||
|
assert_status status
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_server_error(status = /^5\d\d/)
|
||||||
|
refute last_response.ok?
|
||||||
|
assert_status status
|
||||||
|
end
|
||||||
|
end
|
6
test/fixtures/pex_base.yaml
vendored
Normal file
6
test/fixtures/pex_base.yaml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pexs:
|
||||||
|
- id: 1
|
||||||
|
user_id: 1
|
||||||
|
name: 'pex_test_1'
|
||||||
|
category: 'category'
|
||||||
|
amount: 1.234
|
7
test/fixtures/user_base.yaml
vendored
Normal file
7
test/fixtures/user_base.yaml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
users:
|
||||||
|
- id: 1
|
||||||
|
username: "test"
|
||||||
|
hashed_password: "$2a$12$yWy1fyQBTGYfwRY7H8QGAubHS3nJiMWAaHl8HhXZcTZnuQxRlqIhu" # test
|
||||||
|
- id: 2
|
||||||
|
username: "testbis"
|
||||||
|
hashed_password: "$2a$12$yWy1fyQBTGYfwRY7H8QGAubHS3nJiMWAaHl8HhXZcTZnuQxRlqIhu" # test
|
23
test/fixtures_reader.rb
Normal file
23
test/fixtures_reader.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
|
class FixtureReader
|
||||||
|
def initialize
|
||||||
|
cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
KEEP_TABLES = %i(meta)
|
||||||
|
|
||||||
|
def apply!(id)
|
||||||
|
path = File.join("test", "fixtures", "#{id}.yaml")
|
||||||
|
data = YAML.load_file path
|
||||||
|
data.each do |table, rows|
|
||||||
|
rows.each { |row| LifePex::DB[table.to_sym].insert row }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup!
|
||||||
|
LifePex::DB.tables.reject { |table, _| KEEP_TABLES.include?(table) }.each do |table|
|
||||||
|
LifePex::DB[table].truncate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
149
test/pex2_system_test.rb
Normal file
149
test/pex2_system_test.rb
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
require_relative "./base"
|
||||||
|
|
||||||
|
class Pex2SystemTest < LifePexTest
|
||||||
|
def setup
|
||||||
|
@fixture ||= FixtureReader.new # should be in startup
|
||||||
|
@fixture.apply! "user_base"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
@fixture.cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
private def login_as(username = "test", password = "test")
|
||||||
|
api_post '/api/user/v1/login', { username: username, password: password }
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_life_cycle
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_empty api_reponse_body
|
||||||
|
|
||||||
|
pex_input = { name: "pex1", category: "cat1", amount: 1.23 }
|
||||||
|
api_post "/api/pex/v2/pexs", pex_input
|
||||||
|
assert last_response.ok?, "A new pex should have been added"
|
||||||
|
assert_message_exist
|
||||||
|
assert_hash_include pex_input, api_reponse_body["pex"]
|
||||||
|
assert_entity_owned api_reponse_body["pex"]
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 1, api_reponse_body.size, "A new pex should exist"
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_hash_include pex_input, api_reponse_body["pex"]
|
||||||
|
assert_entity_owned api_reponse_body["pex"]
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}/more"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_hash_include pex_input, api_reponse_body["pex"]
|
||||||
|
assert_entity_owned api_reponse_body["pex"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_errors
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23 }
|
||||||
|
assert_client_error 401 # not connect
|
||||||
|
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { }
|
||||||
|
assert_client_error 400 # missing name
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { amount: 1.23 }
|
||||||
|
assert_client_error 400 # missing name
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1' }
|
||||||
|
assert_client_error 400 # missing amount
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23 }
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23 }
|
||||||
|
assert_client_error # duplicate
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_security
|
||||||
|
login_as "test"
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
login_as "testbis"
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_empty api_reponse_body
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert_client_error
|
||||||
|
api_get "/api/pex/v2/pexs/#{pex_id}/more"
|
||||||
|
assert_client_error
|
||||||
|
api_put "/api/pex/v2/pexs/#{pex_id}", {}
|
||||||
|
assert_client_error
|
||||||
|
api_delete "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert_client_error
|
||||||
|
api_post "/api/pex/v2/pexs/#{pex_id}/recap", {}
|
||||||
|
assert_client_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_validation_and_counts
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs/#{pex_id}/recap", {}
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_empty api_reponse_body["user_pexs"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_update
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_put "/api/pex/v2/pexs/#{pex_id}", { name: 'pex2', amount: 1.25 }
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal pex_id, api_reponse_body.dig("pex", "id")
|
||||||
|
assert_equal "pex2", api_reponse_body.dig("pex", "name")
|
||||||
|
assert_equal 1.25, api_reponse_body.dig("pex", "amount")
|
||||||
|
assert_equal "cat1", api_reponse_body.dig("pex", "category")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_pex_delete
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body.size, "No pex exist yet"
|
||||||
|
|
||||||
|
api_post "/api/pex/v2/pexs", { name: 'pex1', amount: 1.23, category: "cat1" }
|
||||||
|
assert last_response.ok?
|
||||||
|
pex_id = api_reponse_body.dig "pex", "id"
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 1, api_reponse_body.size, "A pex should exists now"
|
||||||
|
|
||||||
|
api_delete "/api/pex/v2/pexs/#{pex_id}"
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/pex/v2/pexs"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body.size, "Pex should be removed"
|
||||||
|
end
|
||||||
|
end
|
42
test/user_pex_test.rb
Normal file
42
test/user_pex_test.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require_relative "./base"
|
||||||
|
|
||||||
|
class UserPexSystemTest < LifePexTest
|
||||||
|
def setup
|
||||||
|
@fixture ||= FixtureReader.new # should be in startup
|
||||||
|
@fixture.apply! "user_base"
|
||||||
|
@fixture.apply! "pex_base"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
@fixture.cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_user_pex_counter
|
||||||
|
login_as
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/user-pex/v1/pexs/1/amount/by-date/2021-01-01"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body["count"], "No user pex counted yet"
|
||||||
|
|
||||||
|
api_post "/api/user-pex/v1/pexs/1/validation", { date: "2021-01-01" }
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal "2021-01-01", api_reponse_body.dig("user_pex", "created_at")
|
||||||
|
assert_equal 1, api_reponse_body.dig("user_pex", "pex_id")
|
||||||
|
assert_equal 1, api_reponse_body.dig("user_pex", "user_id")
|
||||||
|
|
||||||
|
api_get "/api/user-pex/v1/pexs/1/amount/by-date/2021-01-01"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 1, api_reponse_body["count"], "One use pex is added"
|
||||||
|
|
||||||
|
api_delete "/api/user-pex/v1/pexs/1/validation", { date: "2021-01-01" }
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
api_get "/api/user-pex/v1/pexs/1/amount/by-date/2021-01-01"
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_equal 0, api_reponse_body["count"], "No user pex remaining"
|
||||||
|
|
||||||
|
api_delete "/api/user-pex/v1/pexs/1/validation", { date: "2021-01-01" }
|
||||||
|
assert_client_error
|
||||||
|
end
|
||||||
|
end
|
37
test/user_system_test.rb
Normal file
37
test/user_system_test.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
require_relative "./base"
|
||||||
|
|
||||||
|
class UserSystemTest < LifePexTest
|
||||||
|
def setup
|
||||||
|
@fixture ||= FixtureReader.new # should be in startup
|
||||||
|
@fixture.apply! "user_base"
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
@fixture.cleanup!
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_login_generates_cookie
|
||||||
|
data = { username: 'test', password: 'test' }
|
||||||
|
api_post '/api/user/v1/login', data
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_not_empty last_response.headers["Set-Cookie"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_register_and_login
|
||||||
|
data = { username: 'test2', password: 'test' }
|
||||||
|
api_post '/api/user/v1/register', data
|
||||||
|
assert last_response.ok?
|
||||||
|
assert_not_empty last_response.headers["Set-Cookie"]
|
||||||
|
assert_match (/auth=[\-\.\w]+; domain=example.org; path=\//), last_response.headers["Set-Cookie"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_register_refuse_duplicate
|
||||||
|
data = { username: 'test2', password: 'test' }
|
||||||
|
api_post '/api/user/v1/register', data
|
||||||
|
assert last_response.ok?
|
||||||
|
|
||||||
|
data = { username: 'test2', password: 'test' }
|
||||||
|
api_post '/api/user/v1/register', data
|
||||||
|
refute last_response.ok?
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user