├── log └── .keep ├── storage └── .keep ├── tmp ├── .keep └── pids │ └── .keep ├── vendor └── .keep ├── lib └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt └── site.webmanifest ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ └── contact_test.rb ├── system │ ├── .keep │ ├── login_test.rb │ ├── contacts_test.rb │ ├── organizations_test.rb │ └── users_test.rb ├── controllers │ └── .keep ├── integration │ └── .keep ├── factories │ ├── accounts.rb │ ├── users.rb │ ├── organizations.rb │ └── contacts.rb ├── test_helper.rb └── application_system_test_case.rb ├── .ruby-version ├── app ├── models │ ├── concerns │ │ ├── .keep │ │ └── soft_delete.rb │ ├── application_record.rb │ ├── account.rb │ ├── organization.rb │ ├── contact.rb │ ├── ability.rb │ └── user.rb ├── controllers │ ├── concerns │ │ ├── .keep │ │ ├── inertia_flash.rb │ │ ├── auth.rb │ │ ├── inertia_json.rb │ │ └── inertia_csrf.rb │ ├── reports_controller.rb │ ├── users │ │ └── sessions_controller.rb │ ├── dashboard_controller.rb │ ├── application_controller.rb │ ├── organizations_controller.rb │ ├── contacts_controller.rb │ └── users_controller.rb ├── views │ └── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb ├── helpers │ └── application_helper.rb ├── javascript │ ├── images │ │ └── favicon.png │ ├── Layouts │ │ ├── Minimal.vue │ │ └── Main.vue │ ├── Shared │ │ ├── LoadingButton.vue │ │ ├── TrashedMessage.vue │ │ ├── TextareaInput.vue │ │ ├── SelectInput.vue │ │ ├── TextInput.vue │ │ ├── Dropdown.vue │ │ ├── SearchFilter.vue │ │ ├── FileInput.vue │ │ ├── Logo.vue │ │ ├── Pagination.vue │ │ ├── MainMenu.vue │ │ ├── Modal.vue │ │ ├── Icon.vue │ │ └── FlashMessages.vue │ ├── Pages │ │ ├── Reports │ │ │ └── Index.vue │ │ ├── Organizations │ │ │ ├── _New.vue │ │ │ ├── Form.vue │ │ │ ├── Edit.vue │ │ │ └── Index.vue │ │ ├── Error.vue │ │ ├── Dashboard │ │ │ └── Index.vue │ │ ├── Users │ │ │ ├── New.vue │ │ │ ├── Form.vue │ │ │ └── Edit.vue │ │ ├── Contacts │ │ │ ├── New.vue │ │ │ ├── Edit.vue │ │ │ └── Form.vue │ │ └── Auth │ │ │ └── Login.vue │ ├── styles │ │ ├── buttons.css │ │ ├── transitions.css │ │ ├── form.css │ │ └── application.css │ └── entrypoints │ │ └── application.js ├── mailers │ └── application_mailer.rb └── services │ └── auth_failure.rb ├── .browserslistrc ├── bin ├── dev ├── rake ├── brakeman ├── rails ├── ci ├── bundler-audit ├── rubocop ├── yarn ├── vite ├── setup └── bundle ├── Procfile.dev ├── .prettierignore ├── lighthouse.png ├── screenshot.jpg ├── Procfile ├── .yarnrc.yml ├── .prettierrc.json ├── config ├── initializers │ ├── oj.rb │ ├── inertia_rails.rb │ ├── mime_types.rb │ ├── session_store.rb │ ├── git.rb │ ├── js_routes.rb │ ├── wrap_parameters.rb │ ├── permissions_policy.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── environment.rb ├── boot.rb ├── bundler-audit.yml ├── vite.json ├── ci.rb ├── locales │ ├── en.yml │ └── devise.en.yml ├── routes.rb ├── storage.yml ├── puma.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── application.rb └── database.yml ├── config.ru ├── .vscode ├── extensions.json └── settings.json ├── Rakefile ├── db ├── migrate │ ├── 20191129182209_create_accounts.rb │ ├── 20241110120021_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ ├── 20191129183932_create_users.rb │ ├── 20191129182602_create_organizations.rb │ ├── 20201210064113_add_service_name_to_active_storage_blobs.rb │ ├── 20201210064131_create_active_storage_variant_records.rb │ ├── 20191129182823_create_contacts.rb │ ├── 20191201161437_add_devise_to_users.rb │ └── 20191202122725_create_active_storage_tables.active_storage.rb ├── seeds.rb └── schema.rb ├── .dockerignore ├── .gitattributes ├── docker └── startup.sh ├── .yarnclean ├── Dockerfile ├── .github ├── workflows │ ├── automerge.yml │ ├── security-checks.yml │ ├── dedupe.yml │ └── push.yml └── dependabot.yml ├── vite.config.mts ├── eslint.config.mjs ├── package.json ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── .rubocop.yml └── Gemfile /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.8 2 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | not IE 11 3 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | foreman start -f Procfile.dev 2 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | backend: bin/rails s -p 3000 2 | frontend: bin/vite dev 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /app/javascript/routes.js 2 | db/schema.rb 3 | public/* 4 | -------------------------------------------------------------------------------- /lighthouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledermann/pingcrm/HEAD/lighthouse.png -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledermann/pingcrm/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p $PORT -e $RAILS_ENV 2 | release: rake db:migrate 3 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableScripts: false 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.12.0.cjs 6 | -------------------------------------------------------------------------------- /app/javascript/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ledermann/pingcrm/HEAD/app/javascript/images/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /test/factories/accounts.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :account do 3 | name { 'Acme Corporation' } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("brakeman", "brakeman") 6 | -------------------------------------------------------------------------------- /config/initializers/oj.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext' 2 | require 'active_support/json' 3 | require 'oj' 4 | Oj.optimize_rails 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /app/javascript/Layouts/Minimal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/inertia_rails.rb: -------------------------------------------------------------------------------- 1 | InertiaRails.configure do |config| 2 | config.version = ViteRuby.digest 3 | config.always_include_errors_hash = true 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/reports_controller.rb: -------------------------------------------------------------------------------- 1 | class ReportsController < ApplicationController 2 | def index 3 | render inertia: 'Reports/Index', props: {} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PingCRM on Rails", 3 | "short_name": "PingCRM", 4 | "display": "standalone", 5 | "scope": "/", 6 | "start_url": "/" 7 | } 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'active_support/continuous_integration' 4 | 5 | CI = ActiveSupport::ContinuousIntegration 6 | require_relative '../config/ci.rb' 7 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.session_store :cookie_store, 2 | key: '_pingcrm_session', 3 | same_site: :strict 4 | -------------------------------------------------------------------------------- /test/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | first_name { 'John' } 4 | last_name { 'Doe' } 5 | email { Faker::Internet.unique.email } 6 | password { 'secret' } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /app/models/account.rb: -------------------------------------------------------------------------------- 1 | class Account < ApplicationRecord 2 | has_many :organizations, dependent: :destroy 3 | has_many :contacts, dependent: :destroy 4 | has_many :users, dependent: :destroy 5 | 6 | validates :name, presence: true 7 | end 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "ruby-syntax-tree.vscode-syntax-tree", 7 | "github.copilot" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /bin/bundler-audit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'bundler/audit/cli' 4 | 5 | if ARGV.empty? || ARGV.include?('check') 6 | ARGV.concat %w[--config config/bundler-audit.yml] 7 | end 8 | Bundler::Audit::CLI.start 9 | -------------------------------------------------------------------------------- /config/initializers/git.rb: -------------------------------------------------------------------------------- 1 | Rails.configuration.x.git.commit_sha = 2 | ENV.fetch('COMMIT_SHA') { `git rev-parse HEAD` }.first(7) 3 | Rails.configuration.x.git.commit_time = 4 | Time.zone.parse(ENV.fetch('COMMIT_TIME') { `git show -s --format=%cI` }) 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /db/migrate/20191129182209_create_accounts.rb: -------------------------------------------------------------------------------- 1 | class CreateAccounts < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :accounts do |t| 4 | t.string :name, null: false 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/models/contact_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ContactTest < ActiveSupport::TestCase 4 | test 'should not save contact without first name and last name' do 5 | contact = Contact.new 6 | assert_not contact.save 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/bundler-audit.yml: -------------------------------------------------------------------------------- 1 | # Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. 2 | # CVEs that are not relevant to the application can be enumerated on the ignore list below. 3 | 4 | ignore: 5 | - CVE-THAT-DOES-NOT-APPLY 6 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | # Explicit RuboCop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift('--config', File.expand_path('../.rubocop.yml', __dir__)) 7 | 8 | load Gem.bin_path('rubocop', 'rubocop') 9 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/controllers/concerns/inertia_flash.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | # Make flash messages available as shared data 4 | # 5 | module InertiaFlash 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | inertia_share flash: -> { { success: flash.notice, alert: flash.alert } } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/js_routes.rb: -------------------------------------------------------------------------------- 1 | JsRoutes.setup do |config| 2 | # Setup your JS module system: 3 | # ESM, CJS, AMD, UMD or nil 4 | config.module_type = 'ESM' 5 | 6 | config.exclude = [ 7 | # Default Rails routes not required from Inertia.js 8 | /rails_/, 9 | ] 10 | 11 | config.compact = true 12 | end 13 | -------------------------------------------------------------------------------- /app/javascript/Shared/LoadingButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /app/controllers/users/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Users::SessionsController < Devise::SessionsController 2 | # GET /login 3 | def new 4 | render inertia: 'Auth/Login', props: {} 5 | end 6 | 7 | # POST /login 8 | def create 9 | super 10 | end 11 | 12 | # DELETE /logout 13 | def destroy 14 | super 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "sourceCodeDir": "app/javascript", 4 | "watchAdditionalPaths": [] 5 | }, 6 | "development": { 7 | "autoBuild": true, 8 | "publicOutputDir": "vite-dev", 9 | "port": 3036 10 | }, 11 | "test": { 12 | "autoBuild": true, 13 | "publicOutputDir": "vite-test", 14 | "port": 3037 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/services/auth_failure.rb: -------------------------------------------------------------------------------- 1 | # Devise: Redirect user to login page on auth failure 2 | # 3 | class AuthFailure < Devise::FailureApp 4 | def http_auth? 5 | if request.inertia? 6 | # Explicitly disable HTTP authentication on Inertia 7 | # requests and force a redirect on failure 8 | false 9 | else 10 | super 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20241110120021_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | change_column_null(:active_storage_blobs, :checksum, true) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .byebug_history 2 | .env.example 3 | .git 4 | .gitignore 5 | .dockerignore 6 | .github 7 | .eslintignore 8 | .eslintrc.js 9 | .foreman 10 | .rubocop.yml 11 | .yarnclean 12 | Dockerfile 13 | docker-compose.yml 14 | log/* 15 | node_modules 16 | public/assets/* 17 | Procfile 18 | Procfile.dev 19 | tmp/* 20 | !/tmp/pids/ 21 | !/tmp/pids/.keep 22 | yarn-error.log 23 | screenshot.jpg 24 | lighthouse.png 25 | **/*.DS_Store 26 | -------------------------------------------------------------------------------- /app/javascript/Pages/Reports/Index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /app/models/organization.rb: -------------------------------------------------------------------------------- 1 | class Organization < ApplicationRecord 2 | belongs_to :account 3 | has_many :contacts, dependent: :destroy 4 | 5 | validates :name, presence: true 6 | 7 | include SoftDelete 8 | 9 | scope :search, 10 | ->(query) { 11 | if query.present? 12 | where('organizations.name ILIKE ?', "%#{query}%") 13 | else 14 | all 15 | end 16 | } 17 | end 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | # Mark the database schema as having been generated. 3 | db/schema.rb linguist-generated 4 | # Mark the yarn lockfile as having been generated. 5 | yarn.lock linguist-generated 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /test/factories/organizations.rb: -------------------------------------------------------------------------------- 1 | require 'faker' 2 | 3 | FactoryBot.define do 4 | factory :organization do 5 | account 6 | name { Faker::Company.name } 7 | email { Faker::Internet.unique.email } 8 | phone { Faker::PhoneNumber.phone_number } 9 | address { Faker::Address.street_address } 10 | city { Faker::Address.city } 11 | region { Faker::Address.state } 12 | country { 'US' } 13 | postal_code { Faker::Address.postcode } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require_relative '../config/environment' 3 | require 'rails/test_help' 4 | require 'capybara/rails' 5 | 6 | Capybara.server = :puma, { Silent: true } # To clean up your test output 7 | 8 | class ActiveSupport::TestCase 9 | # Run tests in parallel with specified workers 10 | parallelize(workers: :number_of_processors) 11 | 12 | include FactoryBot::Syntax::Methods 13 | 14 | # Add more helper methods to be used by all tests here... 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20191129183932_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :users do |t| 4 | t.belongs_to :account, null: false, foreign_key: true 5 | t.string :first_name, null: false 6 | t.string :last_name, null: false 7 | t.string :email, null: false 8 | t.string :password 9 | t.boolean :owner, null: false, default: false 10 | t.datetime :deleted_at 11 | 12 | t.timestamps 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/factories/contacts.rb: -------------------------------------------------------------------------------- 1 | require 'faker' 2 | 3 | FactoryBot.define do 4 | factory :contact do 5 | organization 6 | last_name { Faker::Name.last_name } 7 | first_name { Faker::Name.first_name } 8 | email { Faker::Internet.unique.email } 9 | phone { Faker::PhoneNumber.phone_number } 10 | address { Faker::Address.street_address } 11 | city { Faker::Address.city } 12 | region { Faker::Address.state } 13 | country { 'US' } 14 | postal_code { Faker::Address.postcode } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class DashboardController < ApplicationController 2 | def index 3 | render inertia: 'Dashboard/Index', 4 | props: { 5 | git: { 6 | commit_time: Rails.configuration.x.git.commit_time, 7 | commit_sha: Rails.configuration.x.git.commit_sha, 8 | commit_url: 9 | "https://github.com/ledermann/pingcrm/commits/#{Rails.configuration.x.git.commit_sha}", 10 | }, 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20191129182602_create_organizations.rb: -------------------------------------------------------------------------------- 1 | class CreateOrganizations < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :organizations do |t| 4 | t.belongs_to :account, null: false, foreign_key: true 5 | t.string :name, null: false 6 | t.string :email 7 | t.string :phone 8 | t.string :address 9 | t.string :city 10 | t.string :region 11 | t.string :country 12 | t.string :postal_code 13 | t.datetime :deleted_at 14 | 15 | t.timestamps 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/concerns/auth.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module Auth 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before_action :authenticate_user! 8 | 9 | rescue_from CanCan::AccessDenied do 10 | render inertia: 'Error', props: { status: 403 } 11 | end 12 | end 13 | 14 | private 15 | 16 | def after_sign_in_path_for(resource) 17 | stored_location_for(resource) || root_path 18 | end 19 | 20 | def after_sign_out_path_for(_resource_or_scope) 21 | new_user_session_path 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) { wrap_parameters format: [:json] } 8 | 9 | # To enable root element in JSON for ActiveRecord objects. 10 | # ActiveSupport.on_load(:active_record) do 11 | # self.include_root_in_json = true 12 | # end 13 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /db/migrate/20201210064113_add_service_name_to_active_storage_blobs.rb: -------------------------------------------------------------------------------- 1 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 2 | def up 3 | add_column :active_storage_blobs, :service_name, :string 4 | 5 | if (configured_service = ActiveStorage::Blob.service.name) 6 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 7 | end 8 | 9 | change_column :active_storage_blobs, :service_name, :string, null: false 10 | end 11 | 12 | def down 13 | remove_column :active_storage_blobs, :service_name 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += %i[ 7 | passw 8 | email 9 | secret 10 | token 11 | _key 12 | crypt 13 | salt 14 | certificate 15 | otp 16 | ssn 17 | cvv 18 | cvc 19 | ] 20 | -------------------------------------------------------------------------------- /docker/startup.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | echo "Starting ..." 4 | echo "Git commit: $COMMIT_SHA - $COMMIT_TIME" 5 | echo "----------------" 6 | 7 | # Wait for PostgreSQL 8 | until nc -z -v -w30 "$DB_HOST" 5432; do 9 | echo "Waiting for PostgreSQL on $DB_HOST:5432 ..." 10 | sleep 1 11 | done 12 | echo "PostgreSQL is up and running!" 13 | 14 | # If running the rails server then create or migrate existing database 15 | if [ "${*}" == "./bin/rails server" ]; then 16 | echo "Preparing database..." 17 | ./bin/rails db:prepare 18 | echo "Database is ready!" 19 | fi 20 | 21 | exec "${@}" 22 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] 9 | -------------------------------------------------------------------------------- /db/migrate/20201210064131_create_active_storage_variant_records.rb: -------------------------------------------------------------------------------- 1 | class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :active_storage_variant_records do |t| # rubocop:disable Rails/CreateTableWithTimestamps 4 | t.belongs_to :blob, null: false, index: false 5 | t.string :variation_digest, null: false 6 | 7 | t.index [:blob_id, :variation_digest], 8 | name: 'index_active_storage_variant_records_uniqueness', 9 | unique: true 10 | t.foreign_key :active_storage_blobs, column: :blob_id 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/javascript/styles/buttons.css: -------------------------------------------------------------------------------- 1 | .btn-spinner, 2 | .btn-spinner:after { 3 | border-radius: 50%; 4 | width: 1.5em; 5 | height: 1.5em; 6 | } 7 | 8 | .btn-spinner { 9 | font-size: 10px; 10 | position: relative; 11 | text-indent: -9999em; 12 | border-top: 0.2em solid white; 13 | border-right: 0.2em solid white; 14 | border-bottom: 0.2em solid white; 15 | border-left: 0.2em solid transparent; 16 | transform: translateZ(0); 17 | animation: spinning 1s infinite linear; 18 | } 19 | 20 | @keyframes spinning { 21 | 0% { 22 | transform: rotate(0deg); 23 | } 24 | 100% { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /db/migrate/20191129182823_create_contacts.rb: -------------------------------------------------------------------------------- 1 | class CreateContacts < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :contacts do |t| 4 | t.belongs_to :account, null: false, foreign_key: true 5 | t.belongs_to :organization, foreign_key: true 6 | t.string :first_name, null: false 7 | t.string :last_name, null: false 8 | t.string :email 9 | t.string :phone 10 | t.string :address 11 | t.string :city 12 | t.string :region 13 | t.string :country 14 | t.string :postal_code 15 | t.datetime :deleted_at 16 | 17 | t.timestamps 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | APP_ROOT = File.expand_path('..', __dir__) 5 | Dir.chdir(APP_ROOT) do 6 | executable_path = ENV['PATH'].split(File::PATH_SEPARATOR).find do |path| 7 | normalized_path = File.expand_path(path) 8 | normalized_path != __dir__ && File.executable?(Pathname.new(normalized_path).join('yarn')) 9 | end 10 | if executable_path 11 | exec File.expand_path(Pathname.new(executable_path).join('yarn')), *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | 13 | # examples 14 | example 15 | examples 16 | 17 | # code coverage directories 18 | coverage 19 | .nyc_output 20 | 21 | # build scripts 22 | Makefile 23 | Gulpfile.js 24 | Gruntfile.js 25 | 26 | # configs 27 | appveyor.yml 28 | circle.yml 29 | codeship-services.yml 30 | codeship-steps.yml 31 | wercker.yml 32 | .tern-project 33 | .gitattributes 34 | .editorconfig 35 | .*ignore 36 | .eslintrc 37 | .jshintrc 38 | .flowconfig 39 | .documentup.json 40 | .yarn-metadata.json 41 | .travis.yml 42 | 43 | # misc 44 | *.md 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | ARG SKIP_BOOTSNAP_PRECOMPILE=true 5 | 6 | FROM ghcr.io/ledermann/rails-base-builder:3.4.8-alpine AS builder 7 | 8 | # Remove some files not needed in resulting image 9 | RUN rm .browserslistrc package.json vite.config.mts 10 | 11 | FROM ghcr.io/ledermann/rails-base-final:3.4.8-alpine 12 | LABEL maintainer="georg@ledermann.dev" 13 | 14 | # Add Alpine packages 15 | RUN apk add --no-cache vips 16 | 17 | USER app 18 | 19 | # Enable YJIT 20 | ENV RUBY_YJIT_ENABLE=1 21 | 22 | # Entrypoint prepares the database. 23 | ENTRYPOINT ["docker/startup.sh"] 24 | 25 | # Start the server by default, this can be overwritten at runtime 26 | CMD ["./bin/rails", "server"] 27 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | include Devise::Test::IntegrationHelpers 5 | 6 | driven_by :selenium, 7 | using: :headless_chrome, 8 | screen_size: [1400, 1400] do |driver_option| 9 | driver_option.add_argument('--disable-ipc-flooding-protection') 10 | 11 | # Chrome 120 compatibility 12 | driver_option.add_argument '--headless=new' 13 | end 14 | 15 | teardown do 16 | messages = 17 | page 18 | .driver 19 | .browser 20 | .logs 21 | .get(:browser) 22 | .map { |log| "[#{log.level}] #{log.message}" } 23 | 24 | assert_empty(messages) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/javascript/Shared/TrashedMessage.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 32 | -------------------------------------------------------------------------------- /app/controllers/concerns/inertia_json.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | # Helper method to build a complex Hash with Jbuilder 4 | # 5 | # Usage example: 6 | # 7 | # class ContactsController < ApplicationController 8 | # def index 9 | # render inertia: 'Contacts/Index', props: { 10 | # contacts: { 11 | # jbuilder do |json| 12 | # json.data(contacts) do |contact| 13 | # json.(contact, :id, :name) 14 | # json.organization(contact.organization, :name) 15 | # end 16 | # end 17 | # } 18 | # } 19 | # end 20 | # end 21 | # 22 | module InertiaJson 23 | extend ActiveSupport::Concern 24 | 25 | def jbuilder(&) 26 | JbuilderTemplate.new(view_context, &).attributes! 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: '${{ secrets.PAT }}' 18 | 19 | - name: Enable auto-merge for Dependabot PRs 20 | if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} 21 | run: gh pr merge --auto --squash "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GITHUB_TOKEN: ${{secrets.PAT}} 25 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Auth 3 | 4 | include Pagy::Method 5 | 6 | include InertiaCsrf 7 | include InertiaFlash 8 | include InertiaJson 9 | 10 | inertia_share auth: -> { 11 | { 12 | user: 13 | current_user.as_json( 14 | only: %i[id first_name last_name], 15 | include: { 16 | account: { 17 | only: %i[id name], 18 | }, 19 | }, 20 | ), 21 | } 22 | } 23 | 24 | private 25 | 26 | def pagy_metadata(pagy) 27 | pagy.data_hash 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | 6 | "files.trimTrailingWhitespace": true, 7 | "files.watcherExclude": { 8 | "**/tmp/**": true, 9 | "**/public/vite*": true 10 | }, 11 | 12 | "search.exclude": { 13 | "**/.yarn": true, 14 | "**/tmp": true, 15 | "**/public/vite*": true, 16 | "**/node_modules": true 17 | }, 18 | 19 | "eslint.validate": ["javascript", "vue"], 20 | 21 | "ruby.lint": { 22 | "rubocop": { 23 | "forceExclusion": true 24 | } 25 | }, 26 | 27 | "[ruby]": { 28 | "editor.defaultFormatter": "ruby-syntax-tree.vscode-syntax-tree", 29 | "editor.formatOnSave": true 30 | }, 31 | 32 | "syntaxTree.singleQuotes": true, 33 | "syntaxTree.trailingComma": true 34 | } 35 | -------------------------------------------------------------------------------- /app/models/contact.rb: -------------------------------------------------------------------------------- 1 | class Contact < ApplicationRecord 2 | belongs_to :account 3 | belongs_to :organization, optional: true 4 | 5 | validates :first_name, :last_name, presence: true 6 | 7 | include SoftDelete 8 | 9 | scope :order_by_name, -> { order(:last_name, :first_name) } 10 | 11 | scope :search, 12 | ->(query) { 13 | if query.present? 14 | left_joins(:organization).where( 15 | 'contacts.first_name ILIKE :query OR 16 | contacts.last_name ILIKE :query OR 17 | contacts.email ILIKE :query OR 18 | organizations.name ILIKE :query', 19 | query: "%#{query}%", 20 | ) 21 | else 22 | all 23 | end 24 | } 25 | 26 | def name 27 | "#{last_name}, #{first_name}" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/javascript/styles/transitions.css: -------------------------------------------------------------------------------- 1 | /* Source: https: //github.com/adamwathan/vue-tailwind-examples */ 2 | 3 | .origin-top-right { 4 | transform-origin: top right; 5 | } 6 | 7 | .transition-all { 8 | transition-property: all; 9 | } 10 | 11 | .transition-fastest { 12 | transition-duration: 50ms; 13 | } 14 | 15 | .transition-faster { 16 | transition-duration: 100ms; 17 | } 18 | 19 | .transition-fast { 20 | transition-duration: 150ms; 21 | } 22 | 23 | .transition-medium { 24 | transition-duration: 200ms; 25 | } 26 | 27 | .ease-out-quad { 28 | transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94); 29 | } 30 | 31 | .ease-in-quad { 32 | transition-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53); 33 | } 34 | 35 | .scale-70 { 36 | transform: scale(0.7); 37 | } 38 | 39 | .scale-100 { 40 | transform: scale(1); 41 | } 42 | -------------------------------------------------------------------------------- /bin/vite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'vite' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("vite_ruby", "vite") 28 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import tailwindcss from '@tailwindcss/vite'; 3 | import ViteRails from 'vite-plugin-rails'; 4 | import VuePlugin from '@vitejs/plugin-vue'; 5 | import { resolve } from 'path'; 6 | 7 | export default defineConfig({ 8 | build: { 9 | assetsInlineLimit: 0, 10 | rollupOptions: { 11 | output: { 12 | manualChunks(id) { 13 | if (id.includes('node_modules')) { 14 | return 'vendor'; 15 | } 16 | }, 17 | }, 18 | }, 19 | }, 20 | plugins: [ 21 | tailwindcss(), 22 | ViteRails({ 23 | fullReload: { 24 | additionalPaths: ['config/routes.rb', 'app/views/**/*'], 25 | }, 26 | }), 27 | VuePlugin(), 28 | ], 29 | resolve: { 30 | alias: { 31 | '@': resolve(__dirname, 'app/javascript'), 32 | }, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /app/models/concerns/soft_delete.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module SoftDelete 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | scope :not_trashed, -> { where(deleted_at: nil) } 8 | scope :only_trashed, -> { where.not(deleted_at: nil) } 9 | scope :with_trashed, -> { all } 10 | 11 | scope :trash_filter, 12 | ->(name) { 13 | case name 14 | when 'with' 15 | with_trashed 16 | when 'only' 17 | only_trashed 18 | else 19 | not_trashed 20 | end 21 | } 22 | end 23 | 24 | def soft_delete 25 | update deleted_at: Time.current 26 | end 27 | 28 | def soft_delete! 29 | update! deleted_at: Time.current 30 | end 31 | 32 | def restore 33 | update deleted_at: nil 34 | end 35 | 36 | def restore! 37 | update! deleted_at: nil 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /config/ci.rb: -------------------------------------------------------------------------------- 1 | # Run using bin/ci 2 | 3 | CI.run do 4 | step 'Setup', 'bin/setup --skip-server' 5 | 6 | step 'Style: Ruby', 'bin/rubocop' 7 | 8 | step 'Security: npm audit', 'bin/yarn npm audit --recursive' 9 | step 'Security: Bundler audit', 'bin/bundler-audit' 10 | step 'Security: Gem audit', 'bin/bundler-audit' 11 | step 'Security: Brakeman code analysis', 12 | 'bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error' 13 | 14 | step 'Tests: Rails', 'bin/rails test' 15 | step 'Tests: System', 'bin/rails test:system' 16 | step 'Tests: Seeds', 'env RAILS_ENV=test bin/rails db:seed:replant' 17 | 18 | # Optional: set a green GitHub commit status to unblock PR merge. 19 | # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. 20 | # if success? 21 | # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" 22 | # else 23 | # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." 24 | # end 25 | end 26 | -------------------------------------------------------------------------------- /.github/workflows/security-checks.yml: -------------------------------------------------------------------------------- 1 | name: Security checks 2 | on: 3 | schedule: 4 | - cron: '0 4 * * *' 5 | workflow_dispatch: 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | ruby-security: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v6 17 | 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | bundler-cache: true 22 | 23 | - name: Run security audit for Ruby gems 24 | run: bin/bundler-audit check --update 25 | 26 | yarn-security: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v6 31 | 32 | - name: Setup Node.js 33 | uses: actions/setup-node@v6 34 | with: 35 | cache: yarn 36 | 37 | - name: Install Yarn packages 38 | run: bin/yarn install --immutable 39 | 40 | - name: Run security audit for Yarn packages 41 | run: bin/yarn npm audit --recursive 42 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: 'ON' 29 | 30 | en: 31 | hello: 'Hello world' 32 | -------------------------------------------------------------------------------- /app/models/ability.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | # Without login, nothing is possible 6 | return unless user 7 | 8 | # A deleted user can't do anything 9 | return if user.deleted_at? 10 | 11 | # All users can read and edit (but not add, create, update, destroy or restore) 12 | # users from the same account 13 | can :read, User, account_id: user.account_id, deleted_at: nil 14 | can :edit, User, account_id: user.account_id, deleted_at: nil 15 | 16 | # All users can manage non-deleted contacts and organization linked to same account 17 | can :manage, Contact, account_id: user.account_id, deleted_at: nil 18 | can :manage, Organization, account_id: user.account_id, deleted_at: nil 19 | 20 | return unless user.owner? 21 | 22 | # Admin users can manage deleted records, too (still restricted to same account) 23 | can :manage, User, account_id: user.account_id 24 | can :manage, Contact, account_id: user.account_id 25 | can :manage, Organization, account_id: user.account_id 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20191201161437_add_devise_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddDeviseToUsers < ActiveRecord::Migration[6.0] 4 | def up 5 | remove_column :users, :password 6 | 7 | change_table :users, bulk: true do |t| 8 | ## Database authenticatable 9 | t.string :encrypted_password, null: false, default: '' 10 | 11 | ## Recoverable 12 | t.string :reset_password_token 13 | t.datetime :reset_password_sent_at 14 | 15 | ## Rememberable 16 | t.datetime :remember_created_at 17 | end 18 | 19 | add_index :users, :email, unique: true 20 | add_index :users, :reset_password_token, unique: true 21 | end 22 | 23 | def down 24 | change_table :users, bulk: true do |t| 25 | t.remove_index :email 26 | t.remove_index :reset_password_token 27 | 28 | t.remove_column :encrypted_password 29 | t.remove_column :reset_password_sent_at 30 | t.remove_column :reset_password_token 31 | t.remove_column :remember_created_at 32 | 33 | t.add_column :password, :string 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import pluginVue from 'eslint-plugin-vue'; 4 | 5 | export default [ 6 | js.configs.recommended, 7 | ...pluginVue.configs['flat/recommended'], 8 | { 9 | languageOptions: { 10 | globals: { 11 | ...globals.browser, 12 | ...globals.node, 13 | }, 14 | ecmaVersion: 2024, 15 | }, 16 | rules: { 17 | 'vue/no-unused-vars': 'error', 18 | 'vue/multi-word-component-names': 'off', 19 | 'vue/require-default-prop': 'off', 20 | 'vue/no-reserved-component-names': 'off', 21 | 'vue/max-attributes-per-line': 'off', 22 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 23 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 24 | }, 25 | }, 26 | { 27 | ignores: [ 28 | '.ruby-lsp/', 29 | '.yarn/', 30 | 'app/javascript/routes.js', 31 | 'config/', 32 | 'db/', 33 | 'log/', 34 | 'node_modules/', 35 | 'public/', 36 | 'tmp/', 37 | 'vendor/', 38 | ], 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /db/migrate/20191202122725_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2] 3 | def change 4 | create_table :active_storage_blobs do |t| 5 | t.string :key, null: false 6 | t.string :filename, null: false 7 | t.string :content_type 8 | t.text :metadata 9 | t.bigint :byte_size, null: false 10 | t.string :checksum, null: false 11 | t.datetime :created_at, null: false 12 | 13 | t.index [:key], unique: true 14 | end 15 | 16 | create_table :active_storage_attachments do |t| 17 | t.string :name, null: false 18 | t.references :record, null: false, polymorphic: true, index: false 19 | t.references :blob, null: false 20 | 21 | t.datetime :created_at, null: false 22 | 23 | t.index [:record_type, :record_id, :name, :blob_id], 24 | name: 'index_active_storage_attachments_uniqueness', 25 | unique: true 26 | t.foreign_key :active_storage_blobs, column: :blob_id 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pingcrm", 3 | "private": true, 4 | "scripts": { 5 | "lint": "eslint --cache ." 6 | }, 7 | "dependencies": { 8 | "@inertiajs/vue3": "^2.3.3", 9 | "@plausible-analytics/tracker": "^0.4.4", 10 | "@popperjs/core": "^2.11.8", 11 | "lodash": "^4.17.21", 12 | "timeago.js": "^4.0.2", 13 | "uuid": "^13.0.0", 14 | "vue": "^3.5.26" 15 | }, 16 | "version": "0.1.0", 17 | "devDependencies": { 18 | "@tailwindcss/vite": "^4.1.18", 19 | "@types/node": "^25.0.3", 20 | "@vitejs/plugin-vue": "^6.0.3", 21 | "eslint": "^9.39.2", 22 | "eslint-plugin-vue": "^10.6.2", 23 | "tailwindcss": "^4.1.18", 24 | "vite": "^7.3.0", 25 | "vite-plugin-full-reload": "^1.2.0", 26 | "vite-plugin-rails": "^0.5.0", 27 | "vue-eslint-parser": "^10.2.0" 28 | }, 29 | "dependenciesMeta": { 30 | "@tailwindcss/oxide": { 31 | "built": true 32 | }, 33 | "esbuild": { 34 | "built": true 35 | }, 36 | "vite": { 37 | "built": true 38 | } 39 | }, 40 | "engines": { 41 | "node": ">=22" 42 | }, 43 | "packageManager": "yarn@4.12.0" 44 | } 45 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 3 | 4 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 5 | # Can be used by load balancers and uptime monitors to verify that the app is live. 6 | get 'up' => 'rails/health#show', :as => :rails_health_check 7 | 8 | devise_for :users, skip: %i[sessions passwords registrations] 9 | as :user do 10 | get 'login', to: 'users/sessions#new', as: :new_user_session 11 | post 'login', to: 'users/sessions#create', as: :user_session 12 | match 'logout', 13 | to: 'users/sessions#destroy', 14 | as: :destroy_user_session, 15 | via: Devise.mappings[:user].sign_out_via 16 | end 17 | 18 | resources :reports, only: [:index] 19 | resources :users, except: [:show] do 20 | member { put 'restore' } 21 | end 22 | resources :organizations, except: %i[show new] do 23 | member { put 'restore' } 24 | end 25 | resources :contacts, except: [:show] do 26 | member { put 'restore' } 27 | end 28 | 29 | root 'dashboard#index' 30 | end 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | /tmp/pids/* 22 | !/tmp/pids/ 23 | !/tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | 29 | /public/assets 30 | 31 | # Ignore master key for decrypting credentials and more. 32 | /config/master.key 33 | 34 | /node_modules 35 | 36 | # Vite on Rails 37 | /public/vite 38 | /public/vite-dev 39 | /public/vite-test 40 | 41 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 42 | .pnp.* 43 | .yarn/* 44 | !.yarn/patches 45 | !.yarn/plugins 46 | !.yarn/releases 47 | !.yarn/sdks 48 | !.yarn/versions 49 | .eslintcache 50 | -------------------------------------------------------------------------------- /app/javascript/Pages/Organizations/_New.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Georg Ledermann 4 | Based on the work of Jonathan Reinink 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= tag :meta, name: 'plausible-url', content: ENV['PLAUSIBLE_URL'] %> 11 | <%= tag :meta, name: 'app-host', content: ENV['APP_HOST'] %> 12 | 13 | <%= csrf_meta_tags %> 14 | <%= csp_meta_tag %> 15 | 16 | 17 | 18 | 19 | <%= vite_client_tag %> 20 | <%= vite_javascript_tag 'application' %> 21 | 22 | 23 | 24 | 27 | 28 | <%= yield %> 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/javascript/Shared/TextareaInput.vue: -------------------------------------------------------------------------------- 1 |