Compare commits

...

11 commits

Author SHA1 Message Date
1resu c3ae113a38 Update Readme 2021-12-21 08:45:42 +01:00
1resu 604de8118f Use puma in favour of webrick 2021-12-21 08:45:42 +01:00
JuliusR 9f3d301550 improve handling of User resource 2021-12-21 08:45:42 +01:00
JuliusR d705402d8b simplify routes 2021-12-21 08:45:42 +01:00
JuliusR dd6d35e3bd add unique index on Users#email 2021-12-21 08:45:42 +01:00
JuliusR e5a9c03eb9 rails db:schema:dump
This updates the db/schema.rb from a clean development database
(mariadb:10.1) using Rails 6.1.
2021-12-21 08:45:42 +01:00
JuliusR c2ee9065db add public/packs to .gitignore 2021-12-21 08:45:42 +01:00
JuliusR 0d88a64df9 stop ignoring config/*.yml, but ignore config/database.yml 2021-12-21 08:45:42 +01:00
JuliusR 16807381e5 copy .gitignore from old sharedlists 2021-12-21 08:45:42 +01:00
JuliusR b318469606 replace database.yml by database.yml.SAMPLE (now mysql2 only) 2021-12-21 08:45:41 +01:00
JuliusR 248d787ba5 fix GEM_PATH and PATH in Dockerfile.development 2021-12-21 08:45:41 +01:00
15 changed files with 175 additions and 179 deletions

69
.gitignore vendored
View file

@ -1,40 +1,37 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files. .bundle
# .rake_tasks*
# If you find yourself ignoring temporary files generated by your text editor db/*.sqlite3
# or operating system, you probably want to add a global ignore instead: log
# git config --global core.excludesfile '~/.gitignore_global' node_modules
tmp/*
!tmp/.keep
public/assets
public/packs
public/system
public/uploads
supplier_assets/**
vendor/bundle
# Ignore bundler config. # ignore database configuration, but SHARE OTHER CONFIG FILES
/.bundle config/database.yml
# Ignore the default SQLite database. # IDEs, Developer tools
/db/*.sqlite3 .idea
/db/*.sqlite3-* .loadpath
.project
.sass-cache
.rbenv-version
.get-dump.yml
.bash_history
nbproject/
.*.sw?
*~
# Ignore all logfiles and tempfiles. coverage
/log/* tags
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory. # Capistrano etc.
/tmp/pids/* Capfile
!/tmp/pids/ config/deploy
!/tmp/pids/.keep config/deploy.rb
Gemfile.capistrano*
# Ignore uploaded files in development.
/storage/*
!/storage/.keep
/public/assets
.byebug_history
# Ignore master key for decrypting credentials and more.
/config/master.key
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity

View file

@ -12,8 +12,8 @@ USER app
ENV BUNDLE_JOBS=4 \ ENV BUNDLE_JOBS=4 \
BUNDLE_PATH=/srv/app/vendor/bundle \ BUNDLE_PATH=/srv/app/vendor/bundle \
GEM_PATH=/srv/app/vendor/bundle:$GEM_PATH \ GEM_PATH=/srv/app/vendor/bundle/ruby/2.7.0:$GEM_PATH \
PATH=/srv/app/vendor/bundle/bin:$PATH PATH=/srv/app/vendor/bundle/ruby/2.7.0/bin:$PATH
EXPOSE 3000 EXPOSE 3000

View file

@ -9,6 +9,7 @@ gem 'webpacker', '~> 5.0'
gem 'turbolinks', '~> 5' gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.7' gem 'jbuilder', '~> 2.7'
gem 'bootsnap', '>= 1.4.4', require: false gem 'bootsnap', '>= 1.4.4', require: false
gem 'puma', '~> 5.0'
group :development, :test do group :development, :test do
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]

View file

@ -127,6 +127,8 @@ GEM
mini_portile2 (~> 2.6.1) mini_portile2 (~> 2.6.1)
racc (~> 1.4) racc (~> 1.4)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.5.2)
nio4r (~> 2.0)
racc (1.6.0) racc (1.6.0)
rack (2.2.3) rack (2.2.3)
rack-mini-profiler (2.3.3) rack-mini-profiler (2.3.3)
@ -254,6 +256,7 @@ DEPENDENCIES
listen (~> 3.3) listen (~> 3.3)
midi-smtp-server (~> 3.0) midi-smtp-server (~> 3.0)
mysql2 (>= 0.5) mysql2 (>= 0.5)
puma (~> 5.0)
rack-mini-profiler (~> 2.0) rack-mini-profiler (~> 2.0)
rails (~> 6.1.4, >= 6.1.4.4) rails (~> 6.1.4, >= 6.1.4.4)
roo roo

View file

@ -5,7 +5,7 @@
Sharedlists is a simple rails driven database for managing multiple product lists of various suppliers. Sharedlists is a simple rails driven database for managing multiple product lists of various suppliers.
This app is used in conjunction with [foodsoft](https://github.com/foodcoops/foodsoft). This app is used in conjunction with [foodsoft](https://github.com/foodcoops/foodsoft).
Recommended [Ruby](http://ruby-lang.org/) version is 2.3 (note that 2.4 does not work). Recommended [Ruby](http://ruby-lang.org/) version is 2.7.
## Development ## Development
@ -15,7 +15,7 @@ Recommended [Ruby](http://ruby-lang.org/) version is 2.3 (note that 2.4 does not
Copy `config/database.yml.SAMPLE` to `config/database.yml` and Copy `config/database.yml.SAMPLE` to `config/database.yml` and
docker-compose run --rm app bundle docker-compose run --rm app bundle
docker-compose run --rm app rake db:setup docker-compose run --rm app rails db:setup
### Run ### Run
@ -31,6 +31,8 @@ To access sharedlists, you'll need to create a user (and I guess you want admin
> u.save! > u.save!
> exit > exit
You can create more users within the web interface.
## Production ## Production
Either fetch the image, or build it: Either fetch the image, or build it:
@ -39,16 +41,16 @@ Either fetch the image, or build it:
# or # or
docker build --tag sharedlists:latest --rm . docker build --tag sharedlists:latest --rm .
Then set environment variables `SECRET_TOKEN` and `DATABASE_URL` and run: Then set environment variables `SECRET_KEY_BASE` and `DATABASE_URL` and run:
docker run --name sharedlists_web \ docker run --name sharedlists_web \
-e SECRET_TOKEN -e DATABASE_URL -e RAILS_FORCE_SSL=false \ -e SECRET_KEY_BASE -e DATABASE_URL -e RAILS_FORCE_SSL=false \
sharedlists:latest sharedlists:latest
To run cronjobs, start another instance: To run cronjobs, start another instance:
docker run --name sharedlists_cron \ docker run --name sharedlists_cron \
-e SECRET_TOKEN -e DATABASE_URL \ -e SECRET_KEY_BASE -e DATABASE_URL \
sharedlists:latest ./proc-start cron sharedlists:latest ./proc-start cron
If you want to process incoming mails, add another instance like the previous, If you want to process incoming mails, add another instance like the previous,
@ -97,19 +99,19 @@ Once you have the `sync_ftp_files` task working, you may wish to setup a
### Email ### Email
Some suppliers send a regular email with an article list in the attachment. For this, an Some suppliers send a regular email with an article list in the attachment. For this, an
email server needs to be run using the rake task `mail:smtp_server`. email server needs to be run using the rails task `mail:smtp_server`.
On production, you may want to run this on localhost on an unprivileged port, with a On production, you may want to run this on localhost on an unprivileged port, with a
proper [MTA](https://en.wikipedia.org/wiki/Message_transfer_agent) in front that proper [MTA](https://en.wikipedia.org/wiki/Message_transfer_agent) in front that
does message routing. does message routing.
To enable this for a certain supplier, tick the checkbox _Update articles by email_. Then To enable this for a certain supplier, tick the checkbox _Update articles by email_. Then
select a file format to use for importing, and the supplier's email address from which the select a file format to use for importing, and the supplier's email address from which the
email is sent. If you only want to import for mails with a subject that contains a certain email is sent. If you only want to import mails with a subject that contains a certain
text (e.g. _Articles in week_), fill in the subject field as well. text (e.g. _Articles in week_), fill in the subject field as well.
What email address does the supplier need to send to? Users will find this after saving What email address does the supplier need to send to? Users will find this after initial creating and
the supplier after _Send to_. saving the supplier after _Send to_.
This needs setting up of the environment variable `MAILER_DOMAIN`, on which you receive the This needs setting up of the environment variable `MAILER_DOMAIN`, on which you receive the
emails. It is allowed to prefix the address, you may want to set the prefix in `MAILER_PREFIX`. mails. It is allowed to prefix the address, you may want to set the prefix in `MAILER_PREFIX`.
This is useful when you're running an email server in front to route mails. This is useful when you're running a mail server in front to route mails.

View file

@ -22,6 +22,14 @@ class ApplicationController < ActionController::Base
end end
end end
def admin_required!
user = current_user
if user.nil? || !user.admin?
flash[:error] = "Not authorized!"
redirect_to root_url
end
end
def authenticate_supplier_admin! def authenticate_supplier_admin!
@supplier = Supplier.find((params[:supplier_id] || params[:id])) @supplier = Supplier.find((params[:supplier_id] || params[:id]))
unless current_user.has_access_to?(@supplier) unless current_user.has_access_to?(@supplier)

View file

@ -6,14 +6,14 @@ class SessionsController < ApplicationController
end end
def create def create
user = User.authenticate(params[:email], params[:password]) user = User.find_by(email: params[:email])
if user if user && user.authenticate(params[:password])
session[:user_id] = user.id session[:user_id] = user.id
flash[:notice] = "Logged in!" flash[:notice] = "Logged in!"
redirect_to root_url redirect_to root_url
else else
flash.now[:error] = "Invalid email or password" flash.now[:error] = "Invalid email or password"
render "new" render :new
end end
end end

View file

@ -1,14 +1,18 @@
class UsersController < ApplicationController class UsersController < ApplicationController
before_action :admin_required!
def new def new
@user=User.new @user=User.new
end end
def create def create
@user=User.new(user_params) @user = User.new(user_params)
if @user.save if @user.save
render 'show' flash[:notice] = "Konto wurde erfolgreich erstellt."
redirect_to @user
else else
redirect_to new_user_path render :new
end end
end end
@ -18,16 +22,11 @@ class UsersController < ApplicationController
def update def update
@user = User.find(params[:id]) @user = User.find(params[:id])
attrs = user_params if @user.update(user_params)
respond_to do |format| flash[:notice] = 'Konto wurde erfolgreich aktualisiert.'
if @user.update(attrs) redirect_to @user
flash[:notice] = 'Konto wurde erfolgreich aktualisiert.' else
format.html { redirect_to user_url(@user) } render :edit
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @user.errors.to_xml }
end
end end
end end
@ -50,6 +49,6 @@ class UsersController < ApplicationController
private private
def user_params def user_params
params.require(:user).permit(:email, :password) params.require(:user).permit(:email, :password, :password_confirmation, :admin)
end end
end end

View file

@ -2,38 +2,43 @@ class User < ApplicationRecord
has_many :user_accesses, :dependent => :destroy has_many :user_accesses, :dependent => :destroy
has_many :suppliers, :through => :user_accesses has_many :suppliers, :through => :user_accesses
attr_accessor :password attr_reader :password
before_save :encrypt_password
validates_confirmation_of :password validates :email, presence: true, uniqueness: true
validates_presence_of :password, :on => :create validates :password, confirmation: true
validates_presence_of :email validate do |user|
validates_uniqueness_of :email unless user.password_hash.present? && user.password_salt.present?
user.errors.add :password, :blank
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end end
end end
def encrypt_password def self.attributes_protected_by_default
if password.present? super + %w(password_hash password_salt)
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end end
def has_access_to?(supplier) def has_access_to?(supplier)
admin? or !UserAccess.first(:conditions => {:supplier_id => supplier.id, :user_id => id}).nil? admin? or !UserAccess.where(supplier_id: supplier.id, user_id: id).first.nil?
end
def authenticate(password_plain)
if self.password_hash == BCrypt::Engine.hash_secret(password_plain, self.password_salt)
self
else
false
end
end
def password=(password_plain)
@password = password_plain
unless password_plain.blank?
new_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password_plain, new_salt)
self.password_salt = new_salt
end
end end
def admin? def admin?
!!admin !!admin
end end
end end

View file

@ -2,6 +2,7 @@
= f.input :email, required: true = f.input :email, required: true
= f.input :password, required: true = f.input :password, required: true
= f.input :password_confirmation, required: true = f.input :password_confirmation, required: true
= f.input :admin, required: true
.form-actions .form-actions
= f.submit class: 'btn' = f.submit class: 'btn'

View file

@ -1,25 +0,0 @@
# SQLite. Versions 3.8.0 and up are supported.
# gem install sqlite3
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
#
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: db/test.sqlite3
production:
<<: *default
database: db/production.sqlite3

View file

@ -0,0 +1,9 @@
development:
adapter: mysql2
encoding: utf8
reconnect: false
database: development
pool: 5
username: root
password: secret
host: mysql

View file

@ -1,6 +1,4 @@
Rails.application.routes.draw do Rails.application.routes.draw do
get 'users/new'
get 'users/show'
get 'log_in' => 'sessions#new', :as => :log_in get 'log_in' => 'sessions#new', :as => :log_in
match 'log_out' => 'sessions#destroy', :as => :log_out, :via => [:get, :post] match 'log_out' => 'sessions#destroy', :as => :log_out, :via => [:get, :post]
resources :sessions, :only => [:new, :create, :destroy] resources :sessions, :only => [:new, :create, :destroy]
@ -18,8 +16,4 @@ Rails.application.routes.draw do
end end
resources :users resources :users
match '/:controller(/:action(/:id))', :via => [:get, :post]
match '/users', to: 'users#index', via: 'get'
match '/users/:id', to: 'users#show', via: 'get'
end end

View file

@ -0,0 +1,5 @@
class IndexUsersByUniqueEmail < ActiveRecord::Migration[6.1]
def change
add_index :users, :email, unique: true
end
end

119
db/schema.rb generated
View file

@ -1,85 +1,82 @@
# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead # This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to # of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition. # incrementally modify your database, and then regenerate this schema definition.
# #
# Note that this schema.rb definition is the authoritative source for your # This file is the source Rails uses to define your schema when running `bin/rails
# database schema. If you need to create the application database on another # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# system, you should be using db:schema:load, not running all the migrations # be faster and is potentially less error prone than running all of your
# from scratch. The latter is a flawed and unsustainable approach (the more migrations # migrations from scratch. Old migrations may fail to apply correctly if those
# you'll amass, the slower it'll run and the greater likelihood for issues). # migrations use external dependencies or application code.
# #
# It's strongly recommended to check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(:version => 20190811115732) do ActiveRecord::Schema.define(version: 2021_12_19_074758) do
create_table "articles", :force => true do |t| create_table "articles", charset: "utf8", force: :cascade do |t|
t.string "name", :null => false t.string "name", null: false
t.integer "supplier_id", :null => false t.integer "supplier_id", null: false
t.string "number" t.string "number"
t.string "note" t.string "note"
t.string "manufacturer" t.string "manufacturer"
t.string "origin" t.string "origin"
t.string "unit" t.string "unit"
t.decimal "price", :precision => 8, :scale => 2, :default => 0.0, :null => false t.decimal "price", precision: 8, scale: 2, default: "0.0", null: false
t.decimal "tax", :precision => 3, :scale => 1, :default => 7.0, :null => false t.decimal "tax", precision: 3, scale: 1, default: "7.0", null: false
t.decimal "deposit", :precision => 8, :scale => 2, :default => 0.0, :null => false t.decimal "deposit", precision: 8, scale: 2, default: "0.0", null: false
t.decimal "unit_quantity", :precision => 4, :scale => 1, :default => 1.0, :null => false t.decimal "unit_quantity", precision: 4, scale: 1, default: "1.0", null: false
t.decimal "scale_quantity", :precision => 4, :scale => 2 t.decimal "scale_quantity", precision: 4, scale: 2
t.decimal "scale_price", :precision => 8, :scale => 2 t.decimal "scale_price", precision: 8, scale: 2
t.datetime "created_on" t.datetime "created_on"
t.datetime "updated_on" t.datetime "updated_on"
t.string "category" t.string "category"
t.index ["name"], name: "index_articles_on_name"
t.index ["number", "supplier_id"], name: "index_articles_on_number_and_supplier_id", unique: true
end end
add_index "articles", ["name"], :name => "index_articles_on_name" create_table "suppliers", charset: "utf8", force: :cascade do |t|
add_index "articles", ["number", "supplier_id"], :name => "index_articles_on_number_and_supplier_id", :unique => true t.string "name", null: false
t.string "address", null: false
create_table "suppliers", :force => true do |t| t.string "phone", null: false
t.string "name", :null => false t.string "phone2"
t.string "address", :null => false t.string "fax"
t.string "phone", :null => false t.string "email"
t.string "phone2" t.string "url"
t.string "fax" t.string "delivery_days"
t.string "email" t.string "note"
t.string "url"
t.string "delivery_days"
t.string "note"
t.datetime "created_on" t.datetime "created_on"
t.datetime "updated_on" t.datetime "updated_on"
t.boolean "ftp_sync", :default => false t.boolean "ftp_sync", default: false
t.string "ftp_host" t.string "ftp_host"
t.string "ftp_user" t.string "ftp_user"
t.string "ftp_password" t.string "ftp_password"
t.string "ftp_type", :default => "bnn", :null => false t.string "ftp_type", default: "bnn", null: false
t.string "ftp_regexp", :default => "^([.]/)?PL" t.string "ftp_regexp", default: "^([.]/)?PL"
t.boolean "mail_sync" t.boolean "mail_sync"
t.string "mail_from" t.string "mail_from"
t.string "mail_subject" t.string "mail_subject"
t.string "mail_type" t.string "mail_type"
t.string "salt", :null => false t.string "salt", null: false
t.index ["name"], name: "index_suppliers_on_name", unique: true
end end
add_index "suppliers", ["name"], :name => "index_suppliers_on_name", :unique => true create_table "user_accesses", charset: "utf8", force: :cascade do |t|
t.integer "user_id"
create_table "user_accesses", :force => true do |t| t.integer "supplier_id"
t.integer "user_id"
t.integer "supplier_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.index ["supplier_id"], name: "index_user_accesses_on_supplier_id"
t.index ["user_id", "supplier_id"], name: "index_user_accesses_on_user_id_and_supplier_id"
t.index ["user_id"], name: "index_user_accesses_on_user_id"
end end
add_index "user_accesses", ["supplier_id"], :name => "index_user_accesses_on_supplier_id" create_table "users", charset: "utf8", force: :cascade do |t|
add_index "user_accesses", ["user_id", "supplier_id"], :name => "index_user_accesses_on_user_id_and_supplier_id" t.string "email"
add_index "user_accesses", ["user_id"], :name => "index_user_accesses_on_user_id" t.string "password_hash"
t.string "password_salt"
create_table "users", :force => true do |t|
t.string "email"
t.string "password_hash"
t.string "password_salt"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "admin", :default => false t.boolean "admin", default: false
t.index ["email"], name: "index_users_on_email", unique: true
end end
end end