├── .browserslistrc ├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .erb-lint.yml ├── .eslint-rules ├── custom.js ├── globals.js ├── helpers │ └── index.js ├── imports │ ├── enforced.js │ └── order.js ├── overrides.js ├── promise.js └── react.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore ├── _ │ └── husky.sh ├── helpers │ ├── lint_staged.sh │ ├── prevent_conflict_markers.sh │ ├── prevent_pushing_to_main.sh │ └── run_install_commands.sh ├── post-merge ├── pre-commit └── pre-push ├── .neetoci └── default.yml ├── .node-version ├── .nvmrc ├── .prettierrc.js ├── .rubocop.yml ├── .ruby-version ├── .scripts ├── run_rubocop_and_erblint_on_modified_files.sh ├── setup_vscode.sh └── sync_with_wheel.sh ├── .semaphore ├── commands │ └── run_eslint_on_modified_files.sh └── semaphore.yml ├── .slugignore ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile.dev ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── admin │ ├── dashboard.rb │ └── user.rb ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ └── EmptyNotesList.svg │ └── javascripts │ │ ├── .keep │ │ └── active_admin.js ├── carriers │ └── .keep ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ └── v1 │ │ │ ├── base_controller.rb │ │ │ ├── cypress_runs_controller.rb │ │ │ ├── notes_controller.rb │ │ │ ├── sessions_controller.rb │ │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ ├── api_rescuable.rb │ │ ├── api_responders.rb │ │ ├── authenticable.rb │ │ ├── expirable.rb │ │ ├── loggable.rb │ │ └── set_honey_badger_context.rb │ ├── home_controller.rb │ ├── pages_controller.rb │ ├── passwords_controller.rb │ └── profiles_controller.rb ├── helpers │ └── application_helper.rb ├── javascript │ ├── packs │ │ └── application.js │ ├── src │ │ ├── App.jsx │ │ ├── apis │ │ │ ├── authentication.js │ │ │ ├── axios.js │ │ │ ├── notes.js │ │ │ └── profiles.js │ │ ├── common │ │ │ └── logger.js │ │ ├── components │ │ │ ├── Authentication │ │ │ │ ├── Login.jsx │ │ │ │ ├── ResetPassword.jsx │ │ │ │ ├── Signup.jsx │ │ │ │ └── constants.js │ │ │ ├── Dashboard │ │ │ │ ├── Notes │ │ │ │ │ ├── DeleteAlert.jsx │ │ │ │ │ ├── Pane │ │ │ │ │ │ ├── Create.jsx │ │ │ │ │ │ ├── Edit.jsx │ │ │ │ │ │ └── Form.jsx │ │ │ │ │ ├── Table.jsx │ │ │ │ │ ├── constants.js │ │ │ │ │ └── index.jsx │ │ │ │ ├── Settings │ │ │ │ │ ├── ConfirmPasswordFormModal.jsx │ │ │ │ │ ├── Email.jsx │ │ │ │ │ ├── Password.jsx │ │ │ │ │ ├── Profile.jsx │ │ │ │ │ ├── constants.js │ │ │ │ │ ├── hooks │ │ │ │ │ │ └── useFormikPasswordConfirmationModal.js │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── navLinks.js │ │ │ │ │ └── utils.js │ │ │ │ └── index.jsx │ │ │ ├── Hero.jsx │ │ │ ├── Main.jsx │ │ │ ├── commons │ │ │ │ ├── EmptyState │ │ │ │ │ ├── index.jsx │ │ │ │ │ └── utils.jsx │ │ │ │ ├── PrivateRoute.jsx │ │ │ │ └── Sidebar │ │ │ │ │ ├── constants.js │ │ │ │ │ └── index.jsx │ │ │ ├── constants.js │ │ │ └── routeConstants.js │ │ ├── contexts │ │ │ ├── auth.jsx │ │ │ └── user.jsx │ │ ├── jsconfig.json │ │ ├── reducers │ │ │ ├── auth.js │ │ │ └── user.js │ │ └── utils │ │ │ ├── index.js │ │ │ └── storage.js │ └── stylesheets │ │ ├── application.scss │ │ ├── components │ │ └── _notes.scss │ │ └── layouts │ │ └── sidebar.scss ├── jobs │ └── application_job.rb ├── mailers │ └── mailer.rb ├── models │ ├── application_record.rb │ ├── note.rb │ └── user.rb ├── views │ ├── home │ │ └── index.html.erb │ ├── layouts │ │ └── application.html.erb │ ├── mailer │ │ └── contact_email.html.slim │ ├── pages │ │ └── features.html.erb │ ├── shared │ │ ├── _alerts.html.erb │ │ ├── _bundle.html.erb │ │ ├── _footer.html.erb │ │ ├── _modal.html.erb │ │ └── header │ │ │ ├── _tabs.html.erb │ │ │ └── _user_is_signed_in.html.erb │ └── users │ │ ├── confirmations │ │ └── new.html.erb │ │ ├── mailer │ │ ├── confirmation_instructions.html.erb │ │ ├── reset_password_instructions.html.erb │ │ └── unlock_instructions.html.erb │ │ ├── shared │ │ └── _links.html.erb │ │ └── unlocks │ │ └── new.html.erb └── workers │ ├── base_worker.rb │ ├── cypress_runs_worker.rb │ └── event_notification_worker.rb ├── bin ├── bundle ├── rails ├── rake ├── setup ├── update └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── build │ ├── config.js │ └── constants.js ├── cable.yml ├── database.yml.ci ├── database.yml.docker ├── database.yml.postgresql ├── environment.rb ├── environments │ ├── development.rb │ ├── heroku.rb │ ├── production.rb │ ├── staging.rb │ └── test.rb ├── honeybadger.yml ├── initializers │ ├── active_admin.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── bullet.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── devise.rb │ ├── email_prefixer.rb │ ├── email_setup.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mail_interceptor.rb │ ├── mime_types.rb │ ├── permissions_policy.rb │ ├── rack_deflater.rb │ ├── session_store.rb │ ├── sidekiq.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ ├── en.bootstrap.yml │ └── en.yml ├── master.key.sample ├── puma.rb ├── routes.rb ├── routes │ ├── active_admin.rb │ ├── api.rb │ └── sidekiq.rb ├── secrets.yml ├── sidekiq.yml └── storage.yml ├── cypress-tests ├── .eslintrc.js ├── currents.config.js ├── cypress.json ├── cypress │ ├── config │ │ └── cypress.development.json │ ├── constants │ │ ├── routes.js │ │ ├── selectors │ │ │ ├── common.js │ │ │ └── login.js │ │ └── texts │ │ │ └── login.js │ ├── fixtures │ │ ├── credentials │ │ │ └── oliver.json │ │ └── fake.js │ ├── integration │ │ └── authentication │ │ │ └── login.spec.js │ ├── jsconfig.json │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ ├── inSensTrimmedText.js │ │ ├── index.js │ │ └── utils │ │ └── common.js ├── package.json └── yarn.lock ├── db ├── migrate │ ├── 20131112180000_enable_uuid_extension.rb │ ├── 20131112184628_add_devise_to_users.rb │ ├── 20131213184726_create_active_admin_comments.rb │ ├── 20201014122313_create_notes.rb │ ├── 20210519154811_create_active_storage_tables.active_storage.rb │ ├── 20210519154812_add_service_name_to_active_storage_blobs.active_storage.rb │ ├── 20220829042749_create_active_storage_variant_records.active_storage.rb │ └── 20220829042750_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb ├── schema.rb └── seeds.rb ├── docker-compose.yml ├── docker-entrypoint.sh ├── docs └── using_docker.md ├── esbuild.config.js ├── lib ├── tasks │ ├── assets.rake │ └── setup.rake └── templates │ └── slim │ └── scaffold │ └── index.html.slim ├── neeto-deploy.json ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── tailwind.config.js ├── test ├── controllers │ ├── active_admin │ │ └── dashboard_controller_test.rb │ ├── api │ │ └── v1 │ │ │ ├── notes_controller_test.rb │ │ │ ├── sessions_controller_test.rb │ │ │ └── users_controller_test.rb │ ├── home_controller_test.rb │ ├── pages_controller_test.rb │ ├── passwords_controller_test.rb │ └── profiles_controller_test.rb ├── factories │ ├── notes.rb │ └── users.rb ├── integration │ └── api_invalid_json_data_test.rb ├── models │ ├── note_test.rb │ └── user_test.rb └── test_helper.rb ├── vite.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | cover 95% 2 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | orbs: 4 | cypress: cypress-io/cypress@1.16.1 5 | 6 | workflows: 7 | version: 2 8 | 9 | jobs: 10 | cypress-ci: 11 | parallelism: 1 12 | docker: 13 | - image: "cypress/included:8.5.0" 14 | working_directory: ~/wheel 15 | steps: 16 | - checkout 17 | - run: 18 | name: Execute cypress 19 | command: | 20 | cd cypress-tests 21 | npm install 22 | npm run cypress:run --START_URL=${START_URL} --RECORD_KEY=${RECORD_KEY} 23 | - store_artifacts: 24 | path: cypress-tests/cypress/videos 25 | - store_artifacts: 26 | path: cypress-tests/cypress/screenshots 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # .dockerignore 2 | # Ignore bundler config. 3 | /.bundle 4 | # Ignore all logfiles and tempfiles. 5 | /log/* 6 | /tmp/* 7 | !/log/.keep 8 | !/tmp/.keep 9 | # Ignore pidfiles, but keep the directory. 10 | /tmp/pids/* 11 | !/tmp/pids/ 12 | !/tmp/pids/.keep 13 | # Ignore uploaded files in development. 14 | /storage/* 15 | !/storage/.keep 16 | /public/assets 17 | .byebug_history 18 | # Ignore master key for decrypting credentials and more. 19 | /public/packs 20 | /public/packs-test 21 | /node_modules 22 | /yarn-error.log 23 | yarn-debug.log* 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # VScode respects this. More info at editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.erb-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | glob: "app/views/**/*.{html}{+*,}.erb" 3 | exclude: 4 | - "**/vendor/**/*" 5 | - "**/node_modules/**/*" 6 | - "app/views/shared/_bundle.html.erb" 7 | EnableDefaultLinters: true 8 | linters: 9 | PartialInstanceVariable: 10 | enabled: true 11 | ErbSafety: 12 | enabled: true 13 | Rubocop: 14 | enabled: true 15 | rubocop_config: 16 | inherit_from: 17 | - .rubocop.yml 18 | Style/FrozenStringLiteralComment: 19 | Enabled: false 20 | Layout/TrailingEmptyLines: 21 | Enabled: false 22 | -------------------------------------------------------------------------------- /.eslint-rules/custom.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.eslint-rules/globals.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Globals can be disabled with the string "off" 3 | // "writable" to allow the variable to be overwritten or "readonly" to disallow overwriting. 4 | globals: { 5 | Atomics: "readonly", 6 | SharedArrayBuffer: "readonly", 7 | // Makes logger function available everywhere. Else eslint will complaint of undef-var. 8 | logger: "readonly", 9 | module: "writable", 10 | // Makes props obtained from Rails backend available everywhere in this project. 11 | globalProps: "readonly", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.eslint-rules/helpers/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const buildPathGroupsBasedOnWebpackAliases = ({ 4 | customJSRoot = "app/javascript/", 5 | customAliasPath = "config/webpack/resolve.js", 6 | }) => { 7 | const rootOfProject = __dirname + `/../../`; 8 | 9 | const isFile = filePath => 10 | fs.existsSync(filePath) && fs.lstatSync(filePath).isFile(); 11 | 12 | const webpackAliasPath = rootOfProject + customAliasPath; 13 | 14 | const hasWebpackAliasConfig = isFile(webpackAliasPath); 15 | 16 | const isRailsProject = isFile(rootOfProject + "Gemfile"); 17 | 18 | const emptyPathGroups = []; 19 | 20 | if (!hasWebpackAliasConfig || !isRailsProject) return emptyPathGroups; 21 | 22 | const { alias } = require(webpackAliasPath); 23 | 24 | const railsJSFilesRoot = rootOfProject + customJSRoot; 25 | 26 | const pathGroups = Object.entries(alias).map(([name, path]) => { 27 | // sometimes alias might be already resolved to full absolute path 28 | const isAleadyAnAbsolutePath = 29 | path.includes("cypress-tests/") || path.includes("app/"); 30 | 31 | const absolutePath = isAleadyAnAbsolutePath 32 | ? path 33 | : `${railsJSFilesRoot}${path}`; 34 | const wildCard = 35 | isFile(absolutePath + ".js") || isFile(absolutePath + ".jsx") 36 | ? "" 37 | : "/**"; 38 | 39 | let group = "internal"; 40 | if (name === "neetoui") { 41 | group = "external"; 42 | } 43 | 44 | return { pattern: `${name}${wildCard}`, group }; 45 | }); 46 | 47 | return pathGroups; 48 | }; 49 | 50 | module.exports = { buildPathGroupsBasedOnWebpackAliases }; 51 | -------------------------------------------------------------------------------- /.eslint-rules/imports/enforced.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // not-auto-fixable: Prefer a default export if module exports a single name. 4 | "import/prefer-default-export": "off", 5 | // not-auto-fixable: Forbid a module from importing a module with a dependency path back to itself. 6 | "import/no-cycle": ["error", { maxDepth: 1, ignoreExternal: true }], 7 | // not-auto-fixable: Prevent unnecessary path segments in import and require statements. 8 | "import/no-useless-path-segments": ["error", { noUselessIndex: true }], 9 | // not-auto-fixable: Report any invalid exports, i.e. re-export of the same name. 10 | "import/export": "error", 11 | // not-auto-fixable: Forbid the use of mutable exports with var or let. 12 | "import/no-mutable-exports": "error", 13 | // not-auto-fixable: Ensure all imports appear before other statements. 14 | "import/first": "error", 15 | // not-auto-fixable: Ensure all exports appear after other statements. 16 | "import/exports-last": "error", 17 | // auto-fixable: Enforce a newline after import statements. 18 | "import/newline-after-import": ["error", { count: 1 }], 19 | // auto-fixable: Remove file extensions for import statements. 20 | "import/extensions": [ 21 | "error", 22 | "never", 23 | { 24 | ignorePackages: true, 25 | pattern: { json: "always", mp3: "always" }, 26 | }, 27 | ], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.eslint-rules/imports/order.js: -------------------------------------------------------------------------------- 1 | const { buildPathGroupsBasedOnWebpackAliases } = require(__dirname + 2 | "/../helpers"); 3 | const pathGroups = buildPathGroupsBasedOnWebpackAliases({}); 4 | 5 | const pathGroupForKeepingReactImportsAtTop = { 6 | pattern: "react+(-native|)", 7 | group: "external", 8 | position: "before" 9 | }; 10 | 11 | /* 12 | Example pathGroups structure. Adding this here 13 | so that if anyone wants to add custom config, 14 | they can make use of this: 15 | [ 16 | { pattern: 'apis/**', group: 'internal' }, 17 | { pattern: 'common/**', group: 'internal' }, 18 | { pattern: 'components/**', group: 'internal' }, 19 | { pattern: 'constants/**', group: 'internal' }, 20 | { pattern: 'contexts/**', group: 'internal' }, 21 | { pattern: 'reducers/**', group: 'internal' }, 22 | { pattern: 'Constants', group: 'internal' }, 23 | { pattern: 'neetoui/**', group: 'external' }, 24 | { 25 | pattern: 'react+(-native|)', 26 | group: 'external', 27 | position: 'before' 28 | } 29 | ] 30 | */ 31 | pathGroups.push(pathGroupForKeepingReactImportsAtTop); 32 | 33 | module.exports = { 34 | rules: { 35 | // auto-fixable: Enforce a convention in module import order - we enforce https://www.bigbinary.com/react-best-practices/sort-import-statements 36 | "import/order": [ 37 | "error", 38 | { 39 | "newlines-between": "always", 40 | alphabetize: { order: "asc", caseInsensitive: true }, 41 | warnOnUnassignedImports: true, 42 | groups: [ 43 | "builtin", 44 | "external", 45 | "internal", 46 | "index", 47 | "sibling", 48 | "parent", 49 | "object", 50 | "type" 51 | ], 52 | /* 53 | * Currently we check for existence of webpack alias 54 | * config and then iterate over the aliases and create 55 | * these pathGroups. Only caveat with this mechanism 56 | * is that in VSCode eslint plugin won't dynamically 57 | * read it. But eslint cli would! 58 | */ 59 | pathGroups, 60 | // Ignore react imports so that they're always ordered to the top of the file. 61 | pathGroupsExcludedImportTypes: ["react", "react-native"] 62 | } 63 | ] 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /.eslint-rules/overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Currently we are using this section for excluding certain files from certain rules. 3 | overrides: [ 4 | { 5 | files: [ 6 | ".eslintrc.js", 7 | ".prettierrc.js", 8 | "app/assets/**/*", 9 | "app/javascript/packs/**/*", 10 | "*.json" 11 | ], 12 | rules: { 13 | "import/order": "off", 14 | "react-hooks/rules-of-hooks": "off" 15 | } 16 | }, 17 | { 18 | files: ["app/javascript/packs/**/*.{js,jsx}"], 19 | rules: { 20 | "no-redeclare": "off" 21 | } 22 | } 23 | ] 24 | }; 25 | -------------------------------------------------------------------------------- /.eslint-rules/promise.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // not-auto-fixable: ensure people use async/await promising chaining rather than using "then-catch-finally" statements 4 | "promise/prefer-await-to-then": "error", 5 | // auto-fixable: avoid calling "new" on a Promise static method like reject, resolve etc 6 | "promise/no-new-statics": "error" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | coverage 3 | db 4 | docs 5 | log 6 | .scripts 7 | .semaphore 8 | test 9 | tmp 10 | .vscode 11 | app/javascript/packs 12 | jsconfig.json 13 | esbuild.config.js 14 | vite.config.js 15 | config/build/config.js 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore bundler config 2 | /.bundle 3 | 4 | # Ignore all SQLite databases 5 | /db/*.sqlite3* 6 | 7 | # Ignore database.yml file 8 | /config/database.yml 9 | 10 | # Ignore all logfiles 11 | /log/* 12 | /tmp 13 | /db/backups 14 | /public/uploads 15 | *.DS_Store 16 | coverage 17 | .*swp 18 | /uploads/* 19 | .byebug_history 20 | 21 | # Ignore node packages 22 | node_modules 23 | 24 | # Rails specific ignores 25 | /storage 26 | /vendor 27 | test/reports 28 | test/fixtures 29 | /public/packs 30 | /public/assets 31 | /public/packs-test 32 | 33 | # Ignore master key for decrypting credentials and more 34 | /config/master.key 35 | 36 | # Ignore environment variables 37 | /.env 38 | /.env.development.local 39 | 40 | #---------------------------------------# 41 | # IDEs & Editors Ignores # 42 | #---------------------------------------# 43 | # Sublime Text 44 | *.sublime* 45 | .sublime-gulp.cache 46 | # JetBrains IDEs 47 | .idea/ 48 | # VIM 49 | ## Session 50 | Session.vim 51 | Sessionx.vim 52 | ## Temporary 53 | .netrwhist 54 | ## Backup and swap files 55 | *~ 56 | ## Auto-generated tag files 57 | /tags 58 | # Vscode 59 | .vscode-test 60 | 61 | #---------------------------------------# 62 | # Linux Ignores # 63 | #---------------------------------------# 64 | # directory preferences 65 | .directory 66 | 67 | #---------------------------------------# 68 | # OSX Ignores # 69 | #---------------------------------------# 70 | .DS_Store 71 | .AppleDouble 72 | .LSOverride 73 | .localized 74 | 75 | #---------------------------------------# 76 | # Windows Ignores # 77 | #---------------------------------------# 78 | # Windows image file caches 79 | Thumbs.db 80 | ehthumbs.db 81 | # Folder config file 82 | Desktop.ini 83 | # Recycle Bin used on file shares 84 | $RECYCLE.BIN/ 85 | # Files that might appear on external disk 86 | .Spotlight-V100 87 | .Trashes 88 | 89 | #---------------------------------------# 90 | # Tools and Package specific ignores # 91 | #---------------------------------------# 92 | .eslintcache 93 | yarn-error.log 94 | yarn-debug.log* 95 | .yarn-integrity 96 | .yarnrc 97 | yarn-1.22.5.cjs 98 | yarn-1.22.5.js 99 | 100 | # Ignore node_modules in cypress folder 101 | cypress-tests/node_modules 102 | 103 | # Ignore package-lock.json in cypress folder 104 | cypress-tests/package-lock.json 105 | 106 | # Ignore all logfiles and tempfiles in cypress folder 107 | cypress-tests/cypress/results 108 | cypress-tests/debug.log 109 | 110 | # Ignore IDE related files in cypress folder 111 | cypress-tests/.vscode 112 | 113 | # Ignore cypress screenshots and videos 114 | cypress-tests/cypress/artifacts/screenshots/ 115 | cypress-tests/cypress/artifacts/videos/ 116 | cypress-tests/cypress/screenshots/ 117 | cypress-tests/cypress/videos/ 118 | 119 | # Ignore redis dump file 120 | dump.rdb 121 | 122 | # Ignore build files 123 | /app/assets/builds 124 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/_/husky.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "$husky_skip_init" ]; then 3 | debug () { 4 | if [ "$HUSKY_DEBUG" = "1" ]; then 5 | echo "husky (debug) - $1" 6 | fi 7 | } 8 | 9 | readonly hook_name="$(basename "$0")" 10 | debug "starting $hook_name..." 11 | 12 | if [ "$HUSKY" = "0" ]; then 13 | debug "HUSKY env variable is set to 0, skipping hook" 14 | exit 0 15 | fi 16 | 17 | if [ -f ~/.huskyrc ]; then 18 | debug "sourcing ~/.huskyrc" 19 | . ~/.huskyrc 20 | fi 21 | 22 | export readonly husky_skip_init=1 23 | sh -e "$0" "$@" 24 | exitCode="$?" 25 | 26 | if [ $exitCode != 0 ]; then 27 | echo "husky - $hook_name hook exited with code $exitCode (error)" 28 | fi 29 | 30 | exit $exitCode 31 | fi 32 | -------------------------------------------------------------------------------- /.husky/helpers/lint_staged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | lint_staged_files() { 4 | npx lint-staged 5 | } 6 | -------------------------------------------------------------------------------- /.husky/helpers/prevent_conflict_markers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | repeat() { 4 | char="$1" 5 | times="$2" 6 | text="" 7 | i=0 8 | 9 | while [ $i -lt $times ] 10 | do 11 | text="${text}${char}" 12 | i=$((i+1)) 13 | done 14 | echo "$text" 15 | } 16 | 17 | prevent_conflict_markers() { 18 | RED=$(tput setab 1) 19 | NORMAL=$(tput sgr0) 20 | 21 | left_arrow=$(repeat '<' 7) 22 | right_arrow=$(repeat '>' 7) 23 | equal_symbol=$(repeat '=' 7) 24 | 25 | CONFLICT_MARKERS="^${left_arrow}$|^${equal_symbol}$|^${right_arrow}$" 26 | occurrences_count=$(git --no-pager diff --staged --ignore-blank-lines --name-only -G"$CONFLICT_MARKERS" | wc -l) 27 | if [ "$occurrences_count" -gt 0 ] 28 | then 29 | echo "$RED ERROR $NORMAL Found ${CONFLICT_MARKERS} symbols in staged files." 30 | echo "Conflict markers should be removed from the following files before committing:" 31 | git --no-pager diff --staged --ignore-blank-lines --name-only -G"$CONFLICT_MARKERS" 32 | exit 1; 33 | fi 34 | } 35 | -------------------------------------------------------------------------------- /.husky/helpers/prevent_pushing_to_main.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | prevent_pushing_to_main() { 4 | current_branch=`git symbolic-ref HEAD` 5 | current_origin=`git remote` 6 | if [ current_origin = "origin" -o "$current_branch" = "refs/heads/master" -o "$current_branch" = "refs/heads/main" ] 7 | then 8 | cat < /dev/null 2 | git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' 3 | git fetch origin main 4 | 5 | CHANGED_FILES=$(git diff-tree --diff-filter=a -r --no-commit-id --name-only HEAD remotes/origin/main) 6 | 7 | if [ -n "$CHANGED_FILES" ]; then 8 | echo "$CHANGED_FILES" | xargs bundle exec rubocop --force-exclusion 9 | else 10 | echo "No file changes detected. Skipping RuboCop." 11 | fi 12 | 13 | -------------------------------------------------------------------------------- /.scripts/setup_vscode.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "json" 4 | 5 | def which(cmd) 6 | exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] 7 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 8 | exts.each do |ext| 9 | exe = File.join(path, "#{cmd}#{ext}") 10 | return exe if File.executable?(exe) && !File.directory?(exe) 11 | end 12 | end 13 | nil 14 | end 15 | 16 | code_file_location = which("code") 17 | code_insiders_file_location = which("code-insiders") 18 | 19 | if code_file_location.nil? && code_insiders_file_location.nil? 20 | puts "Couldn't find an executable for VSCode. Please ensure that the code or code-insiders command is available in your PATH env" 21 | return 22 | end 23 | 24 | config = `curl -s 'https://raw.githubusercontent.com/bigbinary/wheel/main/.vscode/extensions.json'` 25 | extensions = JSON.parse(config)["recommendations"] 26 | 27 | extension_installation_command = code_file_location.nil? ? "code-insiders --install-extension" : "code --install-extension" 28 | 29 | extensions.each do |extension| 30 | puts `#{extension_installation_command} #{extension}` 31 | end 32 | -------------------------------------------------------------------------------- /.scripts/sync_with_wheel.sh: -------------------------------------------------------------------------------- 1 | # For executing this script, run the following from your terminal: 2 | # curl -s -L "https://raw.githubusercontent.com/bigbinary/wheel/master/.scripts/sync_with_wheel.sh" | bash 3 | yarn remove babel-eslint 2> /dev/null 4 | yarn add -D @babel/eslint-parser 5 | yarn add -D prettier 6 | yarn add -D eslint \ 7 | eslint-plugin-react-hooks@4.2.1-alpha-13455d26d-20211104 \ 8 | eslint-plugin-import \ 9 | eslint-config-prettier \ 10 | eslint-plugin-prettier \ 11 | eslint-plugin-json \ 12 | eslint-plugin-react \ 13 | eslint-plugin-promise \ 14 | eslint-plugin-jam3 \ 15 | eslint-plugin-cypress \ 16 | eslint-plugin-unused-imports \ 17 | prettier-plugin-tailwindcss 18 | 19 | raw_base_url="https://raw.githubusercontent.com/bigbinary/wheel/main" 20 | declare -a configs=( 21 | ".eslintrc.js" 22 | ".eslintignore" 23 | ".eslint-rules/helpers/index.js" 24 | ".eslint-rules/imports/enforced.js" 25 | ".eslint-rules/imports/order.js" 26 | ".eslint-rules/globals.js" 27 | ".eslint-rules/overrides.js" 28 | ".eslint-rules/promise.js" 29 | ".eslint-rules/react.js" 30 | ".rubocop.yml" 31 | ".prettierrc.js" 32 | ".editorconfig" 33 | ".vscode/extensions.json" 34 | ".vscode/settings.json" 35 | ".husky/helpers/lint_staged.sh" 36 | ".husky/helpers/prevent_pushing_to_main.sh" 37 | ".husky/helpers/prevent_conflict_markers.sh" 38 | ".husky/pre-commit" 39 | ".husky/pre-push" 40 | "cypress-tests/.eslintrc.js" 41 | ".semaphore/commands/run_eslint_on_modified_files.sh" 42 | ".node-version" 43 | ".nvmrc" 44 | ".ruby-version" 45 | ".erb-lint.yml" 46 | "bin/bundle" 47 | "bin/rails" 48 | "bin/rake" 49 | "bin/setup" 50 | "bin/update" 51 | "bin/webpacker" 52 | "bin/webpacker-dev-server" 53 | "bin/yarn" 54 | ) 55 | 56 | for config in "${configs[@]}"; do 57 | echo "Downloading ${config}..." 58 | curl --create-dirs -o "${config}" "${raw_base_url}/${config}" 59 | done 60 | -------------------------------------------------------------------------------- /.semaphore/commands/run_eslint_on_modified_files.sh: -------------------------------------------------------------------------------- 1 | git fetch --unshallow 2> /dev/null 2 | git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' 3 | git fetch origin main 4 | 5 | CHANGED_FILES=$(git diff-tree --diff-filter=a -r --no-commit-id --name-only HEAD remotes/origin/main | grep --color=none -i -e "\.js$" -e "\.jsx$"| awk '!/eslint.config.mjs/ {print}') 6 | 7 | if [ -n "$CHANGED_FILES" ]; then 8 | echo "$CHANGED_FILES" | xargs npx eslint --cache --no-error-on-unmatched-pattern 9 | else 10 | echo "No file changes detected. Skipping ESlint." 11 | fi 12 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: cicheck 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu2004 7 | auto_cancel: 8 | running: 9 | when: "branch != 'main'" 10 | fail_fast: 11 | stop: 12 | when: "true" 13 | global_job_config: 14 | prologue: 15 | commands: 16 | - checkout 17 | # TODO: keep the version aligned with neeto side 18 | - sem-version ruby 3.3.5 19 | - sem-version node 22.13 20 | - sem-service start postgres 13 21 | - sem-service start redis 7.0.5 22 | - cp config/database.yml.ci config/database.yml 23 | - bundle config path 'vendor/bundle' 24 | - cache restore 25 | - bundle install --jobs 1 26 | - yarn install 27 | - cache store 28 | env_vars: 29 | - name: TZ 30 | value: UTC 31 | - name: RAILS_ENV 32 | value: test 33 | - name: RACK_ENV 34 | value: test 35 | blocks: 36 | - name: Auditors | Linters | Tasks | Tests 37 | task: 38 | jobs: 39 | - name: Checks 40 | commands: 41 | - bundle exec ruby-audit check 42 | - bundle exec rubocop 43 | - bundle exec erblint --lint-all --format compact 44 | - 'curl -s -L 45 | "https://raw.githubusercontent.com/bigbinary/wheel/main/.semaphore/commands/run_eslint_on_modified_files.sh" 46 | | bash' 47 | - bundle exec rake db:create db:schema:load --trace 48 | - bundle exec rails test 49 | - bundle exec rake setup 50 | -------------------------------------------------------------------------------- /.slugignore: -------------------------------------------------------------------------------- 1 | /test 2 | /spec 3 | /docs 4 | /cypress-tests 5 | /selenium-tests 6 | /doc 7 | /tmp/cache 8 | /.cache/yarn 9 | /.semaphore 10 | /.rubocop.yml 11 | /README.md 12 | /Procfile.dev 13 | /.editorconfig 14 | /.dockerignore 15 | /Dockerfile.dev 16 | /docker-entrypoint.sh 17 | /docker-compose.yml -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "kaiwood.endwise", 5 | "Shopify.ruby-lsp", 6 | "redhat.vscode-yaml", 7 | "editorconfig.editorconfig", 8 | "dbaeumer.vscode-eslint", 9 | "streetsidesoftware.code-spell-checker", 10 | "bung87.rails", 11 | "eamodio.gitlens" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[ruby]": { 3 | "editor.autoClosingBrackets": "beforeWhitespace", 4 | "editor.semanticHighlighting.enabled": true, 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "Shopify.ruby-lsp" 7 | }, 8 | "files.trimTrailingWhitespace": true, 9 | "files.trimFinalNewlines": true, 10 | "editor.formatOnSave": true, 11 | "yaml.format.enable": true, 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM neeto/ruby-3.3.5:node22.13 2 | 3 | ENV APP_PATH /var/app 4 | ENV BUNDLE_VERSION 2.2.32 5 | ENV RAILS_PORT 3000 6 | ENV LAUNCHY_DRY_RUN true 7 | ENV BROWSER /dev/null 8 | ENV BUNDLE_PATH /usr/local/bundle 9 | ENV GEM_PATH /usr/local/bundle 10 | ENV GEM_HOME /usr/local/bundle 11 | 12 | COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh 13 | 14 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh 15 | 16 | # install dependencies for M1 Macs 17 | RUN apk add --update --no-cache curl py-pip python3 18 | 19 | # install dependencies for application 20 | RUN apk -U add --no-cache \ 21 | make \ 22 | gcc \ 23 | build-base \ 24 | git \ 25 | postgresql-dev \ 26 | postgresql-client \ 27 | libxml2-dev \ 28 | libxslt-dev \ 29 | nodejs-current \ 30 | npm \ 31 | yarn \ 32 | tzdata \ 33 | && rm -rf /var/cache/apk/* \ 34 | && mkdir -p $APP_PATH 35 | 36 | 37 | RUN gem install bundler --version "$BUNDLE_VERSION" 38 | 39 | # navigate to app directory 40 | WORKDIR $APP_PATH 41 | 42 | COPY Gemfile Gemfile.lock ./ 43 | COPY package.json yarn.lock ./ 44 | 45 | RUN bundle check || bundle install --jobs=8 46 | RUN yarn install --check-files 47 | 48 | COPY . . 49 | 50 | EXPOSE $RAILS_PORT 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | ruby "3.3.5" 6 | 7 | gem "rails", "~> 7.1.3.4" 8 | gem "sprockets" 9 | 10 | # friends of Rails 11 | gem "sass-rails", ">= 6" 12 | gem "sprockets-rails" 13 | gem "uglifier", ">= 2.7.1" 14 | 15 | # React 16 | gem "react-rails" 17 | 18 | # database 19 | gem "pg" 20 | 21 | # Application server 22 | gem "puma" 23 | 24 | # JSON builder 25 | gem "jbuilder", ">= 2.7" 26 | 27 | # Authentication 28 | gem "devise", "~> 4.7" 29 | 30 | # Error tracking 31 | gem "honeybadger" 32 | 33 | # Support cross-browser css compatibility 34 | gem "autoprefixer-rails" 35 | 36 | # Admin framework 37 | gem "activeadmin" 38 | 39 | # Email validation 40 | gem "email_validator" 41 | 42 | # Adds prefix to subject in emails 43 | gem "email_prefixer" 44 | 45 | # Reduces boot times through caching; required in config/boot.rb 46 | gem "bootsnap", ">= 1.9.4", require: false 47 | 48 | # Background jobs 49 | gem "sidekiq", "<8" 50 | 51 | group :development, :test do 52 | # Rails integration for factory-bot 53 | gem "factory_bot_rails" 54 | 55 | # Adds step-by-step debugging and stack navigation capabilities to pry using byebug. 56 | # supports both syntax - pry and byebug 57 | gem "pry-byebug" 58 | 59 | # For auto-generating demo data 60 | gem "faker", "~> 2.19" 61 | end 62 | 63 | group :development, :staging, :heroku do 64 | # Intercepts outgoing emails in non-production environment 65 | gem "mail_interceptor" 66 | end 67 | 68 | group :development do 69 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code 70 | gem "web-console" 71 | 72 | # reports N+1 queries 73 | gem "bullet" 74 | 75 | # A Ruby static code analyzer, based on the community Ruby style guide 76 | gem "rubocop", require: false 77 | gem "rubocop-rails", require: false 78 | # For linting ERB files 79 | gem "erb_lint", require: false, git: "https://github.com/Shopify/erb-lint.git", branch: "main" 80 | 81 | # Patch-level verification for Bundler 82 | gem "bundler-audit", require: false 83 | 84 | # vulnerability checker for Ruby itself 85 | gem "ruby_audit", require: false 86 | 87 | # Preview email in browser 88 | gem "letter_opener" 89 | end 90 | 91 | group :test do 92 | # Complete suite of testing facilities 93 | gem "minitest" 94 | 95 | # for test coverage report 96 | gem "simplecov", require: false 97 | 98 | # Minitest reporter plugin for CircleCI. 99 | gem "minitest-ci" 100 | 101 | # Check semaphore config - this gem only relevant in semaphoreCI 102 | gem "ffi", github: "ffi/ffi", submodules: true 103 | end 104 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BigBinary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb 2 | worker: bundle exec sidekiq -C config/sidekiq.yml 3 | release: bundle exec rake db:migrate && bundle exec rake reset_and_populate_sample_data -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails s -p 3000 2 | vite: yarn dev --host 3 | worker: bundle exec sidekiq -C config/sidekiq.yml 4 | log: tail -f log/development.log 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative "config/application" 7 | 8 | Rails.application.load_tasks 9 | 10 | Knapsack.load_tasks if defined?(Knapsack) 11 | -------------------------------------------------------------------------------- /app/admin/dashboard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveAdmin.register_page "Dashboard" do 4 | 5 | menu priority: 1, label: proc { I18n.t("active_admin.dashboard") } 6 | 7 | content title: proc { I18n.t("active_admin.dashboard") } do 8 | div class: "blank_slate_container", id: "dashboard_default_message" do 9 | span class: "blank_slate" do 10 | span I18n.t("active_admin.dashboard_welcome.welcome") 11 | small I18n.t("active_admin.dashboard_welcome.call_to_action") 12 | end 13 | end 14 | 15 | # Here is an example of a simple dashboard with columns and panels. 16 | # 17 | # columns do 18 | # column do 19 | # panel "Recent Posts" do 20 | # ul do 21 | # Post.recent(5).map do |post| 22 | # li link_to(post.title, admin_post_path(post)) 23 | # end 24 | # end 25 | # end 26 | # end 27 | 28 | # column do 29 | # panel "Info" do 30 | # para "Welcome to ActiveAdmin." 31 | # end 32 | # end 33 | # end 34 | end # content 35 | end 36 | -------------------------------------------------------------------------------- /app/admin/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveAdmin.register User do 4 | 5 | index do 6 | column :email 7 | column :current_sign_in_at 8 | column :last_sign_in_at 9 | column :sign_in_count 10 | end 11 | 12 | filter :email 13 | 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../builds 2 | //= link_tree ../builds .css 3 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/wheel/e53ea8d6452a09b95f04f13bb18412f221a17f52/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/wheel/e53ea8d6452a09b95f04f13bb18412f221a17f52/app/assets/javascripts/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/active_admin.js: -------------------------------------------------------------------------------- 1 | //= require active_admin/base 2 | -------------------------------------------------------------------------------- /app/carriers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bigbinary/wheel/e53ea8d6452a09b95f04f13bb18412f221a17f52/app/carriers/.keep -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/api/v1/base_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::V1::BaseController < ApplicationController 4 | include ApiResponders 5 | include Loggable 6 | include ApiRescuable 7 | include Authenticable 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/api/v1/cypress_runs_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::V1::CypressRunsController < Api::V1::BaseController 4 | skip_before_action :authenticate_user_using_x_auth_token 5 | skip_before_action :authenticate_user! 6 | skip_before_action :verify_authenticity_token 7 | 8 | # before_action :verify_authorization_token 9 | 10 | def create 11 | CypressRunsWorker.perform_async(params[:app_url]) 12 | 13 | head :ok 14 | end 15 | 16 | private 17 | 18 | def verify_authorization_token 19 | cypress_auth_token = cypress_secrets[:auth_token] 20 | 21 | authenticate_or_request_with_http_token do |token, _options| 22 | ActiveSupport::SecurityUtils.secure_compare(token, cypress_auth_token) 23 | end 24 | end 25 | 26 | def cypress_secrets 27 | @_cypress_secrets ||= Rails.application.secrets.cypress 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/api/v1/notes_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::V1::NotesController < Api::V1::BaseController 4 | before_action :load_note!, only: %i[update delete] 5 | before_action :load_notes, only: :bulk_destroy 6 | 7 | def index 8 | render_json({ notes: current_user.notes }) 9 | end 10 | 11 | def create 12 | current_user.notes.create!(note_params) 13 | render_message(t("successfully_created", entity: "Note")) 14 | end 15 | 16 | def update 17 | @note.update!(note_params) 18 | render_message(t("successfully_updated", entity: "Note")) 19 | end 20 | 21 | def bulk_destroy 22 | records_size = @notes.size 23 | if @notes.destroy_all 24 | render_message(t("successfully_deleted", count: records_size, entity: records_size > 1 ? "Notes" : "Note")) 25 | else 26 | render_error(t("Something went wrong!")) 27 | end 28 | end 29 | 30 | private 31 | 32 | def note_params 33 | params.require(:note).permit(:title, :description) 34 | end 35 | 36 | def load_note! 37 | @note = current_user.notes.find(params[:id]) 38 | end 39 | 40 | def load_notes 41 | @notes = current_user.notes.where(id: params[:ids]) 42 | render_error(t("not_found", entity: "Note")) if @notes.empty? 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/controllers/api/v1/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::V1::SessionsController < Api::V1::BaseController 4 | skip_before_action :authenticate_user! 5 | skip_before_action :authenticate_user_using_x_auth_token 6 | 7 | def create 8 | user = User.find_for_database_authentication(email: session_params[:email]) 9 | if invalid_password?(user) 10 | render_error(t("invalid_credentials"), :unauthorized) 11 | else 12 | sign_in(user) 13 | render_json({ auth_token: user.authentication_token, user:, is_admin: user.super_admin? }, :created) 14 | end 15 | end 16 | 17 | def destroy 18 | sign_out(@user) 19 | reset_session 20 | end 21 | 22 | private 23 | 24 | def session_params 25 | params.require(:user).permit(:email, :password) 26 | end 27 | 28 | def invalid_password?(user) 29 | user.blank? || !user.valid_password?(session_params[:password]) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Api::V1::UsersController < Api::V1::BaseController 4 | skip_before_action :authenticate_user!, only: :create 5 | skip_before_action :authenticate_user_using_x_auth_token, only: :create 6 | 7 | before_action :load_user!, only: %i[show destroy] 8 | 9 | def show 10 | render_json(@user) 11 | end 12 | 13 | def create 14 | user = User.create!(user_params) 15 | render_message( 16 | t("signup_successful"), 17 | :ok, 18 | { user:, auth_token: user.authentication_token } 19 | ) 20 | end 21 | 22 | def destroy 23 | @user.destroy! 24 | render_message(t("successfully_deleted", count: 1, entity: "User")) 25 | end 26 | 27 | private 28 | 29 | def load_user! 30 | @user = User.find(params[:id]) 31 | end 32 | 33 | def user_params 34 | params.require(:user).permit(:email, :first_name, :last_name, :password, :password_confirmation) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | include SetHoneyBadgerContext 5 | include Expirable 6 | 7 | private 8 | 9 | def ensure_current_user_is_superadmin! 10 | authenticate_user! 11 | 12 | unless current_user.super_admin? 13 | redirect_to root_path, status: :forbidden, alert: "Unauthorized Access!" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/concerns/api_rescuable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApiRescuable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found 8 | rescue_from ActiveRecord::RecordInvalid, with: :handle_validation_error 9 | rescue_from ActiveRecord::RecordNotUnique, with: :handle_record_not_unique 10 | rescue_from ActionController::ParameterMissing, with: :handle_api_error 11 | end 12 | 13 | private 14 | 15 | def handle_validation_error(exception) 16 | log_exception(exception) 17 | render_error(exception.record.errors_to_sentence) 18 | end 19 | 20 | def handle_record_not_found(exception) 21 | log_exception(exception) 22 | render_error(exception.message, :not_found) 23 | end 24 | 25 | def handle_record_not_unique(exception) 26 | log_exception(exception) 27 | render_error(exception.record.errors_to_sentence) 28 | end 29 | 30 | def handle_api_error(exception) 31 | log_exception(exception) 32 | render_error(exception.original_message, :internal_server_error) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/concerns/api_responders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApiResponders 4 | extend ActiveSupport::Concern 5 | 6 | private 7 | 8 | def render_error(message, status = :unprocessable_entity, context = {}) 9 | is_message_array = message.is_a?(Array) 10 | error_message = is_message_array ? message.to_sentence : message 11 | render status:, json: { error: error_message }.merge(context) 12 | end 13 | 14 | def render_message(message, status = :ok, context = {}) 15 | render status:, json: { notice: message }.merge(context) 16 | end 17 | 18 | def render_json(json = {}, status = :ok) 19 | render status:, json: 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/concerns/authenticable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authenticable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before_action :authenticate_user_using_x_auth_token 8 | before_action :authenticate_user! 9 | end 10 | 11 | private 12 | 13 | def authenticate_user_using_x_auth_token 14 | user_email = request.headers["X-Auth-Email"].presence 15 | auth_token = request.headers["X-Auth-Token"].presence 16 | user = user_email && User.find_by(email: user_email) 17 | 18 | if user && auth_token && Devise.secure_compare(user.authentication_token, auth_token) 19 | sign_in user, store: false 20 | else 21 | render_error(t("invalid_credentials"), :unauthorized) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/concerns/expirable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Expirable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | rescue_from ActionController::InvalidAuthenticityToken, with: :ensure_user_is_logged_out_during_session_expiry 8 | end 9 | 10 | private 11 | 12 | def ensure_user_is_logged_out_during_session_expiry 13 | render_json({ error: t("session.expiry") }, :unauthorized) and return 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/concerns/loggable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Loggable 4 | extend ActiveSupport::Concern 5 | 6 | private 7 | 8 | def log_exception(exception) 9 | Rails.logger.info exception.class.to_s 10 | Rails.logger.info exception.to_s 11 | Rails.logger.info exception.backtrace.join("\n") 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/concerns/set_honey_badger_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SetHoneyBadgerContext 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before_action :set_honeybadger_context 8 | end 9 | 10 | private 11 | 12 | def set_honeybadger_context 13 | hash = { uuid: request.uuid } 14 | hash.merge!(user_id: current_user.id, user_email: current_user.email) if current_user 15 | Honeybadger.context hash 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HomeController < ApplicationController 4 | def index 5 | render 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | def contact 5 | @contact = Contact.new 6 | end 7 | 8 | def features 9 | render 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PasswordsController < Devise::RegistrationsController 4 | include ApiResponders 5 | 6 | before_action :load_resource 7 | 8 | def update 9 | if resource.update_with_password(update_params) 10 | bypass_sign_in resource, scope: :user 11 | render_message(t("successfully_updated", entity: "Password")) 12 | else 13 | clean_up_passwords resource 14 | render_error(resource.errors_to_sentence) 15 | end 16 | end 17 | 18 | private 19 | 20 | def update_params 21 | resource_params.permit(:password, :password_confirmation, :current_password) 22 | end 23 | 24 | def resource_params 25 | params.require(:user) 26 | end 27 | 28 | def load_resource 29 | self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/profiles_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ProfilesController < Devise::RegistrationsController 4 | include ApiResponders 5 | 6 | prepend_before_action :authenticate_scope!, only: :update 7 | before_action :load_resource 8 | 9 | def update 10 | if resource.update_with_password(update_params) 11 | bypass_sign_in resource, scope: :user 12 | render_message(t("successfully_updated", entity: "User profile"), :ok, { user: resource }) 13 | else 14 | render_error(resource.errors_to_sentence) 15 | end 16 | end 17 | 18 | def update_email 19 | if resource.update_with_password(update_params) 20 | sign_out(resource) 21 | render_message(t("successfully_updated", entity: "Email")) 22 | else 23 | render_error(resource.errors_to_sentence) 24 | end 25 | end 26 | 27 | private 28 | 29 | def update_params 30 | resource_params.permit(:email, :current_password, :first_name, :last_name) 31 | end 32 | 33 | def resource_params 34 | params.require(:user) 35 | end 36 | 37 | def load_resource 38 | self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | def get_client_props 5 | { 6 | user: current_user, 7 | is_admin: current_user && current_user.super_admin? 8 | } 9 | end 10 | 11 | def super_admin_signed_in? 12 | user_signed_in? && current_user.super_admin? 13 | end 14 | 15 | def nav_link(text, path, condition = false, options = {}) 16 | class_name = (current_page?(path) || condition) ? "active" : "" 17 | 18 | content_tag(:li, class: class_name) do 19 | options[:title] = text unless options.has_key?(:title) 20 | link_to text, path, options 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | /* global require */ 3 | // This file is automatically compiled by Webpack, along with any other files 4 | // present in this directory. You're encouraged to place your actual application logic in 5 | // a relevant structure within app/javascript and only use these pack files to reference 6 | // that code so it'll be compiled. 7 | // 8 | // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate 9 | // layout file, like app/views/layouts/application.html.erb 10 | 11 | import "../stylesheets/application.scss"; 12 | import React from "react"; 13 | import ReactRailsUJS from "react_ujs"; 14 | 15 | import App from "../src/App"; 16 | 17 | // Support component names relative to this directory: 18 | const componentsContext = { App }; 19 | ReactRailsUJS.getConstructor = name => { 20 | return componentsContext[name]; 21 | }; 22 | -------------------------------------------------------------------------------- /app/javascript/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { AuthProvider } from "contexts/auth"; 4 | import { UserProvider } from "contexts/user"; 5 | 6 | import Main from "./components/Main"; 7 | 8 | const App = props => ( 9 | 10 | 11 |
12 | 13 | 14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /app/javascript/src/apis/authentication.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const login = payload => axios.post("api/v1/login", { user: payload }); 4 | 5 | const logout = () => axios.delete("api/v1/logout"); 6 | 7 | const signup = ({ 8 | email, 9 | firstName: first_name, 10 | lastName: last_name, 11 | password, 12 | passwordConfirmation: password_confirmation, 13 | }) => 14 | axios.post("api/v1/users", { 15 | user: { 16 | email, 17 | first_name, 18 | last_name, 19 | password, 20 | password_confirmation, 21 | }, 22 | }); 23 | 24 | const authenticationApi = { 25 | login, 26 | logout, 27 | signup, 28 | }; 29 | 30 | export default authenticationApi; 31 | -------------------------------------------------------------------------------- /app/javascript/src/apis/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Toastr } from "neetoui"; 3 | 4 | import { getFromLocalStorage } from "utils/storage"; 5 | 6 | axios.defaults.baseURL = "/"; 7 | 8 | const setAuthHeaders = (setLoading = () => null) => { 9 | axios.defaults.headers = { 10 | Accept: "application/json", 11 | "Content-Type": "application/json", 12 | "X-CSRF-TOKEN": document 13 | .querySelector('[name="csrf-token"]') 14 | .getAttribute("content"), 15 | }; 16 | const token = getFromLocalStorage("authToken"); 17 | const email = getFromLocalStorage("authEmail"); 18 | if (token && email) { 19 | axios.defaults.headers["X-Auth-Email"] = email; 20 | axios.defaults.headers["X-Auth-Token"] = token; 21 | } 22 | setLoading(false); 23 | }; 24 | 25 | const resetAuthTokens = () => { 26 | delete axios.defaults.headers["X-Auth-Email"]; 27 | delete axios.defaults.headers["X-Auth-Token"]; 28 | }; 29 | 30 | const handleSuccessResponse = response => { 31 | if (response) { 32 | response.success = response.status === 200; 33 | if (response.data.notice) { 34 | Toastr.success(response.data.notice); 35 | } 36 | } 37 | 38 | return response; 39 | }; 40 | 41 | const handleErrorResponse = (error, authDispatch) => { 42 | if (error.response?.status === 401) { 43 | authDispatch({ type: "LOGOUT" }); 44 | Toastr.error(error.response?.data?.error); 45 | } else { 46 | Toastr.error(error.response?.data?.error || error.message); 47 | } 48 | 49 | return Promise.reject(error); 50 | }; 51 | 52 | const registerIntercepts = authDispatch => { 53 | axios.interceptors.response.use(handleSuccessResponse, error => 54 | handleErrorResponse(error, authDispatch) 55 | ); 56 | }; 57 | 58 | export { setAuthHeaders, resetAuthTokens, registerIntercepts }; 59 | -------------------------------------------------------------------------------- /app/javascript/src/apis/notes.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const fetch = () => axios.get("api/v1/notes"); 4 | const create = payload => axios.post("api/v1/notes", payload); 5 | const update = (id, payload) => axios.put(`api/v1/notes/${id}`, payload); 6 | const destroy = payload => axios.post("api/v1/notes/bulk_destroy", payload); 7 | 8 | const notesApi = { 9 | fetch, 10 | create, 11 | update, 12 | destroy, 13 | }; 14 | 15 | export default notesApi; 16 | -------------------------------------------------------------------------------- /app/javascript/src/apis/profiles.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const updatePassword = ({ 4 | currentPassword: current_password, 5 | password, 6 | passwordConfirmation: password_confirmation, 7 | }) => 8 | axios.patch("my/password", { 9 | user: { current_password, password, password_confirmation }, 10 | }); 11 | 12 | const updateProfile = ({ 13 | email, 14 | firstName: first_name, 15 | lastName: last_name, 16 | password: current_password, 17 | }) => 18 | axios.put("/my/profile", { 19 | user: { email, first_name, last_name, current_password }, 20 | }); 21 | 22 | const updateEmail = ({ email, password: current_password }) => 23 | axios.patch("/my/email", { user: { email, current_password } }); 24 | 25 | const profilesApi = { 26 | updatePassword, 27 | updateProfile, 28 | updateEmail, 29 | }; 30 | 31 | export default profilesApi; 32 | -------------------------------------------------------------------------------- /app/javascript/src/common/logger.js: -------------------------------------------------------------------------------- 1 | import Logger from "js-logger"; 2 | 3 | export const initializeLogger = () => { 4 | /* eslint react-hooks/rules-of-hooks: "off" */ 5 | Logger.useDefaults(); 6 | if (process.env.RAILS_ENV === "production") { 7 | Logger.setLevel(Logger.OFF); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /app/javascript/src/components/Authentication/ResetPassword.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Form, Formik } from "formik"; 4 | import { Button } from "neetoui"; 5 | import { Input } from "neetoui/formik"; 6 | 7 | import { LOGIN_PATH, SIGNUP_PATH } from "components/routeConstants"; 8 | 9 | import { 10 | RESET_PASSWORD_FORM_INITIAL_VALUES, 11 | RESET_PASSWORD_FORM_VALIDATION_SCHEMA, 12 | } from "./constants"; 13 | 14 | const ResetPassword = () => ( 15 |
16 |
17 |

18 | Forgot your password? 19 |

20 |
21 | Enter your email address below and we'll send you a link to reset 22 | your password. 23 |
24 | null} 28 | > 29 | {({ isSubmitting }) => ( 30 |
34 | 35 |
36 |
48 |
49 | )} 50 |
51 |
52 |

{`Don't have an account?`}

53 |
55 |
56 |
57 | ); 58 | 59 | export default ResetPassword; 60 | -------------------------------------------------------------------------------- /app/javascript/src/components/Authentication/constants.js: -------------------------------------------------------------------------------- 1 | import * as yup from "yup"; 2 | 3 | export const LOGIN_FORM_INITIAL_VALUES = { 4 | email: "", 5 | password: "", 6 | }; 7 | 8 | export const RESET_PASSWORD_FORM_INITIAL_VALUES = { 9 | email: "", 10 | }; 11 | 12 | export const SIGNUP_FORM_INITIAL_VALUES = { 13 | email: "", 14 | firstName: "", 15 | lastName: "", 16 | password: "", 17 | passwordConfirmation: "", 18 | }; 19 | 20 | export const LOGIN_FORM_VALIDATION_SCHEMA = yup.object().shape({ 21 | email: yup.string().email("Invalid email address").required("Required"), 22 | password: yup.string().required("Required"), 23 | }); 24 | 25 | export const RESET_PASSWORD_FORM_VALIDATION_SCHEMA = yup.object().shape({ 26 | email: yup.string().email("Invalid email address").required("Required"), 27 | }); 28 | 29 | export const SIGNUP_FORM_VALIDATION_SCHEMA = yup.object().shape({ 30 | email: yup.string().email("Invalid email address").required("Required"), 31 | firstName: yup.string().required("Required"), 32 | lastName: yup.string().required("Required"), 33 | password: yup.string().required("Required"), 34 | passwordConfirmation: yup 35 | .string() 36 | .oneOf([yup.ref("password")], "Passwords must match") 37 | .required("Required"), 38 | }); 39 | -------------------------------------------------------------------------------- /app/javascript/src/components/Dashboard/Notes/DeleteAlert.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { Alert } from "neetoui"; 4 | 5 | import notesApi from "apis/notes"; 6 | 7 | const DeleteAlert = ({ 8 | refetch, 9 | onClose, 10 | selectedNoteIds, 11 | setSelectedNoteIds, 12 | }) => { 13 | const [deleting, setDeleting] = useState(false); 14 | 15 | const handleDelete = async () => { 16 | try { 17 | setDeleting(true); 18 | await notesApi.destroy({ ids: selectedNoteIds }); 19 | onClose(); 20 | setSelectedNoteIds([]); 21 | refetch(); 22 | } catch (error) { 23 | logger.error(error); 24 | setDeleting(false); 25 | } 26 | }; 27 | 28 | return ( 29 | 1 ? "notes" : "note" 35 | }?`} 36 | onClose={onClose} 37 | onSubmit={handleDelete} 38 | /> 39 | ); 40 | }; 41 | 42 | export default DeleteAlert; 43 | -------------------------------------------------------------------------------- /app/javascript/src/components/Dashboard/Notes/Pane/Create.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Pane, Typography } from "neetoui"; 4 | 5 | import Form from "./Form"; 6 | 7 | import { NOTES_FORM_INITIAL_FORM_VALUES } from "../constants"; 8 | 9 | const Create = ({ fetchNotes, showPane, setShowPane }) => { 10 | const onClose = () => setShowPane(false); 11 | 12 | return ( 13 | 14 | 15 | 16 | Create a new note 17 | 18 | 19 |
25 | 26 | ); 27 | }; 28 | 29 | export default Create; 30 | -------------------------------------------------------------------------------- /app/javascript/src/components/Dashboard/Notes/Pane/Edit.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Pane, Typography } from "neetoui"; 4 | 5 | import Form from "./Form"; 6 | 7 | const Edit = ({ fetchNotes, showPane, setShowPane, note }) => { 8 | const onClose = () => setShowPane(false); 9 | 10 | return ( 11 | 12 | 13 | 14 | Edit note 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default Edit; 23 | -------------------------------------------------------------------------------- /app/javascript/src/components/Dashboard/Notes/Pane/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Formik, Form as FormikForm } from "formik"; 4 | import { Pane } from "neetoui"; 5 | import { ActionBlock, Input, Textarea } from "neetoui/formik"; 6 | 7 | import notesApi from "apis/notes"; 8 | 9 | import { NOTES_FORM_VALIDATION_SCHEMA } from "../constants"; 10 | 11 | const Form = ({ onClose, refetch, note, isEdit }) => { 12 | const handleSubmit = async values => { 13 | try { 14 | if (isEdit) { 15 | await notesApi.update(note.id, values); 16 | } else { 17 | await notesApi.create(values); 18 | } 19 | refetch(); 20 | onClose(); 21 | } catch (err) { 22 | logger.error(err); 23 | } 24 | }; 25 | 26 | return ( 27 | 32 | 33 | 34 | 40 |