7 |
8 |
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 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
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 |
2 |
3 |
6 |
11 | Create Organization
12 |
13 |
14 |
15 |
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 |
2 |
3 |
4 |
13 |
14 | {{ errors[0] }}
15 |
16 |
17 |
18 |
19 |
51 |
--------------------------------------------------------------------------------
/app/controllers/concerns/inertia_csrf.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | # Store the CSRF token in a non-session cookie so Axios can access it
4 | # Name it as XSRF-TOKEN, because this is the Axios default
5 | #
6 | # More info: https://pragmaticstudio.com/tutorials/rails-session-cookies-for-api-authentication
7 | #
8 | module InertiaCsrf
9 | extend ActiveSupport::Concern
10 |
11 | included do
12 | before_action :set_csrf_cookie
13 |
14 | rescue_from ActionController::InvalidAuthenticityToken do
15 | redirect_back_or_to '/', notice: 'The page expired, please try again.'
16 | end
17 | end
18 |
19 | # Rails uses HTTP_X_CSRF_TOKEN, but axios sends HTTP_X_XSRF_TOKEN (different name, X instead of C)
20 | # By overriding `request_authenticity_tokens` we can tell Rails to check HTTP_X_XSRF_TOKEN, too
21 | # Source: https://github.com/rails/rails/blob/v6.0.3.2/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L305-L308
22 | def request_authenticity_tokens
23 | super << request.headers['HTTP_X_XSRF_TOKEN']
24 | end
25 |
26 | private
27 |
28 | def set_csrf_cookie
29 | cookies['XSRF-TOKEN'] = {
30 | value: form_authenticity_token,
31 | same_site: 'Strict',
32 | }
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
9 | # amazon:
10 | # service: S3
11 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
12 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
13 | # region: us-east-1
14 | # bucket: your_own_bucket-<%= Rails.env %>
15 |
16 | # Remember not to checkin your GCS keyfile to a repository
17 | # google:
18 | # service: GCS
19 | # project: your_project
20 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
21 | # bucket: your_own_bucket-<%= Rails.env %>
22 |
23 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
24 | # microsoft:
25 | # service: AzureStorage
26 | # storage_account_name: your_account_name
27 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
28 | # container: your_container_name-<%= Rails.env %>
29 |
30 | # mirror:
31 | # service: Mirror
32 | # primary: local
33 | # mirrors: [ amazon, google, microsoft ]
34 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 |
4 | APP_ROOT = File.expand_path('..', __dir__)
5 |
6 | def system!(*args)
7 | system(*args, exception: true)
8 | end
9 |
10 | FileUtils.chdir APP_ROOT do
11 | # This script is a way to set up or update your development environment automatically.
12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
13 | # Add necessary setup steps to this file.
14 |
15 | puts '== Installing dependencies =='
16 | system('bundle check') || system!('bundle install')
17 | system! 'gem install foreman'
18 | system! 'gem install syntax_tree'
19 |
20 | # Install JavaScript dependencies
21 | system! 'bin/yarn'
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:prepare'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | unless ARGV.include?('--skip-server')
35 | puts "\n== Restarting application server =="
36 | STDOUT.flush # flush the output before exec(2) so that it displays
37 | exec 'bin/rails restart'
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/javascript/Pages/Error.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
41 |
87 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | Rails.application.configure do
8 | config.content_security_policy do |policy|
9 | policy.font_src :self
10 | policy.img_src(*%i[self data].compact)
11 | policy.object_src :none
12 | policy.form_action :self
13 | policy.manifest_src :self
14 | policy.default_src :none
15 |
16 | if Rails.env.development?
17 | policy.connect_src :self,
18 | # Allow @vite/client to hot reload changes
19 | "ws://#{ViteRuby.config.host_with_port}"
20 |
21 | policy.script_src :self,
22 | # Allow @vite/client to hot reload JavaScript changes
23 | "http://#{ViteRuby.config.host_with_port}",
24 | # Allow Inertia.js to display error modal
25 | :unsafe_inline
26 |
27 | policy.style_src :self,
28 | # Allow @vite/client to hot reload CSS changes
29 | :unsafe_inline
30 | else
31 | policy.connect_src(*[:self, ENV.fetch('PLAUSIBLE_URL', nil)].compact)
32 | policy.script_src(*[:self].compact)
33 | policy.style_src :self,
34 | # Allow @inertiajs/progress to display progress bar
35 | "'sha256-YfWBLaAD17kgcjrajLlty6AH2yMikIiscRhC6OENK74='"
36 | end
37 |
38 | policy.base_uri :self
39 |
40 | # Specify URI for violation reports
41 | # policy.report_uri "/csp-violation-report-endpoint"
42 | end
43 |
44 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles.
45 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
46 | # config.content_security_policy_nonce_directives = %w(script-src style-src)
47 | #
48 | # # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag`
49 | # # if the corresponding directives are specified in `content_security_policy_nonce_directives`.
50 | # # config.content_security_policy_nonce_auto = true
51 | #
52 | # # Report violations without enforcing the policy.
53 | # # config.content_security_policy_report_only = true
54 | end
55 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # The test environment is used exclusively to run your application's
2 | # test suite. You never need to work with it otherwise. Remember that
3 | # your test database is "scratch space" for the test suite and is wiped
4 | # and recreated between test runs. Don't rely on the data there!
5 |
6 | Rails.application.configure do
7 | # Settings specified here will take precedence over those in config/application.rb.
8 |
9 | # While tests run files are not watched, reloading is not necessary.
10 | config.enable_reloading = false
11 |
12 | # Eager loading loads your entire application. When running a single test locally,
13 | # this is usually not necessary, and can slow down your test suite. However, it's
14 | # recommended that you enable it in continuous integration systems to ensure eager
15 | # loading is working properly before deploying your code.
16 | config.eager_load = ENV['CI'].present?
17 |
18 | # Configure public file server for tests with cache-control for performance.
19 | config.public_file_server.headers = {
20 | 'cache-control' => 'public, max-age=3600',
21 | }
22 |
23 | # Show full error reports.
24 | config.consider_all_requests_local = true
25 | config.cache_store = :null_store
26 |
27 | # Render exception templates for rescuable exceptions and raise for other exceptions.
28 | config.action_dispatch.show_exceptions = :rescuable
29 |
30 | # Disable request forgery protection in test environment.
31 | config.action_controller.allow_forgery_protection = false
32 |
33 | # Store uploaded files on the local file system in a temporary directory.
34 | config.active_storage.service = :test
35 |
36 | # Tell Action Mailer not to deliver emails to the real world.
37 | # The :test delivery method accumulates sent emails in the
38 | # ActionMailer::Base.deliveries array.
39 | config.action_mailer.delivery_method = :test
40 |
41 | # Set host to be used by links generated in mailer templates.
42 | config.action_mailer.default_url_options = { host: 'example.com' }
43 |
44 | # Print deprecation notices to the stderr.
45 | config.active_support.deprecation = :stderr
46 |
47 | # Raises error for missing translations.
48 | # config.i18n.raise_on_missing_translations = true
49 |
50 | # Annotate rendered view with file names.
51 | # config.action_view.annotate_rendered_view_with_filenames = true
52 |
53 | # Raise error when a before_action's only/except options reference missing actions.
54 | config.action_controller.raise_on_missing_callback_actions = true
55 | end
56 |
--------------------------------------------------------------------------------
/app/javascript/Shared/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/app/javascript/Pages/Organizations/Form.vue:
--------------------------------------------------------------------------------
1 |
2 |
63 |
64 |
65 |
93 |
--------------------------------------------------------------------------------
/app/javascript/Pages/Auth/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
73 |
74 |
75 |
146 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
2 |
3 | en:
4 | devise:
5 | confirmations:
6 | confirmed: 'Your email address has been successfully confirmed.'
7 | send_instructions: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
8 | send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes.'
9 | failure:
10 | already_authenticated: 'You are already signed in.'
11 | inactive: 'Your account is not activated yet.'
12 | invalid: 'These credentials do not match our records.'
13 | locked: 'Your account is locked.'
14 | deleted: 'Your account does not exist anymore!'
15 | last_attempt: 'You have one more attempt before your account is locked.'
16 | not_found_in_database: 'Invalid %{authentication_keys} or password.'
17 | timeout: 'Your session expired. Please sign in again to continue.'
18 | unauthenticated: 'You need to sign in before continuing.'
19 | unconfirmed: 'You have to confirm your email address before continuing.'
20 | mailer:
21 | confirmation_instructions:
22 | subject: 'Confirmation instructions'
23 | reset_password_instructions:
24 | subject: 'Reset password instructions'
25 | unlock_instructions:
26 | subject: 'Unlock instructions'
27 | email_changed:
28 | subject: 'Email Changed'
29 | password_change:
30 | subject: 'Password Changed'
31 | omniauth_callbacks:
32 | failure: 'Could not authenticate you from %{kind} because "%{reason}".'
33 | success: 'Successfully authenticated from %{kind} account.'
34 | passwords:
35 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
36 | send_instructions: 'You will receive an email with instructions on how to reset your password in a few minutes.'
37 | send_paranoid_instructions: 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.'
38 | updated: 'Your password has been changed successfully. You are now signed in.'
39 | updated_not_active: 'Your password has been changed successfully.'
40 | registrations:
41 | destroyed: 'Bye! Your account has been successfully cancelled. We hope to see you again soon.'
42 | signed_up: 'Welcome! You have signed up successfully.'
43 | signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
44 | signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
45 | signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please follow the link to activate your account.'
46 | update_needs_confirmation: 'You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address.'
47 | updated: 'Your account has been updated successfully.'
48 | updated_but_not_signed_in: 'Your account has been updated successfully, but since your password was changed, you need to sign in again'
49 | sessions:
50 | signed_in: ''
51 | signed_out: ''
52 | already_signed_out: ''
53 | unlocks:
54 | send_instructions: 'You will receive an email with instructions for how to unlock your account in a few minutes.'
55 | send_paranoid_instructions: 'If your account exists, you will receive an email with instructions for how to unlock it in a few minutes.'
56 | unlocked: 'Your account has been unlocked successfully. Please sign in to continue.'
57 | errors:
58 | messages:
59 | already_confirmed: 'was already confirmed, please try signing in'
60 | confirmation_period_expired: 'needs to be confirmed within %{period}, please request a new one'
61 | expired: 'has expired, please request a new one'
62 | not_found: 'not found'
63 | not_locked: 'was not locked'
64 | not_saved:
65 | one: '1 error prohibited this %{resource} from being saved:'
66 | other: '%{count} errors prohibited this %{resource} from being saved:'
67 |
--------------------------------------------------------------------------------
/app/javascript/Shared/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
27 |
36 |
45 |
54 |
63 |
75 |
84 |
93 |
102 |
111 |
120 |
121 |
122 |
132 |
--------------------------------------------------------------------------------
/app/javascript/Layouts/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |