Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
c3ae113a38 | |||
604de8118f | |||
9f3d301550 | |||
d705402d8b | |||
dd6d35e3bd | |||
e5a9c03eb9 | |||
c2ee9065db | |||
0d88a64df9 | |||
16807381e5 | |||
b318469606 | |||
248d787ba5 |
69
.gitignore
vendored
69
.gitignore
vendored
|
@ -1,40 +1,37 @@
|
|||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
.bundle
|
||||
.rake_tasks*
|
||||
db/*.sqlite3
|
||||
log
|
||||
node_modules
|
||||
tmp/*
|
||||
!tmp/.keep
|
||||
public/assets
|
||||
public/packs
|
||||
public/system
|
||||
public/uploads
|
||||
supplier_assets/**
|
||||
vendor/bundle
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
# ignore database configuration, but SHARE OTHER CONFIG FILES
|
||||
config/database.yml
|
||||
|
||||
# Ignore the default SQLite database.
|
||||
/db/*.sqlite3
|
||||
/db/*.sqlite3-*
|
||||
# IDEs, Developer tools
|
||||
.idea
|
||||
.loadpath
|
||||
.project
|
||||
.sass-cache
|
||||
.rbenv-version
|
||||
.get-dump.yml
|
||||
.bash_history
|
||||
nbproject/
|
||||
.*.sw?
|
||||
*~
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
/tmp/*
|
||||
!/log/.keep
|
||||
!/tmp/.keep
|
||||
coverage
|
||||
tags
|
||||
|
||||
# Ignore pidfiles, but keep the directory.
|
||||
/tmp/pids/*
|
||||
!/tmp/pids/
|
||||
!/tmp/pids/.keep
|
||||
|
||||
# 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
|
||||
# Capistrano etc.
|
||||
Capfile
|
||||
config/deploy
|
||||
config/deploy.rb
|
||||
Gemfile.capistrano*
|
||||
|
|
|
@ -12,8 +12,8 @@ USER app
|
|||
|
||||
ENV BUNDLE_JOBS=4 \
|
||||
BUNDLE_PATH=/srv/app/vendor/bundle \
|
||||
GEM_PATH=/srv/app/vendor/bundle:$GEM_PATH \
|
||||
PATH=/srv/app/vendor/bundle/bin:$PATH
|
||||
GEM_PATH=/srv/app/vendor/bundle/ruby/2.7.0:$GEM_PATH \
|
||||
PATH=/srv/app/vendor/bundle/ruby/2.7.0/bin:$PATH
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -9,6 +9,7 @@ gem 'webpacker', '~> 5.0'
|
|||
gem 'turbolinks', '~> 5'
|
||||
gem 'jbuilder', '~> 2.7'
|
||||
gem 'bootsnap', '>= 1.4.4', require: false
|
||||
gem 'puma', '~> 5.0'
|
||||
|
||||
group :development, :test do
|
||||
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
||||
|
|
|
@ -127,6 +127,8 @@ GEM
|
|||
mini_portile2 (~> 2.6.1)
|
||||
racc (~> 1.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.5.2)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
rack-mini-profiler (2.3.3)
|
||||
|
@ -254,6 +256,7 @@ DEPENDENCIES
|
|||
listen (~> 3.3)
|
||||
midi-smtp-server (~> 3.0)
|
||||
mysql2 (>= 0.5)
|
||||
puma (~> 5.0)
|
||||
rack-mini-profiler (~> 2.0)
|
||||
rails (~> 6.1.4, >= 6.1.4.4)
|
||||
roo
|
||||
|
|
24
README.md
24
README.md
|
@ -5,7 +5,7 @@
|
|||
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).
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
docker-compose run --rm app bundle
|
||||
docker-compose run --rm app rake db:setup
|
||||
docker-compose run --rm app rails db:setup
|
||||
|
||||
### Run
|
||||
|
||||
|
@ -31,6 +31,8 @@ To access sharedlists, you'll need to create a user (and I guess you want admin
|
|||
> u.save!
|
||||
> exit
|
||||
|
||||
You can create more users within the web interface.
|
||||
|
||||
## Production
|
||||
|
||||
Either fetch the image, or build it:
|
||||
|
@ -39,16 +41,16 @@ Either fetch the image, or build it:
|
|||
# or
|
||||
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 \
|
||||
-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
|
||||
|
||||
To run cronjobs, start another instance:
|
||||
|
||||
docker run --name sharedlists_cron \
|
||||
-e SECRET_TOKEN -e DATABASE_URL \
|
||||
-e SECRET_KEY_BASE -e DATABASE_URL \
|
||||
sharedlists:latest ./proc-start cron
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
proper [MTA](https://en.wikipedia.org/wiki/Message_transfer_agent) in front that
|
||||
does message routing.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
What email address does the supplier need to send to? Users will find this after saving
|
||||
the supplier after _Send to_.
|
||||
What email address does the supplier need to send to? Users will find this after initial creating and
|
||||
saving the supplier after _Send to_.
|
||||
|
||||
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`.
|
||||
This is useful when you're running an email server in front to route mails.
|
||||
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 a mail server in front to route mails.
|
|
@ -22,6 +22,14 @@ class ApplicationController < ActionController::Base
|
|||
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!
|
||||
@supplier = Supplier.find((params[:supplier_id] || params[:id]))
|
||||
unless current_user.has_access_to?(@supplier)
|
||||
|
|
|
@ -6,14 +6,14 @@ class SessionsController < ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
user = User.authenticate(params[:email], params[:password])
|
||||
if user
|
||||
user = User.find_by(email: params[:email])
|
||||
if user && user.authenticate(params[:password])
|
||||
session[:user_id] = user.id
|
||||
flash[:notice] = "Logged in!"
|
||||
redirect_to root_url
|
||||
else
|
||||
flash.now[:error] = "Invalid email or password"
|
||||
render "new"
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
class UsersController < ApplicationController
|
||||
|
||||
before_action :admin_required!
|
||||
|
||||
def new
|
||||
@user=User.new
|
||||
end
|
||||
|
@ -6,9 +9,10 @@ class UsersController < ApplicationController
|
|||
def create
|
||||
@user = User.new(user_params)
|
||||
if @user.save
|
||||
render 'show'
|
||||
flash[:notice] = "Konto wurde erfolgreich erstellt."
|
||||
redirect_to @user
|
||||
else
|
||||
redirect_to new_user_path
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -18,16 +22,11 @@ class UsersController < ApplicationController
|
|||
|
||||
def update
|
||||
@user = User.find(params[:id])
|
||||
attrs = user_params
|
||||
respond_to do |format|
|
||||
if @user.update(attrs)
|
||||
if @user.update(user_params)
|
||||
flash[:notice] = 'Konto wurde erfolgreich aktualisiert.'
|
||||
format.html { redirect_to user_url(@user) }
|
||||
format.xml { head :ok }
|
||||
redirect_to @user
|
||||
else
|
||||
format.html { render :action => "edit" }
|
||||
format.xml { render :xml => @user.errors.to_xml }
|
||||
end
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -50,6 +49,6 @@ class UsersController < ApplicationController
|
|||
|
||||
private
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :password)
|
||||
params.require(:user).permit(:email, :password, :password_confirmation, :admin)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,37 +3,42 @@ class User < ApplicationRecord
|
|||
has_many :user_accesses, :dependent => :destroy
|
||||
has_many :suppliers, :through => :user_accesses
|
||||
|
||||
attr_reader :password
|
||||
|
||||
attr_accessor :password
|
||||
before_save :encrypt_password
|
||||
|
||||
validates_confirmation_of :password
|
||||
validates_presence_of :password, :on => :create
|
||||
validates_presence_of :email
|
||||
validates_uniqueness_of :email
|
||||
|
||||
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
|
||||
validates :email, presence: true, uniqueness: true
|
||||
validates :password, confirmation: true
|
||||
validate do |user|
|
||||
unless user.password_hash.present? && user.password_salt.present?
|
||||
user.errors.add :password, :blank
|
||||
end
|
||||
end
|
||||
|
||||
def encrypt_password
|
||||
if password.present?
|
||||
self.password_salt = BCrypt::Engine.generate_salt
|
||||
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
|
||||
end
|
||||
def self.attributes_protected_by_default
|
||||
super + %w(password_hash password_salt)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def admin?
|
||||
!!admin
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
= f.input :email, required: true
|
||||
= f.input :password, required: true
|
||||
= f.input :password_confirmation, required: true
|
||||
= f.input :admin, required: true
|
||||
|
||||
.form-actions
|
||||
= f.submit class: 'btn'
|
||||
|
|
|
@ -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
|
9
config/database.yml.SAMPLE
Normal file
9
config/database.yml.SAMPLE
Normal file
|
@ -0,0 +1,9 @@
|
|||
development:
|
||||
adapter: mysql2
|
||||
encoding: utf8
|
||||
reconnect: false
|
||||
database: development
|
||||
pool: 5
|
||||
username: root
|
||||
password: secret
|
||||
host: mysql
|
|
@ -1,6 +1,4 @@
|
|||
Rails.application.routes.draw do
|
||||
get 'users/new'
|
||||
get 'users/show'
|
||||
get 'log_in' => 'sessions#new', :as => :log_in
|
||||
match 'log_out' => 'sessions#destroy', :as => :log_out, :via => [:get, :post]
|
||||
resources :sessions, :only => [:new, :create, :destroy]
|
||||
|
@ -18,8 +16,4 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
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
|
||||
|
|
5
db/migrate/20211219074758_index_users_by_unique_email.rb
Normal file
5
db/migrate/20211219074758_index_users_by_unique_email.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class IndexUsersByUniqueEmail < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_index :users, :email, unique: true
|
||||
end
|
||||
end
|
71
db/schema.rb
generated
71
db/schema.rb
generated
|
@ -1,44 +1,42 @@
|
|||
# encoding: UTF-8
|
||||
# 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
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
#
|
||||
# Note that this schema.rb definition is the authoritative source for your
|
||||
# database schema. If you need to create the application database on another
|
||||
# system, you should be using db:schema:load, not running all the migrations
|
||||
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
||||
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
||||
# This file is the source Rails uses to define your schema when running `bin/rails
|
||||
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
||||
# be faster and is potentially less error prone than running all of your
|
||||
# migrations from scratch. Old migrations may fail to apply correctly if those
|
||||
# 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|
|
||||
t.string "name", :null => false
|
||||
t.integer "supplier_id", :null => false
|
||||
create_table "articles", charset: "utf8", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.integer "supplier_id", null: false
|
||||
t.string "number"
|
||||
t.string "note"
|
||||
t.string "manufacturer"
|
||||
t.string "origin"
|
||||
t.string "unit"
|
||||
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 "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 "scale_quantity", :precision => 4, :scale => 2
|
||||
t.decimal "scale_price", :precision => 8, :scale => 2
|
||||
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 "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 "scale_quantity", precision: 4, scale: 2
|
||||
t.decimal "scale_price", precision: 8, scale: 2
|
||||
t.datetime "created_on"
|
||||
t.datetime "updated_on"
|
||||
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
|
||||
|
||||
add_index "articles", ["name"], :name => "index_articles_on_name"
|
||||
add_index "articles", ["number", "supplier_id"], :name => "index_articles_on_number_and_supplier_id", :unique => true
|
||||
|
||||
create_table "suppliers", :force => true do |t|
|
||||
t.string "name", :null => false
|
||||
t.string "address", :null => false
|
||||
t.string "phone", :null => false
|
||||
create_table "suppliers", charset: "utf8", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "address", null: false
|
||||
t.string "phone", null: false
|
||||
t.string "phone2"
|
||||
t.string "fax"
|
||||
t.string "email"
|
||||
|
@ -47,39 +45,38 @@ ActiveRecord::Schema.define(:version => 20190811115732) do
|
|||
t.string "note"
|
||||
t.datetime "created_on"
|
||||
t.datetime "updated_on"
|
||||
t.boolean "ftp_sync", :default => false
|
||||
t.boolean "ftp_sync", default: false
|
||||
t.string "ftp_host"
|
||||
t.string "ftp_user"
|
||||
t.string "ftp_password"
|
||||
t.string "ftp_type", :default => "bnn", :null => false
|
||||
t.string "ftp_regexp", :default => "^([.]/)?PL"
|
||||
t.string "ftp_type", default: "bnn", null: false
|
||||
t.string "ftp_regexp", default: "^([.]/)?PL"
|
||||
t.boolean "mail_sync"
|
||||
t.string "mail_from"
|
||||
t.string "mail_subject"
|
||||
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
|
||||
|
||||
add_index "suppliers", ["name"], :name => "index_suppliers_on_name", :unique => true
|
||||
|
||||
create_table "user_accesses", :force => true do |t|
|
||||
create_table "user_accesses", charset: "utf8", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "supplier_id"
|
||||
t.datetime "created_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
|
||||
|
||||
add_index "user_accesses", ["supplier_id"], :name => "index_user_accesses_on_supplier_id"
|
||||
add_index "user_accesses", ["user_id", "supplier_id"], :name => "index_user_accesses_on_user_id_and_supplier_id"
|
||||
add_index "user_accesses", ["user_id"], :name => "index_user_accesses_on_user_id"
|
||||
|
||||
create_table "users", :force => true do |t|
|
||||
create_table "users", charset: "utf8", force: :cascade do |t|
|
||||
t.string "email"
|
||||
t.string "password_hash"
|
||||
t.string "password_salt"
|
||||
t.datetime "created_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
|
||||
|
|
Loading…
Reference in a new issue