├── .browserslistrc ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── deploy.yml │ ├── js-ci.yml │ └── ruby-ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .rubocop.yml ├── .ruby-version ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── releases │ └── yarn-3.1.1.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ └── api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── index.js │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── config │ │ └── manifest.js │ ├── fonts │ │ ├── Outfit-Black.woff2 │ │ ├── Outfit-Bold.woff2 │ │ ├── Outfit-ExtraBold.woff2 │ │ ├── Outfit-ExtraLight.woff2 │ │ ├── Outfit-Light.woff2 │ │ ├── Outfit-Medium.woff2 │ │ ├── Outfit-Regular.woff2 │ │ ├── Outfit-SemiBold.woff2 │ │ ├── Outfit-Thin.woff2 │ │ ├── fontawesome │ │ │ ├── LICENSE.txt │ │ │ ├── css │ │ │ │ └── all.min.css │ │ │ └── webfonts │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-regular-400.woff2 │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-solid-900.woff2 │ │ │ │ ├── fa-v4compatibility.ttf │ │ │ │ └── fa-v4compatibility.woff2 │ │ └── fonts.css │ ├── images │ │ ├── icon-128.png │ │ ├── icon-square-128.png │ │ └── icon.svg │ └── stylesheets │ │ └── application.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ ├── application_controller.rb │ │ ├── oauth_controller.rb │ │ ├── reminders_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── auth_controller.rb │ ├── authorizations_controller.rb │ ├── concerns │ │ └── .keep │ ├── errors_controller.rb │ ├── hovercard_controller.rb │ ├── integrations_controller.rb │ ├── landing_controller.rb │ ├── oauth_apps_controller.rb │ ├── oauth_callback_controller.rb │ ├── personal_tokens_controller.rb │ ├── reminders_controller.rb │ ├── sessions_controller.rb │ ├── settings_controller.rb │ └── signup_controller.rb ├── data │ └── oauth.rb ├── helpers │ ├── api │ │ └── oauth_helper.rb │ ├── application_helper.rb │ ├── auth_helper.rb │ ├── authorizations_helper.rb │ ├── errors_helper.rb │ ├── hovercard_helper.rb │ ├── integrations_helper.rb │ ├── landing_helper.rb │ ├── oauth_apps_helper.rb │ ├── oauth_callback_helper.rb │ ├── personal_tokens_helper.rb │ ├── reminders_helper.rb │ ├── settings_helper.rb │ └── signup_helper.rb ├── javascript │ ├── application.ts │ ├── controllers │ │ ├── alert_controller.ts │ │ ├── copy_controller.ts │ │ ├── form_confirm_controller.tsx │ │ ├── form_controller.ts │ │ ├── hovercard_controller.ts │ │ └── index.ts │ ├── shim │ │ └── jsx-shim.ts │ └── types │ │ └── jsx-shim.d.ts ├── jobs │ ├── application_job.rb │ └── cache_link_unfurl_job.rb ├── mailers │ ├── application_mailer.rb │ └── user_mailer.rb ├── models │ ├── api_token.rb │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ └── expirable.rb │ ├── oauth_app.rb │ ├── oauth_grant.rb │ ├── password_reset_request.rb │ ├── reminder.rb │ ├── session.rb │ ├── signup_request.rb │ └── user.rb ├── policies │ ├── oauth_app_policy.rb │ └── reminder_policy.rb ├── services │ ├── github_service.rb │ ├── oauth_service │ │ └── github.rb │ ├── open_graph_service.rb │ └── unfurl_service.rb └── views │ ├── api │ ├── oauth │ │ └── authorize.html.haml │ ├── reminders │ │ ├── _reminder.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ └── users │ │ └── me.json.jbuilder │ ├── auth │ ├── forgot_password.html.haml │ ├── forgot_password_submit.html.haml │ ├── forgot_password_verify.html.haml │ └── login.html.haml │ ├── authorizations │ └── _authorization.html.haml │ ├── errors │ ├── internal_server_error.html.haml │ └── not_found.html.haml │ ├── hovercard │ └── unfurl.html.haml │ ├── integrations │ └── index.html.haml │ ├── landing │ ├── _message.html.haml │ ├── _reminder.html.haml │ └── index.html.haml │ ├── layouts │ ├── _sm.html.haml │ ├── application.html.haml │ ├── hovercard.html.haml │ ├── mailer.html.haml │ └── mailer.text.haml │ ├── oauth_apps │ ├── _layout.html.haml │ ├── advanced.html.haml │ ├── edit.html.haml │ ├── index.html.haml │ ├── new.html.haml │ └── show.html.haml │ ├── personal_tokens │ ├── index.html.haml │ └── new.html.haml │ ├── reminders │ ├── _reminder.html.haml │ ├── create.turbo_stream.haml │ ├── destroy.turbo_stream.haml │ └── index.html.haml │ ├── sessions │ └── _session.html.haml │ ├── settings │ ├── _layout.html.haml │ ├── developer.html.haml │ ├── index.html.haml │ └── security.html.haml │ ├── shared │ ├── _alert.html.haml │ ├── _header.html.haml │ ├── _landing_header.html.haml │ ├── _notice.haml │ └── _toast.html.haml │ ├── signup │ ├── index.html.haml │ ├── submit.html.haml │ └── verify.html.haml │ └── user_mailer │ ├── password_changed.html.haml │ ├── password_reset.html.haml │ ├── signup_verification.html.haml │ └── signup_verification.text.haml ├── bin ├── bundle ├── dev ├── esbuild ├── rails ├── rake ├── setup ├── spring └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── honeybadger.yml ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── honeybadger.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── permissions_policy.rb │ ├── redis.rb │ ├── sidekiq.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── routes │ └── api.rb ├── spring.rb └── storage.yml ├── db ├── migrate │ ├── 20211111021027_create_users.rb │ ├── 20211111022726_rename_hashed_password.rb │ ├── 20211111041143_create_sessions.rb │ ├── 20211111052623_create_reminders.rb │ ├── 20211119030654_create_signup_requests.rb │ ├── 20211125033049_unique_fields.rb │ ├── 20211125044511_create_oauth_apps.rb │ ├── 20211125050029_add_name_to_oauth_app.rb │ ├── 20211126050826_add_official_to_oauth_app.rb │ ├── 20211128041643_create_oauth_grants.rb │ ├── 20211128044302_add_expires_at_to_oauth_grant.rb │ ├── 20211128191000_add_fulfilled_to_oauth_grants.rb │ ├── 20211129020838_create_api_tokens.rb │ ├── 20211201171829_add_oauth_app_to_reminders.rb │ ├── 20211201172253_reminder_oauth_app_optional.rb │ ├── 20211202020630_add_ip_and_user_agent_to_sessions.rb │ ├── 20211202174211_add_login_method_to_session.rb │ ├── 20211202225129_add_description_to_api_token.rb │ ├── 20211207152821_add_public_to_oauth_app.rb │ ├── 20211209041857_create_password_reset_requests.rb │ ├── 20211214205049_password_reset_request_fulfilled_default.rb │ ├── 20211215035648_create_active_storage_tables.active_storage.rb │ ├── 20211217022916_add_fields_to_reminder.rb │ ├── 20211217023036_author_name_to_author.rb │ ├── 20211217030224_title_to_description.rb │ ├── 20211219051154_reminder_description_to_text.rb │ ├── 20211229214208_add_installation_url_to_oauth_apps.rb │ ├── 20220122173730_add_service_name_to_active_storage_blobs.active_storage.rb │ ├── 20220122173731_create_active_storage_variant_records.active_storage.rb │ ├── 20220122173732_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ ├── 20220123025226_add_source_to_reminder.rb │ ├── 20220224233219_add_discarded_at_to_oauth_apps.rb │ └── 20220320193230_add_last_active_at_to_session.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── package.json ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── robots.txt └── site.webmanifest ├── storage └── .keep ├── tailwind.config.js ├── test ├── application_system_test_case.rb ├── channels │ └── application_cable │ │ └── connection_test.rb ├── controllers │ ├── .keep │ ├── auth_controller_test.rb │ ├── authorizations_controller_test.rb │ ├── errors_controller_test.rb │ ├── hovercard_controller_test.rb │ ├── integrations_controller_test.rb │ ├── landing_controller_test.rb │ ├── oauth_apps_controller_test.rb │ ├── oauth_callback_controller_test.rb │ ├── personal_tokens_controller_test.rb │ ├── reminders_controller_test.rb │ ├── settings_controller_test.rb │ └── signup_controller_test.rb ├── fixtures │ ├── api_tokens.yml │ ├── files │ │ └── .keep │ ├── oauth_apps.yml │ ├── oauth_grants.yml │ ├── password_reset_requests.yml │ ├── reminders.yml │ ├── sessions.yml │ ├── signup_requests.yml │ └── users.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── jobs │ └── cache_link_unfurl_job_test.rb ├── mailers │ ├── .keep │ ├── previews │ │ └── user_mailer_preview.rb │ └── user_mailer_test.rb ├── models │ ├── .keep │ ├── api_token_test.rb │ ├── oauth_app_test.rb │ ├── oauth_grant_test.rb │ ├── password_reset_request_test.rb │ ├── reminder_test.rb │ ├── session_test.rb │ ├── signup_request_test.rb │ └── user_test.rb ├── system │ └── .keep └── test_helper.rb ├── tmp ├── .keep └── pids │ └── .keep ├── tsconfig.json ├── vendor └── .keep └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /*.config.js 2 | /bin/esbuild -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 13, 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "rules": { 14 | "eqeqeq": "warn" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark the yarn lockfile as having been generated. 7 | yarn.lock linguist-generated 8 | 9 | # Mark any vendored files as having been vendored. 10 | vendor/* linguist-vendored 11 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | concurrency: deploy 12 | 13 | jobs: 14 | app: 15 | name: App 16 | runs-on: ubuntu-latest 17 | needs: 18 | - docker 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: appleboy/ssh-action@v0.1.4 22 | with: 23 | host: "159.223.133.179" 24 | username: dokku 25 | key: ${{ secrets.SSH_PRIVATE_KEY }} 26 | script: git:from-image resolute ghcr.io/resoluteapp/resolute@${{ needs.docker.outputs.digest }} 27 | - name: Report to Honeybadger 28 | uses: honeybadger-io/github-notify-deploy-action@v1 29 | with: 30 | api_key: ${{ secrets.HONEYBADGER_API_KEY }} 31 | - name: Report to Honeybadger (again) 32 | uses: honeybadger-io/github-notify-deploy-action@v1 33 | with: 34 | api_key: ${{ secrets.HONEYBADGER_FRONTEND_API_KEY }} 35 | 36 | docker: 37 | name: Docker 38 | runs-on: ubuntu-latest 39 | permissions: 40 | contents: read 41 | packages: write 42 | outputs: 43 | digest: ${{ steps.push.outputs.digest }} 44 | 45 | steps: 46 | - name: Checkout repository 47 | uses: actions/checkout@v2 48 | 49 | - name: Log in to the Container registry 50 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 51 | with: 52 | registry: ${{ env.REGISTRY }} 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Extract metadata (tags, labels) for Docker 57 | id: meta 58 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 59 | with: 60 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 61 | 62 | - name: Set up Docker Buildx 63 | uses: docker/setup-buildx-action@v1 64 | 65 | - name: Build and push Docker image 66 | id: push 67 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 68 | with: 69 | context: . 70 | push: true 71 | tags: ${{ steps.meta.outputs.tags }} 72 | labels: ${{ steps.meta.outputs.labels }} 73 | build-args: HONEYBADGER_FRONTEND_API_KEY=${{ secrets.HONEYBADGER_FRONTEND_API_KEY }} 74 | -------------------------------------------------------------------------------- /.github/workflows/js-ci.yml: -------------------------------------------------------------------------------- 1 | name: JavaScript CI 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: "16" 15 | - run: yarn install 16 | - run: yarn ts-check 17 | format: 18 | name: Format 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v2 23 | with: 24 | node-version: "16" 25 | - run: yarn install 26 | - run: yarn format:check 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-node@v2 33 | with: 34 | node-version: "16" 35 | - run: yarn install 36 | - run: yarn lint 37 | -------------------------------------------------------------------------------- /.github/workflows/ruby-ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | lint: 8 | name: Lint 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: ruby/setup-ruby@v1 13 | with: 14 | bundler-cache: true 15 | - run: bundle exec rubocop app/ config/ 16 | -------------------------------------------------------------------------------- /.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 | .byebug_history 31 | 32 | # Ignore master key for decrypting credentials and more. 33 | /config/master.key 34 | 35 | /public/packs 36 | /public/packs-test 37 | /node_modules 38 | /yarn-error.log 39 | yarn-debug.log* 40 | .yarn-integrity 41 | 42 | /config/credentials/development.key 43 | 44 | /app/assets/builds/* 45 | !/app/assets/builds/.keep 46 | 47 | .DS_Store 48 | 49 | # Yarn stuff 50 | .pnp.* 51 | .yarn/* 52 | !.yarn/patches 53 | !.yarn/plugins 54 | !.yarn/releases 55 | !.yarn/sdks 56 | !.yarn/versions -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | app/assets/builds/ 2 | .pnp.* 3 | .yarn/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true 3 | } 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rails 2 | 3 | Style/Documentation: 4 | Enabled: false 5 | Style/GlobalVars: 6 | Enabled: false 7 | Metrics/AbcSize: 8 | Severity: info 9 | Enabled: false 10 | Metrics/CyclomaticComplexity: 11 | Severity: info 12 | Enabled: false 13 | Metrics/PerceivedComplexity: 14 | Severity: info 15 | Enabled: false 16 | Rails/TimeZone: 17 | Enabled: false 18 | Layout/IndentationStyle: 19 | EnforcedStyle: tabs 20 | IndentationWidth: 1 21 | Layout/IndentationWidth: 22 | Width: 1 23 | 24 | Metrics/BlockLength: 25 | Exclude: 26 | - "config/**/*" 27 | 28 | # Some defaults 29 | Gemspec/DateAssignment: # new in 1.10 30 | Enabled: true 31 | Gemspec/RequireMFA: # new in 1.23 32 | Enabled: true 33 | Layout/LineEndStringConcatenationIndentation: # new in 1.18 34 | Enabled: true 35 | Layout/SpaceBeforeBrackets: # new in 1.7 36 | Enabled: true 37 | Lint/AmbiguousAssignment: # new in 1.7 38 | Enabled: true 39 | Lint/AmbiguousOperatorPrecedence: # new in 1.21 40 | Enabled: true 41 | Lint/AmbiguousRange: # new in 1.19 42 | Enabled: true 43 | Lint/DeprecatedConstants: # new in 1.8 44 | Enabled: true 45 | Lint/DuplicateBranch: # new in 1.3 46 | Enabled: true 47 | Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 48 | Enabled: true 49 | Lint/EmptyBlock: # new in 1.1 50 | Enabled: true 51 | Lint/EmptyClass: # new in 1.3 52 | Enabled: true 53 | Lint/EmptyInPattern: # new in 1.16 54 | Enabled: true 55 | Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 56 | Enabled: true 57 | Lint/LambdaWithoutLiteralBlock: # new in 1.8 58 | Enabled: true 59 | Lint/NoReturnInBeginEndBlocks: # new in 1.2 60 | Enabled: true 61 | Lint/NumberedParameterAssignment: # new in 1.9 62 | Enabled: true 63 | Lint/OrAssignmentToConstant: # new in 1.9 64 | Enabled: true 65 | Lint/RedundantDirGlobSort: # new in 1.8 66 | Enabled: true 67 | Lint/RequireRelativeSelfPath: # new in 1.22 68 | Enabled: true 69 | Lint/SymbolConversion: # new in 1.9 70 | Enabled: true 71 | Lint/ToEnumArguments: # new in 1.1 72 | Enabled: true 73 | Lint/TripleQuotes: # new in 1.9 74 | Enabled: true 75 | Lint/UnexpectedBlockArity: # new in 1.5 76 | Enabled: true 77 | Lint/UnmodifiedReduceAccumulator: # new in 1.1 78 | Enabled: true 79 | Lint/UselessRuby2Keywords: # new in 1.23 80 | Enabled: true 81 | Security/IoMethods: # new in 1.22 82 | Enabled: true 83 | Style/ArgumentsForwarding: # new in 1.1 84 | Enabled: true 85 | Style/CollectionCompact: # new in 1.2 86 | Enabled: true 87 | Style/DocumentDynamicEvalDefinition: # new in 1.1 88 | Enabled: true 89 | Style/EndlessMethod: # new in 1.8 90 | Enabled: true 91 | Style/HashConversion: # new in 1.10 92 | Enabled: true 93 | Style/HashExcept: # new in 1.7 94 | Enabled: true 95 | Style/IfWithBooleanLiteralBranches: # new in 1.9 96 | Enabled: true 97 | Style/InPatternThen: # new in 1.16 98 | Enabled: true 99 | Style/MultilineInPatternThen: # new in 1.16 100 | Enabled: true 101 | Style/NegatedIfElseCondition: # new in 1.2 102 | Enabled: true 103 | Style/NilLambda: # new in 1.3 104 | Enabled: true 105 | Style/NumberedParameters: # new in 1.22 106 | Enabled: true 107 | Style/NumberedParametersLimit: # new in 1.22 108 | Enabled: true 109 | Style/OpenStructUse: # new in 1.23 110 | Enabled: true 111 | Style/QuotedSymbols: # new in 1.16 112 | Enabled: true 113 | Style/RedundantArgument: # new in 1.4 114 | Enabled: true 115 | Style/RedundantSelfAssignmentBranch: # new in 1.19 116 | Enabled: true 117 | Style/SelectByRegexp: # new in 1.22 118 | Enabled: true 119 | Style/StringChars: # new in 1.12 120 | Enabled: true 121 | Style/SwapValues: # new in 1.1 122 | Enabled: true 123 | Rails/ActiveRecordCallbacksOrder: # new in 2.7 124 | Enabled: true 125 | Rails/AddColumnIndex: # new in 2.11 126 | Enabled: true 127 | Rails/AfterCommitOverride: # new in 2.8 128 | Enabled: true 129 | Rails/AttributeDefaultBlockValue: # new in 2.9 130 | Enabled: true 131 | Rails/EagerEvaluationLogMessage: # new in 2.11 132 | Enabled: true 133 | Rails/ExpandedDateRange: # new in 2.11 134 | Enabled: true 135 | Rails/FindById: # new in 2.7 136 | Enabled: true 137 | Rails/I18nLocaleAssignment: # new in 2.11 138 | Enabled: true 139 | Rails/Inquiry: # new in 2.7 140 | Enabled: true 141 | Rails/MailerName: # new in 2.7 142 | Enabled: true 143 | Rails/MatchRoute: # new in 2.7 144 | Enabled: true 145 | Rails/NegateInclude: # new in 2.7 146 | Enabled: true 147 | Rails/Pluck: # new in 2.7 148 | Enabled: true 149 | Rails/PluckInWhere: # new in 2.7 150 | Enabled: true 151 | Rails/RedundantTravelBack: # new in 2.12 152 | Enabled: true 153 | Rails/RenderInline: # new in 2.7 154 | Enabled: true 155 | Rails/RenderPlainText: # new in 2.7 156 | Enabled: true 157 | Rails/ShortI18n: # new in 2.7 158 | Enabled: true 159 | Rails/SquishedSQLHeredocs: # new in 2.8 160 | Enabled: true 161 | Rails/TimeZoneAssignment: # new in 2.10 162 | Enabled: true 163 | Rails/UnusedIgnoredColumns: # new in 2.11 164 | Enabled: true 165 | Rails/WhereEquals: # new in 2.9 166 | Enabled: true 167 | Rails/WhereExists: # new in 2.7 168 | Enabled: true 169 | Rails/WhereNot: # new in 2.8 170 | Enabled: true 171 | 172 | Naming/BlockForwarding: # new in 1.24 173 | Enabled: true 174 | Style/FileRead: # new in 1.24 175 | Enabled: true 176 | Style/FileWrite: # new in 1.24 177 | Enabled: true 178 | Style/MapToHash: # new in 1.24 179 | Enabled: true 180 | Rails/CompactBlank: # new in 2.13 181 | Enabled: true 182 | Rails/DurationArithmetic: # new in 2.13 183 | Enabled: true 184 | Rails/RedundantPresenceValidationOnBelongsTo: # new in 2.13 185 | Enabled: true 186 | Rails/RootJoinChain: # new in 2.13 187 | Enabled: true 188 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.0.3 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "vivaxy.vscode-conventional-commits", 7 | "esbenp.prettier-vscode", 8 | "rebornix.ruby", 9 | "castwide.solargraph", 10 | "karunamurti.haml", 11 | "bradlc.vscode-tailwindcss", 12 | "wingrunr21.vscode-ruby", 13 | "arcanis.vscode-zipfs", 14 | "dbaeumer.vscode-eslint" 15 | ], 16 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 17 | "unwantedRecommendations": [] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[haml]": { 3 | "editor.detectIndentation": false, 4 | "editor.insertSpaces": false 5 | }, 6 | "[ruby]": { 7 | "editor.detectIndentation": false, 8 | "editor.insertSpaces": false 9 | }, 10 | "scss.lint.unknownAtRules": "ignore", 11 | "css.validate": false, 12 | "search.exclude": { 13 | "**/.yarn": true, 14 | "**/.pnp.*": true 15 | }, 16 | "eslint.nodePath": ".yarn/sdks", 17 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js", 18 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 19 | "typescript.enablePromptUseWorkspaceTsdk": true, 20 | "ruby.format": "rubocop", 21 | "ruby.lint": { 22 | "rubocop": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint your application uses 20 | module.exports = absRequire(`eslint`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.6.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/index.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/index.js your application uses 20 | module.exports = absRequire(`prettier/index.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "2.5.1-sdk", 4 | "main": "./index.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, createRequireFromPath} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/typescript.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/typescript.js your application uses 20 | module.exports = absRequire(`typescript/lib/typescript.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "4.5.4-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.3 2 | 3 | ARG HONEYBADGER_FRONTEND_API_KEY 4 | 5 | ENV RAILS_ENV production 6 | ENV NODE_ENV production 7 | 8 | WORKDIR /usr/src/app 9 | 10 | RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \ 11 | apt-get install -y nodejs && \ 12 | corepack enable 13 | 14 | COPY . . 15 | 16 | RUN bundle install && \ 17 | SECRET_KEY_BASE=1 bin/rails assets:precompile 18 | 19 | CMD ["bin/rails", "server"] 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | ruby '3.0.3' 7 | 8 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main' 9 | gem 'rails', '~> 7.0', '>= 7.0.1' 10 | # Use Postgres as the database for Active Record 11 | gem 'pg', '~> 1.2', '>= 1.2.3' 12 | # Use Puma as the app server 13 | gem 'puma', '~> 5.0' 14 | 15 | gem 'turbo-rails' 16 | 17 | gem 'faraday' 18 | gem 'faraday_middleware' 19 | 20 | gem 'pundit' 21 | 22 | gem 'haml', '~> 5.2' 23 | 24 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 25 | gem 'jbuilder', '~> 2.7' 26 | 27 | # Use Redis adapter to run Action Cable in production 28 | gem 'redis', '~> 4.0' 29 | gem "redis-namespace", "~> 1.8" 30 | 31 | # Use Active Model has_secure_password 32 | gem 'bcrypt', '~> 3.1.7' 33 | 34 | # Use Active Storage variant 35 | # gem 'image_processing', '~> 1.2' 36 | 37 | # Reduces boot times through caching; required in config/boot.rb 38 | gem 'bootsnap', '~> 1.9', '>= 1.9.3', require: false 39 | 40 | group :development, :test do 41 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 42 | gem 'byebug', platforms: %i[mri mingw x64_mingw] 43 | end 44 | 45 | group :development do 46 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code. 47 | gem 'web-console', '>= 4.1.0' 48 | # Display performance information such as SQL time and flame graphs for each request in your browser. 49 | # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md 50 | gem 'listen', '~> 3.3' 51 | gem 'rack-mini-profiler', '~> 2.0', require: false 52 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 53 | gem 'spring' 54 | 55 | gem 'rubocop', '~> 1.23', require: false 56 | gem 'rubocop-rails', '~> 2.12', require: false 57 | end 58 | 59 | group :test do 60 | # Adds support for Capybara system testing and selenium driver 61 | gem 'capybara', '>= 3.26' 62 | gem 'selenium-webdriver' 63 | # Easy installation and use of web drivers to run system tests with browsers 64 | gem 'webdrivers' 65 | end 66 | 67 | gem "browser", "~> 5.3" 68 | 69 | gem "tzinfo-data", "~> 1.2021" 70 | 71 | gem "jsbundling-rails", "~> 0.2.2" 72 | 73 | gem "cssbundling-rails", "~> 0.2.7" 74 | 75 | gem "sprockets-rails", "~> 3.4" 76 | 77 | gem "nokogiri", "~> 1.13" 78 | 79 | gem "honeybadger", "~> 4.10" 80 | 81 | gem "rinku", "~> 2.0" 82 | 83 | gem "sidekiq", "~> 6.4", require: false 84 | 85 | gem "discard", "~> 1.2" 86 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: bin/rails db:migrate 2 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | js: yarn build --watch 3 | css: yarn build:css --watch 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👀 2 | 3 | coming soon... 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../builds 2 | //= link_tree ../images 3 | //= link fonts.css 4 | //= link fontawesome/css/all.min.css 5 | -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-Black.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-Bold.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-ExtraBold.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-ExtraLight.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-Light.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-Medium.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-Regular.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-SemiBold.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/Outfit-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/Outfit-Thin.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/fonts/fontawesome/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Outfit"; 3 | font-style: normal; 4 | font-weight: 100; 5 | font-display: swap; 6 | src: url("Outfit-Thin.woff2") format("woff"); 7 | } 8 | 9 | @font-face { 10 | font-family: "Outfit"; 11 | font-style: normal; 12 | font-weight: 200; 13 | font-display: swap; 14 | src: url("Outfit-ExtraLight.woff2") format("woff"); 15 | } 16 | 17 | @font-face { 18 | font-family: "Outfit"; 19 | font-style: normal; 20 | font-weight: 300; 21 | font-display: swap; 22 | src: url("Outfit-Light.woff2") format("woff"); 23 | } 24 | 25 | @font-face { 26 | font-family: "Outfit"; 27 | font-style: normal; 28 | font-weight: 400; 29 | font-display: swap; 30 | src: url("Outfit-Regular.woff2") format("woff"); 31 | } 32 | 33 | @font-face { 34 | font-family: "Outfit"; 35 | font-style: normal; 36 | font-weight: 500; 37 | font-display: swap; 38 | src: url("Outfit-Medium.woff2") format("woff"); 39 | } 40 | 41 | @font-face { 42 | font-family: "Outfit"; 43 | font-style: normal; 44 | font-weight: 600; 45 | font-display: swap; 46 | src: url("Outfit-SemiBold.woff2") format("woff"); 47 | } 48 | 49 | @font-face { 50 | font-family: "Outfit"; 51 | font-style: normal; 52 | font-weight: 700; 53 | font-display: swap; 54 | src: url("Outfit-Bold.woff2") format("woff"); 55 | } 56 | 57 | @font-face { 58 | font-family: "Outfit"; 59 | font-style: normal; 60 | font-weight: 800; 61 | font-display: swap; 62 | src: url("Outfit-ExtraBold.woff2") format("woff"); 63 | } 64 | 65 | @font-face { 66 | font-family: "Outfit"; 67 | font-style: normal; 68 | font-weight: 900; 69 | font-display: swap; 70 | src: url("Outfit-Black.woff2") format("woff"); 71 | } 72 | -------------------------------------------------------------------------------- /app/assets/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/images/icon-128.png -------------------------------------------------------------------------------- /app/assets/images/icon-square-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/assets/images/icon-square-128.png -------------------------------------------------------------------------------- /app/assets/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body, 7 | html { 8 | @apply bg-black text-white; 9 | } 10 | } 11 | 12 | @layer components { 13 | .btn { 14 | @apply cursor-pointer disabled:cursor-default text-white rounded-md py-2 px-4 shadow-md hover:shadow-lg hover:no-underline disabled:opacity-50 bg-green-600 transition; 15 | } 16 | 17 | a, 18 | .link { 19 | @apply text-green-500 hover:underline cursor-pointer; 20 | } 21 | 22 | .input { 23 | @apply bg-black block w-full py-3 px-4 rounded-md placeholder-gray-500 border-2 border-gray-700 focus:outline-none focus:border-green-500 disabled:cursor-not-allowed disabled:text-gray-500; 24 | 25 | &.unobtrusive { 26 | @apply border-transparent focus:border-green-500; 27 | } 28 | } 29 | 30 | .modal:not(.modal--visible) { 31 | @apply opacity-0; 32 | 33 | & .modal__content { 34 | @apply scale-95; 35 | } 36 | } 37 | 38 | .alert { 39 | @apply text-white rounded-md py-3 px-4 flex justify-between items-center gap-3 shadow-lg bg-red-500; 40 | } 41 | 42 | .notice { 43 | @apply text-white rounded-md py-3 px-4 flex justify-between items-center gap-3 shadow-lg bg-yellow-600; 44 | } 45 | } 46 | 47 | .turbo-progress-bar { 48 | @apply bg-green-700; 49 | } 50 | 51 | [x-cloak] { 52 | display: none !important; 53 | } 54 | 55 | .has-overlay-link a:not(.overlay-link), 56 | .has-overlay-link button { 57 | @apply z-10 relative; 58 | } 59 | -------------------------------------------------------------------------------- /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/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class ApplicationController < ActionController::Base 5 | before_action :authenticate 6 | skip_before_action :verify_authenticity_token 7 | 8 | rescue_from ActionController::ParameterMissing do |e| 9 | render json: { 10 | error: 'invalid_input', 11 | description: "Missing required parameter: #{e.param}" 12 | }, status: :bad_request 13 | end 14 | rescue_from ActiveRecord::RecordInvalid do |e| 15 | render json: { 16 | error: 'invalid_input', 17 | description: e 18 | }, status: :bad_request 19 | end 20 | 21 | private 22 | 23 | def authenticate 24 | @token = authenticate_with_http_token do |t, _options| 25 | ApiToken.find_by(token: t) 26 | end 27 | 28 | return render json: { error: 'invalid_token' }, status: :unauthorized if @token.nil? 29 | end 30 | 31 | def require_scope(scope) 32 | # Allow tokens that have the scope, or are personal tokens 33 | return if @token.scope&.include?(scope) || @token.personal? 34 | 35 | render json: { error: 'missing_scope', description: "Missing scope: #{scope}" }, 36 | status: :forbidden 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/controllers/api/oauth_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class OauthController < ::ApplicationController 5 | before_action :require_auth, only: %i[authorize authorize_submit] 6 | skip_before_action :verify_authenticity_token, only: [:token] 7 | 8 | def authorize 9 | @hide_header = true 10 | 11 | @app = OauthApp.kept.find_by(client_id: params[:client_id]) 12 | 13 | @scopes = (params[:scope] || '').split(/(?:,\s*|\s)/).map(&:downcase).filter do |scope| 14 | !Oauth.scopes[scope].nil? 15 | end 16 | 17 | @state = params[:state] 18 | @response_type = params[:response_type] || 'code' 19 | 20 | @invalid_response_type = true unless %w[token code].include? @response_type 21 | end 22 | 23 | # rubocop:disable Metrics/MethodLength 24 | def authorize_submit 25 | @app = OauthApp.kept.find_by!(client_id: params[:client_id]) 26 | 27 | uri = URI(@app.redirect_uri) 28 | 29 | case params[:commit] 30 | when 'Cancel' 31 | case params[:response_type] 32 | when 'code' 33 | uri.query = URI.encode_www_form(error: 'access_denied') 34 | when 'token' 35 | uri.fragment = URI.encode_www_form(error: 'access_denied') 36 | end 37 | when 'Connect' 38 | case params[:response_type] 39 | when 'code' 40 | grant = OauthGrant.create!(oauth_app: @app, user: @current_user, scope: JSON.parse(params[:scope])) 41 | uri.query = URI.encode_www_form({ code: grant.code, state: params[:state] }.compact) 42 | when 'token' 43 | scope = JSON.parse(params[:scope]) 44 | 45 | token = ApiToken.create(scope: scope, oauth_app: @app, user: @current_user) 46 | 47 | uri.fragment = URI.encode_www_form({ 48 | access_token: token.token, 49 | token_type: 'bearer', 50 | scope: scope.join(', '), 51 | state: params[:state] 52 | }.compact) 53 | end 54 | end 55 | 56 | redirect_to uri.to_s, allow_other_host: true 57 | end 58 | # rubocop:enable Metrics/MethodLength 59 | 60 | def token 61 | grant_type, code, client_id, client_secret = params.require(%i[grant_type code client_id client_secret]) 62 | 63 | return render_oauth_error 'unsupported_grant_type' if grant_type != 'authorization_code' 64 | 65 | return render_oauth_error 'invalid_client' unless authenticate_oauth_app(client_id, client_secret) 66 | 67 | return render_oauth_error 'invalid_grant' unless check_grant_validity(code) 68 | 69 | issue_access_token 70 | rescue ActionController::ParameterMissing 71 | render_oauth_error 'invalid_request' 72 | end 73 | 74 | private 75 | 76 | def render_oauth_error(error) 77 | render json: { error: error }, status: :bad_request 78 | end 79 | 80 | # Ensures that 81 | # - the client ID belongs to an app 82 | # - the client secret is valid 83 | def authenticate_oauth_app(client_id, client_secret) 84 | @app = OauthApp.kept.find_by(client_id: client_id) 85 | 86 | !(@app.nil? || @app.client_secret != client_secret) 87 | end 88 | 89 | # Ensures that 90 | # - the OAuth grant exists 91 | # - the grant was created by the current app 92 | def check_grant_validity(code) 93 | @grant = OauthGrant.find_by(code: code) 94 | 95 | !(@grant.nil? || @grant.fulfilled || @grant.expired? || @grant.oauth_app != @app) 96 | end 97 | 98 | # Issues an access token 99 | def issue_access_token 100 | token = ApiToken.create(scope: @grant.scope, oauth_app: @app, user: @grant.user) 101 | @grant.fulfill! 102 | 103 | render json: { 104 | access_token: token.token, 105 | token_type: 'bearer', 106 | scope: @grant.scope.join(', ') 107 | } 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /app/controllers/api/reminders_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class RemindersController < Api::ApplicationController 5 | before_action(only: :index) { require_scope('reminders:view') } 6 | before_action(only: :create) { require_scope('reminders:create') } 7 | before_action(only: :destroy) { require_scope('reminders:create') } 8 | 9 | def index 10 | @reminders = @token.user.reminders 11 | end 12 | 13 | def create 14 | @reminder = Reminder.create!(params.permit(:text, :title, :author, :author_avatar, :url, :source) 15 | .merge({ user: @token.user, oauth_app: @token.oauth_app })) 16 | 17 | render 'show', status: :created 18 | end 19 | 20 | def destroy 21 | reminder = Reminder.find_by!(id: params[:id], user: @token.user) 22 | 23 | reminder.destroy 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | class UsersController < Api::ApplicationController 5 | def me 6 | @user = @token.user 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | include Pundit 5 | 6 | before_action :authenticate 7 | before_action :touch_current_session 8 | 9 | before_action do 10 | Honeybadger.context( 11 | user_id: @current_user&.id, 12 | user_email: @current_user&.email 13 | ) 14 | end 15 | 16 | rescue_from Pundit::NotAuthorizedError do 17 | render 'errors/not_found', status: :not_found 18 | end 19 | 20 | attr_reader :current_user 21 | 22 | private 23 | 24 | # Fetches the logged-in user 25 | def authenticate 26 | token = session[:token] 27 | @current_session = Session.find_by(token: token) 28 | 29 | @current_user = @current_session.user unless @current_session.nil? 30 | end 31 | 32 | def touch_current_session 33 | @current_session&.touch! 34 | end 35 | 36 | # Redirects to login if not logged in 37 | def require_auth 38 | return if @current_user 39 | 40 | flash.notice = 'You need to log in first!' 41 | 42 | redirect_to "/login?redirect_to=#{URI.encode_www_form_component(request.fullpath)}" 43 | end 44 | 45 | # Redirects to home if signed in 46 | def redirect_if_signed_in 47 | redirect_to '/home' if @current_user 48 | end 49 | 50 | def log_in(options) 51 | user_session = Session.create!(user: options[:user], ip: request.ip, user_agent: request.headers['User-Agent'], 52 | login_method: options[:method]) 53 | 54 | session[:token] = user_session.token 55 | 56 | begin 57 | redirect_to(options[:redirect_to] || '/home') 58 | rescue ActionController::Redirecting::UnsafeRedirectError 59 | redirect_to '/' 60 | end 61 | end 62 | 63 | def log_out 64 | Session.destroy_by(token: session[:token]) 65 | session.delete :token 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/controllers/auth_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AuthController < ApplicationController 4 | before_action :redirect_if_signed_in, except: [:logout] 5 | 6 | def login 7 | if flash[:prefill_email] 8 | @prefill_email = flash[:prefill_email] 9 | @focus_password = true 10 | end 11 | 12 | @redirect_to = params[:redirect_to] 13 | end 14 | 15 | def auth 16 | user = User.find_by(email: params['email']) 17 | 18 | @prefill_email = params['email'] 19 | 20 | return fail_auth 'User not found.' if user.nil? 21 | 22 | unless user.authenticate(params['password']) 23 | @focus_password = true 24 | 25 | return fail_auth 'Incorrect password.' 26 | end 27 | 28 | log_in user: user, redirect_to: params[:redirect_to] 29 | end 30 | 31 | def logout 32 | log_out 33 | 34 | flash.notice = "You've been logged out!" 35 | 36 | redirect_to '/' 37 | end 38 | 39 | def forgot_password_submit 40 | user = User.find_by(email: params[:email]) 41 | 42 | if user.nil? 43 | flash.now[:alert] = 'User not found.' 44 | render 'forgot_password', status: :unprocessable_entity 45 | else 46 | request = PasswordResetRequest.create(user: user) 47 | 48 | UserMailer.with(email: user.email, code: request.code).password_reset.deliver_later 49 | 50 | @email = user.email 51 | 52 | # Hack to get Turbo Drive to work properly 53 | render status: :unprocessable_entity 54 | end 55 | end 56 | 57 | def forgot_password_verify 58 | request = PasswordResetRequest.find_by(code: params[:code], fulfilled: false) 59 | 60 | return redirect_to '/login', alert: 'Invalid reset link.' if request.nil? 61 | return redirect_to '/login', alert: 'Reset link expired.' if request.expired? 62 | 63 | @code = request.code 64 | @email = request.user.email 65 | end 66 | 67 | def forgot_password_finalize 68 | request = PasswordResetRequest.find_by(code: params[:code], fulfilled: false) 69 | 70 | return redirect_to '/login', alert: 'Invalid reset link.' if request.nil? 71 | return redirect_to '/login', alert: 'Reset link expired.' if request.expired? 72 | 73 | request.fulfill! 74 | 75 | request.user.update!(password: params[:password]) 76 | 77 | UserMailer.with(email: request.user.email).password_changed.deliver_later 78 | 79 | flash.notice = 'Your password has been reset.' 80 | 81 | log_in user: request.user 82 | end 83 | 84 | private 85 | 86 | def fail_auth(message) 87 | flash.now[:alert] = message 88 | 89 | @redirect_to = params[:redirect_to] 90 | 91 | render 'login', status: :unprocessable_entity 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /app/controllers/authorizations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AuthorizationsController < ApplicationController 4 | before_action :require_auth 5 | 6 | def destroy 7 | ApiToken.destroy_by(user: @current_user, oauth_app_id: params[:id]) 8 | 9 | redirect_to '/settings/security' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/errors_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ErrorsController < ApplicationController 4 | def not_found 5 | render status: :not_found 6 | end 7 | 8 | def internal_server_error 9 | render status: :internal_server_error 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/hovercard_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HovercardController < ApplicationController 4 | def unfurl 5 | @url = params[:url] 6 | 7 | metadata = UnfurlService.run(@url) 8 | 9 | return head :not_found if metadata.blank? 10 | 11 | @title = metadata.title 12 | @description = metadata.description 13 | @favicon = metadata.favicon 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/integrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class IntegrationsController < ApplicationController 4 | before_action :require_auth 5 | before_action do 6 | @header_selected = :integrations 7 | end 8 | 9 | def index 10 | @apps = OauthApp.published 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/landing_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class LandingController < ApplicationController 4 | before_action :redirect_if_signed_in 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/oauth_apps_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OauthAppsController < ApplicationController 4 | before_action :require_auth 5 | before_action :find_app 6 | before_action do 7 | @header_selected = :settings 8 | end 9 | 10 | def index 11 | @apps = @current_user.oauth_apps.kept 12 | end 13 | 14 | def show 15 | @authorization_count = ApiToken.where(oauth_app: @app).count('DISTINCT user_id') 16 | end 17 | 18 | def create 19 | app = OauthApp.create!(params.require(:oauth_app).permit(:name).merge(user: current_user)) 20 | 21 | redirect_to app 22 | end 23 | 24 | def update 25 | @app.update(params.require(:oauth_app).permit(:name, :redirect_uri, :icon, :installation_url)) 26 | 27 | flash.notice = 'App has been updated.' 28 | 29 | redirect_back fallback_location: edit_oauth_app_path(@app) 30 | end 31 | 32 | def destroy 33 | @app.discard! 34 | 35 | redirect_to oauth_apps_path, notice: "App \"#{@app.name}\" has been deleted." 36 | end 37 | 38 | private 39 | 40 | def find_app 41 | return unless params[:id] 42 | 43 | @app = OauthApp.kept.find(params[:id]) 44 | 45 | authorize @app, :show? 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/controllers/oauth_callback_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OauthCallbackController < ApplicationController 4 | def github 5 | state = decode_state(params[:state]) if params[:state].present? 6 | access_token = OauthService::Github.new.exchange_code(params[:code]) 7 | 8 | return redirect_to '/' if access_token.nil? 9 | 10 | email = GithubService.new(access_token).primary_email 11 | user = User.find_by(email: email) 12 | 13 | return redirect_to_signup email if user.nil? 14 | 15 | log_in user: user, redirect_to: state&.[]('r'), method: 'GitHub' 16 | end 17 | 18 | private 19 | 20 | def redirect_to_signup(email) 21 | flash.notice = 'Sign up to continue.' 22 | flash[:email] = email 23 | 24 | redirect_to '/signup' 25 | end 26 | 27 | def decode_state(state) 28 | JSON.parse(Base64.urlsafe_decode64(state)) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/personal_tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PersonalTokensController < ApplicationController 4 | before_action :require_auth 5 | before_action do 6 | @header_selected = :settings 7 | end 8 | 9 | def index 10 | @tokens = @current_user.api_tokens.personal.order(created_at: :desc) 11 | end 12 | 13 | def create 14 | token = ApiToken.create!(params.permit(:description).merge(user: current_user)) 15 | 16 | flash[:personal_token] = token 17 | 18 | redirect_to personal_tokens_path 19 | end 20 | 21 | def destroy 22 | token = ApiToken.find_by(id: params[:id], user: @current_user, oauth_app: nil) 23 | 24 | token.destroy 25 | 26 | redirect_to personal_tokens_path 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/reminders_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemindersController < ApplicationController 4 | before_action :require_auth 5 | before_action do 6 | @header_selected = :reminders 7 | end 8 | 9 | def index 10 | @reminders = @current_user.reminders.order(created_at: :desc) 11 | end 12 | 13 | def create 14 | text = params[:text].strip 15 | 16 | @reminder = Reminder.create!(user: @current_user, text: text) 17 | 18 | respond_to do |format| 19 | format.html { redirect_to '/home' } 20 | format.turbo_stream 21 | end 22 | end 23 | 24 | def destroy 25 | @reminder = Reminder.find(params[:id]) 26 | 27 | authorize @reminder 28 | @reminder.destroy 29 | 30 | respond_to do |format| 31 | format.html { redirect_to '/home' } 32 | format.turbo_stream 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SessionsController < ApplicationController 4 | before_action :require_auth 5 | 6 | def destroy 7 | session = Session.find_by!(id: params[:id], user: @current_user) 8 | 9 | session.destroy 10 | 11 | if session == @current_session 12 | flash.notice = "You've been logged out!" 13 | 14 | redirect_to '/' 15 | else 16 | redirect_to '/settings/security' 17 | end 18 | end 19 | 20 | def destroy_all 21 | @current_user.sessions.where.not(id: @current_session.id).destroy_all 22 | 23 | redirect_to '/settings/security' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/settings_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SettingsController < ApplicationController 4 | before_action :require_auth 5 | before_action do 6 | @header_selected = :settings 7 | end 8 | 9 | def update 10 | if current_user.authenticate(params[:current_password]) 11 | current_user.update!(password: params[:new_password]) 12 | 13 | UserMailer.with(email: current_user.email).password_changed.deliver_later 14 | flash.notice = 'Your password has been changed.' 15 | else 16 | flash.alert = 'Incorrect password.' 17 | 18 | flash[:change_password_open] = true 19 | end 20 | 21 | redirect_back fallback_location: '/settings' 22 | end 23 | 24 | def developer 25 | @app_count = @current_user.oauth_apps.kept.count 26 | @personal_token_count = ApiToken.where(oauth_app: nil, user: @current_user).count 27 | end 28 | 29 | def security 30 | @sessions = @current_user.sessions.order(Arel.sql('COALESCE(last_active_at, created_at) DESC')) 31 | @authorizations = ApiToken.select('oauth_app_id, MAX(created_at) AS last_authorized_at') 32 | .not_personal 33 | .where(user: @current_user) 34 | .group(:oauth_app_id) 35 | .order(last_authorized_at: :desc) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/controllers/signup_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SignupController < ApplicationController 4 | before_action :redirect_if_signed_in 5 | 6 | def index 7 | @prefill_email = flash[:email] unless flash[:email].nil? 8 | end 9 | 10 | def submit 11 | @email = params[:email] 12 | 13 | unless User.find_by(email: @email).nil? 14 | flash.now[:alert] = 'An account with that email address already exists. Maybe you meant to log in?' 15 | @prefill_email = @email 16 | return render 'index', status: :unprocessable_entity 17 | end 18 | 19 | request = SignupRequest.create!(email: @email) 20 | 21 | UserMailer.with(email: @email, verification_code: request.code).signup_verification.deliver_later 22 | 23 | # Hack to get Turbo Drive to work properly 24 | render status: :unprocessable_entity 25 | end 26 | 27 | def verify 28 | request = SignupRequest.find_by(code: params[:code], fulfilled: false) 29 | 30 | return redirect_to '/signup', alert: 'Invalid verification link.' if request.nil? || request.user_signed_up? 31 | return redirect_to '/signup', alert: 'Verification link expired.' if request.expired? 32 | 33 | @email = request.email 34 | @verification_code = request.code 35 | end 36 | 37 | def finalize 38 | request = SignupRequest.find_by(code: params[:verification_code], fulfilled: false) 39 | 40 | return redirect_to '/signup', alert: 'Invalid verification link.' if request.nil? || request.user_signed_up? 41 | return redirect_to '/signup', alert: 'Verification link expired.' if request.expired? 42 | 43 | request.fulfill! 44 | 45 | user = User.create(email: request.email, password: params[:password]) 46 | log_in user: user 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/data/oauth.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Oauth 4 | def self.scopes 5 | { 6 | 'reminders:create' => 'Create and delete reminders', 7 | 'reminders:view' => 'View your reminders', 8 | 'user:email' => 'View your email address' 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/api/oauth_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module OauthHelper 5 | def scope_description(name) 6 | Oauth.scopes[name] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | def gravatar(email, size = 50) 5 | gravatar_id = Digest::MD5.hexdigest(email) 6 | gravatar_url = "https://www.gravatar.com/avatar/#{gravatar_id}?d=mp&s=#{size}" 7 | image_tag(gravatar_url, class: 'rounded-full', title: email, alt: email) 8 | end 9 | 10 | # rubocop:disable Metrics/MethodLength 11 | def sidebar_link(options) 12 | link_to(options[:title], options[:href], 13 | class: [ 14 | 'px-4', 15 | 'py-2', 16 | 'border-gray-600', 17 | 'border', 18 | 'border-b-0', 19 | 'last:border-b', 20 | 'first:rounded-t-md', 21 | 'last:rounded-b-md', 22 | 'cursor-pointer', 23 | 'hover:bg-gray-700', 24 | *('text-white' unless options[:selected]) 25 | ].join(' ')) 26 | end 27 | # rubocop:enable Metrics/MethodLength 28 | end 29 | -------------------------------------------------------------------------------- /app/helpers/auth_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AuthHelper 4 | def github_authorization_url(redirect_to) 5 | OauthService::Github.new.authorization_url(redirect_to) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/helpers/authorizations_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AuthorizationsHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/errors_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ErrorsHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/hovercard_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HovercardHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/integrations_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module IntegrationsHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/landing_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module LandingHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/oauth_apps_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OauthAppsHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/oauth_callback_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OauthCallbackHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/personal_tokens_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PersonalTokensHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/reminders_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RemindersHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/settings_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SettingsHelper 4 | # rubocop:disable Metrics/MethodLength 5 | def browser_to_icon(browser) 6 | if browser.chrome? 7 | 'chrome' 8 | elsif browser.safari? 9 | 'safari' 10 | elsif browser.firefox? 11 | 'firefox-browser' 12 | elsif browser.edge? 13 | 'edge' 14 | else 15 | 'window-maximize' 16 | end 17 | end 18 | # rubocop:enable Metrics/MethodLength 19 | end 20 | -------------------------------------------------------------------------------- /app/helpers/signup_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SignupHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/javascript/application.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | Stimulus: Application; 4 | User: { 5 | user_id?: number; 6 | user_email?: string; 7 | }; 8 | } 9 | 10 | // Variables provided by bin/esbuild 11 | const HONEYBADGER_API_KEY: string | undefined; 12 | const NODE_ENV: string | undefined; 13 | const GIT_REVISION: string | undefined; 14 | } 15 | 16 | import "@hotwired/turbo-rails"; 17 | import { Application } from "@hotwired/stimulus"; 18 | import controllers from "./controllers"; 19 | 20 | import Honeybadger from "@honeybadger-io/js/dist/browser/honeybadger"; 21 | 22 | Honeybadger.configure({ 23 | apiKey: HONEYBADGER_API_KEY, 24 | environment: NODE_ENV ?? "development", 25 | revision: GIT_REVISION, 26 | }); 27 | 28 | Honeybadger.setContext(window.User); 29 | 30 | import Alpine from "alpinejs"; 31 | import persist from "@alpinejs/persist"; 32 | import collapse from "@alpinejs/collapse"; 33 | 34 | const application = Application.start(); 35 | window.Stimulus = application; 36 | 37 | application.handleError = (error, message, detail) => { 38 | console.warn(message, detail); 39 | Honeybadger.notify(error); 40 | }; 41 | 42 | Alpine.plugin(persist); 43 | Alpine.plugin(collapse); 44 | Alpine.start(); 45 | 46 | // Init Stimulus controllers 47 | controllers(application); 48 | -------------------------------------------------------------------------------- /app/javascript/controllers/alert_controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | dismiss() { 5 | this.element.remove(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/copy_controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static values = { 5 | text: String, 6 | }; 7 | static targets = ["button"]; 8 | 9 | timeout: number | undefined; 10 | textValue: string; 11 | buttonTarget: HTMLElement; 12 | 13 | disconnect() { 14 | if (this.timeout !== undefined) { 15 | clearTimeout(this.timeout); 16 | this.timeout = undefined; 17 | } 18 | } 19 | 20 | copy() { 21 | navigator.clipboard.writeText(this.textValue); 22 | 23 | this.buttonTarget.innerText = "Copied!"; 24 | 25 | if (this.timeout === undefined) { 26 | this.timeout = window.setTimeout(() => { 27 | this.buttonTarget.innerText = "Copy"; 28 | this.timeout = undefined; 29 | }, 2000); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/javascript/controllers/form_confirm_controller.tsx: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static values = { 5 | title: String, 6 | text: String, 7 | actionText: { type: String, default: "Delete" }, 8 | }; 9 | 10 | titleValue: string; 11 | textValue: string; 12 | actionTextValue: string; 13 | 14 | modal: Element | null; 15 | submitting: boolean; 16 | 17 | initialize() { 18 | this.modal = null; 19 | this.submitting = false; 20 | } 21 | 22 | submit(e: SubmitEvent) { 23 | if (!this.submitting) { 24 | e.preventDefault(); 25 | 26 | this.showModal(); 27 | } else { 28 | this.submitting = false; 29 | } 30 | } 31 | 32 | private hideModal() { 33 | if (this.modal !== null && this.modal !== undefined) { 34 | this.modal.addEventListener("transitionend", () => { 35 | if (this.modal !== null && this.modal !== undefined) { 36 | this.modal.remove(); 37 | this.modal = null; 38 | } 39 | }); 40 | this.modal.classList.remove("modal--visible"); 41 | } 42 | } 43 | 44 | private showModal() { 45 | if (this.modal !== null) return; 46 | 47 | const modal: Element = ( 48 | 79 | ); 80 | 81 | document.body.appendChild(modal); 82 | 83 | setTimeout(() => modal.classList.add("modal--visible"), 10); 84 | 85 | this.modal = modal; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/javascript/controllers/form_controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | reset() { 5 | this.element.reset(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/hovercard_controller.ts: -------------------------------------------------------------------------------- 1 | import { Context, Controller } from "@hotwired/stimulus"; 2 | import { computePosition, flip, offset } from "@floating-ui/dom"; 3 | 4 | const cache: Record = {}; 5 | 6 | export default class extends Controller { 7 | card: HTMLElement | undefined; 8 | 9 | delayController: DelayController; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 12 | private url = this.element.getAttribute("href")!; 13 | 14 | constructor(ctx: Context) { 15 | super(ctx); 16 | 17 | this.delayController = new DelayController( 18 | 200, 19 | 100, 20 | async (on, content) => { 21 | if (on) { 22 | if (content !== null && content !== undefined) { 23 | await this.show(content); 24 | } 25 | } else { 26 | this.hide(); 27 | } 28 | }, 29 | async () => { 30 | let content: string | null; 31 | 32 | if (cache[this.url] !== undefined) { 33 | content = cache[this.url]; 34 | } else { 35 | content = await this.getUnfurlContent(this.url); 36 | cache[this.url] = content; 37 | } 38 | 39 | return content; 40 | } 41 | ); 42 | } 43 | 44 | hover() { 45 | this.delayController.on(); 46 | } 47 | 48 | remove() { 49 | this.delayController.off(); 50 | } 51 | 52 | private async show(content: string) { 53 | if (this.card !== undefined) return; 54 | 55 | const card = document.createElement("div"); 56 | card.style.position = "absolute"; 57 | card.style.zIndex = "50"; 58 | 59 | card.innerHTML = content; 60 | 61 | card.addEventListener("mouseenter", () => { 62 | this.delayController.on(); 63 | }); 64 | 65 | card.addEventListener("mouseleave", () => { 66 | this.delayController.off(); 67 | }); 68 | 69 | document.body.appendChild(card); 70 | 71 | const { x, y } = await computePosition(this.element, card, { 72 | placement: "top-start", 73 | middleware: [offset(10), flip()], 74 | }); 75 | 76 | card.style.left = `${x}px`; 77 | card.style.top = `${y}px`; 78 | 79 | this.card = card; 80 | } 81 | 82 | private async hide() { 83 | this.card?.remove(); 84 | this.card = undefined; 85 | } 86 | 87 | private async getUnfurlContent(url: string): Promise { 88 | const query = new URLSearchParams({ url }); 89 | 90 | const response = await fetch(`/hovercard/unfurl?${query}`); 91 | 92 | if (response.status === 200) { 93 | return await response.text(); 94 | } else { 95 | return null; 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * DelayController is a two-state controller ("on" and "off") that has a delay while switching between the two states. 102 | */ 103 | class DelayController { 104 | state: "on" | "transitioning-to-on" | "transitioning-to-off" | "off" = "off"; 105 | onTimeout: number | undefined; 106 | offTimeout: number | undefined; 107 | 108 | listener: (on: boolean, data?: T) => unknown; 109 | dataFetcher: (() => Promise) | undefined; 110 | 111 | onDelay: number; 112 | offDelay: number; 113 | 114 | constructor( 115 | onDelay: number, 116 | offDelay: number, 117 | listener: (on: boolean, data?: T) => Promise, 118 | dataFetcher?: () => Promise 119 | ) { 120 | this.onDelay = onDelay; 121 | this.offDelay = offDelay; 122 | 123 | this.listener = listener; 124 | this.dataFetcher = dataFetcher; 125 | } 126 | 127 | on() { 128 | switch (this.state) { 129 | case "transitioning-to-off": 130 | clearTimeout(this.offTimeout); 131 | this.state = "on"; 132 | break; 133 | case "off": 134 | this.onTimeout = window.setTimeout(async () => { 135 | this.state = "on"; 136 | this.onTimeout = undefined; 137 | 138 | if (this.dataFetcher) { 139 | const data = await this.dataFetcher(); 140 | 141 | if (this.state === "on") { 142 | this.listener(true, data); 143 | } 144 | } else { 145 | this.listener(true); 146 | } 147 | }, this.onDelay); 148 | this.state = "transitioning-to-on"; 149 | break; 150 | } 151 | } 152 | 153 | off() { 154 | switch (this.state) { 155 | case "transitioning-to-on": 156 | clearTimeout(this.onTimeout); 157 | this.state = "off"; 158 | break; 159 | case "on": 160 | this.offTimeout = window.setTimeout(() => { 161 | this.state = "off"; 162 | this.listener(false); 163 | this.offTimeout = undefined; 164 | }, this.offDelay); 165 | this.state = "transitioning-to-off"; 166 | break; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus"; 2 | 3 | import AlertController from "./alert_controller"; 4 | import CopyController from "./copy_controller"; 5 | import FormConfirmController from "./form_confirm_controller"; 6 | import HovercardController from "./hovercard_controller"; 7 | import FormController from "./form_controller"; 8 | 9 | export default (application: Application) => { 10 | application.register("alert", AlertController); 11 | application.register("copy", CopyController); 12 | application.register("form-confirm", FormConfirmController); 13 | application.register("hovercard", HovercardController); 14 | application.register("form", FormController); 15 | }; 16 | -------------------------------------------------------------------------------- /app/javascript/shim/jsx-shim.ts: -------------------------------------------------------------------------------- 1 | export function jsxCreateElement( 2 | tag: string, 3 | attrs: Record, 4 | ...children: string[] 5 | ): Element { 6 | const element = document.createElement(tag); 7 | 8 | for (const i in attrs) { 9 | if (typeof attrs[i] === "string") { 10 | element.setAttribute(i, attrs[i] as string); 11 | } else if (typeof attrs[i] === "function") { 12 | element.addEventListener( 13 | i.substring(2).toLowerCase(), 14 | attrs[i] as EventListenerOrEventListenerObject 15 | ); 16 | } 17 | } 18 | 19 | for (const child of children) { 20 | element.append(child); 21 | } 22 | 23 | return element; 24 | } 25 | -------------------------------------------------------------------------------- /app/javascript/types/jsx-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare function jsxCreateElement( 2 | tag: string, 3 | attrs: Record, 4 | ...children: string[] 5 | ): Element; 6 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /app/jobs/cache_link_unfurl_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This job populates the cache with a link unfurl (if necessary). 4 | class CacheLinkUnfurlJob < ApplicationJob 5 | queue_as :default 6 | 7 | def perform(url, force: false) 8 | unless force 9 | cached = $redis.get("unfurl:#{url}") 10 | 11 | # Only continue if A) the cache is empty or B) the cache has expired 12 | return unless cached.blank? || Time.parse(JSON.parse(cached)['expires_at']).past? 13 | end 14 | 15 | metadata = OpenGraphService.run(url) 16 | 17 | UnfurlService.cache(url, metadata) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserMailer < ApplicationMailer 4 | default from: 'Resolute ' 5 | 6 | def signup_verification 7 | @email = params[:email] 8 | @verification_code = params[:verification_code] 9 | 10 | mail(to: @email, subject: 'Verify your email address') 11 | end 12 | 13 | def password_reset 14 | @email = params[:email] 15 | @code = params[:code] 16 | 17 | mail(to: @email, subject: 'Reset your password') 18 | end 19 | 20 | def password_changed 21 | @email = params[:email] 22 | mail(to: @email, subject: 'Your password was changed') 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/api_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApiToken < ApplicationRecord 4 | belongs_to :oauth_app, optional: true 5 | belongs_to :user 6 | 7 | before_create :set_defaults 8 | 9 | scope :personal, -> { where(oauth_app: nil) } 10 | scope :not_personal, -> { where.not(oauth_app: nil) } 11 | 12 | def personal? 13 | oauth_app.nil? 14 | end 15 | 16 | private 17 | 18 | def set_defaults 19 | self.token = SecureRandom.hex if token.nil? 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/expirable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Expirable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | def expired? 8 | Time.now.utc > expires_at 9 | end 10 | 11 | def fulfill! 12 | update fulfilled: true 13 | end 14 | end 15 | 16 | class_methods do 17 | def expires_in(duration) 18 | after_initialize do 19 | self.expires_at ||= Time.now.utc + duration 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/oauth_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OauthApp < ApplicationRecord 4 | include Discard::Model 5 | 6 | belongs_to :user 7 | 8 | has_many :oauth_grants, dependent: :destroy 9 | has_many :api_tokens, dependent: :destroy 10 | has_many :reminders, dependent: :restrict_with_exception 11 | 12 | has_one_attached :icon 13 | 14 | scope :published, -> { where(public: true) } 15 | 16 | after_initialize :set_defaults 17 | 18 | before_discard do 19 | self.public = false 20 | end 21 | 22 | after_discard do 23 | api_tokens.destroy_all 24 | oauth_grants.destroy_all 25 | end 26 | 27 | def pretty_name 28 | "From #{name}" 29 | end 30 | 31 | private 32 | 33 | def set_defaults 34 | self.client_id = SecureRandom.hex if client_id.nil? 35 | self.client_secret = SecureRandom.hex if client_secret.nil? 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/oauth_grant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OauthGrant < ApplicationRecord 4 | include Expirable 5 | 6 | expires_in 15.minutes 7 | 8 | belongs_to :user 9 | belongs_to :oauth_app 10 | 11 | after_initialize :set_defaults 12 | 13 | private 14 | 15 | def set_defaults 16 | self.code = SecureRandom.hex if code.nil? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/password_reset_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PasswordResetRequest < ApplicationRecord 4 | include Expirable 5 | 6 | expires_in 15.minutes 7 | 8 | belongs_to :user 9 | after_initialize :set_defaults 10 | 11 | def set_defaults 12 | self.code = SecureRandom.hex if code.nil? 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/reminder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Reminder < ApplicationRecord 4 | include ActionView::Helpers::TagHelper 5 | 6 | belongs_to :user 7 | belongs_to :oauth_app, optional: true 8 | 9 | broadcasts_to ->(reminder) { [reminder.user, :reminders] }, inserts_by: :prepend 10 | 11 | after_save :cache_link_unfurls 12 | 13 | def to_html 14 | # rubocop:disable Rails/OutputSafety 15 | Rinku.auto_link( 16 | ERB::Util.html_escape(text), 17 | :urls, 18 | tag.attributes( 19 | target: '_blank', 20 | 'data-controller' => 'hovercard', 21 | 'data-action' => 'mouseenter->hovercard#hover mouseleave->hovercard#remove' 22 | ) 23 | ).html_safe 24 | # rubocop:enable Rails/OutputSafety 25 | end 26 | 27 | private 28 | 29 | def cache_link_unfurls 30 | URI.extract text, ['http', 'https'] do |url| 31 | CacheLinkUnfurlJob.perform_later url 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Session < ApplicationRecord 4 | belongs_to :user 5 | 6 | validates :token, presence: true, uniqueness: true 7 | 8 | after_initialize :set_defaults, unless: :persisted? 9 | 10 | def browser 11 | Browser.new(user_agent) 12 | end 13 | 14 | def touch! 15 | update last_active_at: Time.now 16 | end 17 | 18 | private 19 | 20 | def set_defaults 21 | self.last_active_at ||= Time.now 22 | self.token ||= SecureRandom.hex 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/signup_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SignupRequest < ApplicationRecord 4 | include Expirable 5 | 6 | expires_in 15.minutes 7 | 8 | validates :email, :code, presence: true 9 | validates :code, uniqueness: true 10 | 11 | after_initialize :set_defaults 12 | 13 | # Returns true if the user in question has already signed up 14 | def user_signed_up? 15 | User.exists?(email: email) 16 | end 17 | 18 | private 19 | 20 | def set_defaults 21 | self.code = SecureRandom.hex if code.nil? 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | has_secure_password 5 | 6 | validates :email, presence: true, uniqueness: true 7 | validates :password, presence: true 8 | 9 | has_many :sessions, dependent: :destroy 10 | has_many :reminders, dependent: :destroy 11 | has_many :oauth_apps, dependent: :restrict_with_exception 12 | has_many :api_tokens, dependent: :destroy 13 | has_many :password_reset_requests, dependent: :destroy 14 | end 15 | -------------------------------------------------------------------------------- /app/policies/oauth_app_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OauthAppPolicy 4 | attr_reader :user, :oauth_app 5 | 6 | def initialize(user, oauth_app) 7 | @user = user 8 | @oauth_app = oauth_app 9 | end 10 | 11 | def show? 12 | oauth_app.user == user 13 | end 14 | 15 | def destroy? 16 | oauth_app.user == user 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/policies/reminder_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ReminderPolicy 4 | attr_reader :user, :reminder 5 | 6 | def initialize(user, reminder) 7 | @user = user 8 | @reminder = reminder 9 | end 10 | 11 | def destroy? 12 | reminder.user == user 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/services/github_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class GithubService 4 | def initialize(token) 5 | @token = token 6 | end 7 | 8 | def emails 9 | get '/user/emails' 10 | end 11 | 12 | def primary_email 13 | primary = emails.find do |e| 14 | e['primary'] 15 | end 16 | 17 | primary['email'] 18 | end 19 | 20 | private 21 | 22 | def get(url) 23 | conn = Faraday.new('https://api.github.com') do |f| 24 | f.response :json 25 | end 26 | 27 | conn.get(url, nil, 'Authorization' => "Bearer #{@token}").body 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/services/oauth_service/github.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module OauthService 4 | class Github 5 | def initialize 6 | @client_id = Rails.application.credentials.github[:client_id] 7 | @client_secret = Rails.application.credentials.github[:client_secret] 8 | end 9 | 10 | def authorization_url(redirect_to) 11 | if redirect_to.blank? 12 | "https://github.com/login/oauth/authorize?scope=user:email&client_id=#{@client_id}" 13 | else 14 | # rubocop:disable Layout/LineLength 15 | "https://github.com/login/oauth/authorize?scope=user:email&client_id=#{@client_id}&state=#{Base64.urlsafe_encode64({ r: redirect_to }.to_json)}" 16 | # rubocop:enable Layout/LineLength 17 | end 18 | end 19 | 20 | def exchange_code(code) 21 | conn = Faraday.new do |f| 22 | f.response :json 23 | f.request :url_encoded 24 | end 25 | 26 | response = conn.post 'https://github.com/login/oauth/access_token', 27 | { client_id: @client_id, 28 | client_secret: @client_secret, 29 | code: code }, { 'Accept' => 'application/json' } 30 | 31 | response.body['access_token'] 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/services/open_graph_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This class acts as an Open Graph (https://ogp.me) client. 4 | class OpenGraphService 5 | class Result 6 | attr_reader :title, :description, :favicon 7 | 8 | def initialize(title: nil, description: nil, favicon: nil) 9 | @title = title 10 | @description = description 11 | @favicon = favicon 12 | end 13 | 14 | def blank? 15 | @title.blank? && @description.blank? 16 | end 17 | 18 | def to_hash 19 | { 20 | title: @title, 21 | description: @description, 22 | favicon: @favicon 23 | } 24 | end 25 | 26 | def self.from_hash(data) 27 | Result.new( 28 | title: data['title'], 29 | description: data['description'], 30 | favicon: data['favicon'] 31 | ) 32 | end 33 | 34 | def self.from_json(data) 35 | from_hash(JSON.parse(data)) 36 | end 37 | end 38 | 39 | # rubocop:disable Metrics/MethodLength 40 | def self.run(url) 41 | conn = Faraday.new request: { timeout: 10 } do |f| 42 | f.use FaradayMiddleware::FollowRedirects 43 | end 44 | 45 | response = conn.get(url) 46 | return Result.new if response.status < 200 || response.status >= 299 47 | 48 | doc = Nokogiri::HTML(response.body) 49 | 50 | # rubocop:disable Layout/LineLength 51 | title = doc.xpath("//meta[@property='og:title']").first&.attributes&.[]('content')&.value&.to_s || doc.xpath('//title').first&.children&.[](0)&.to_s 52 | # rubocop:enable Layout/LineLength 53 | 54 | description = doc.xpath("//meta[@property='og:description']").first&.attributes&.[]('content')&.value&.to_s 55 | 56 | favicon = doc.css('link[rel~="icon"]').first&.attributes&.[]('href')&.value&.to_s 57 | favicon = normalize_favicon(favicon, url) unless favicon.nil? 58 | 59 | Result.new( 60 | title: title, 61 | description: description, 62 | favicon: favicon 63 | ) 64 | rescue StandardError 65 | Result.new 66 | end 67 | # rubocop:enable Metrics/MethodLength 68 | 69 | def self.normalize_favicon(favicon, url) 70 | URI.join(url, favicon) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /app/services/unfurl_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This class is a wrapper around `OpenGraphService` that utilizes a cache. 4 | class UnfurlService 5 | def self.run(url) 6 | cached = $redis.get("unfurl:#{url}") 7 | 8 | if cached.blank? 9 | metadata = OpenGraphService.run(url) 10 | cache(url, metadata) 11 | 12 | return metadata 13 | end 14 | 15 | parsed = JSON.parse(cached) 16 | 17 | # Queue a background job to re-crawl the URL if the cache has expired 18 | CacheLinkUnfurlJob.perform_later(url, force: true) if Time.parse(parsed['expires_at']).past? 19 | 20 | OpenGraphService::Result.from_hash(parsed) 21 | end 22 | 23 | def self.cache(url, metadata) 24 | $redis.set( 25 | "unfurl:#{url}", 26 | metadata 27 | .to_hash 28 | .merge(expires_at: (Time.now.utc + 1800).to_s) 29 | .to_json 30 | ) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/views/api/oauth/authorize.html.haml: -------------------------------------------------------------------------------- 1 | .flex.flex-col.gap-5.items-center.justify-center.min-h-screen.w-screen.px-5 2 | .h-auto.bg-gray-800.border-t-4.border-green-400.rounded-md.p-7.text-white.my-10.w-full.max-w-sm 3 | = render "shared/alert" 4 | = render "shared/notice" 5 | 6 | - if @app.nil? 7 | %h2.text-2xl.mb-5.ml-1.font-normal.font-heading Something went wrong: 8 | .border-2.border-red-500.rounded-md.p-5 App not found. 9 | - elsif @app.redirect_uri.blank? 10 | %h2.text-2xl.mb-5.ml-1.font-normal.font-heading Something went wrong: 11 | .border-2.border-red-500.rounded-md.p-5 App "#{@app.name}" has no OAuth redirect URI configured. 12 | - if @app.user == @current_user 13 | .mt-4.p-5.border-2.border-green-500.rounded-md 14 | .text-gray-400.text-xs.mb-1 Only you can see this 15 | Fix this by adding a valid redirect URI in your app's settings. 16 | - elsif @invalid_response_type 17 | %h2.text-2xl.mb-5.ml-1.font-normal.font-heading Something went wrong: 18 | .border-2.border-red-500.rounded-md.p-5 Invalid response_type. 19 | - else 20 | .flex.align-center.gap-5.items-center.mb-6 21 | - if @app.icon.attached? 22 | = image_tag @app.icon, class: "h-12 w-auto rounded-md shadow-lg" 23 | %h2.text-2xl.font-normal.font-heading Connect #{@app.name} to your account? 24 | 25 | - if @scopes.length > 0 26 | .border-gray-600.border-2.mb-6.p-5.rounded-md 27 | .mb-2.font-bold #{@app.name} will be able to: 28 | 29 | %ul.fa-ul{style: "--fa-li-margin: 2rem"} 30 | - @scopes.map {|scope| scope_description(scope)}.each do |scope| 31 | %li 32 | %span.fa-li 33 | %i.fas.fa-square-check.text-green-400 34 | = scope 35 | 36 | 37 | - if @app.official 38 | .text-gray-400.text-sm 39 | %i.fa-solid.fa-fw.fa-shield.text-green-500 40 | %strong= @app.name 41 | is a trusted integration created by Resolute. 42 | - else 43 | .text-gray-400.text-sm 44 | %i.fa-solid.fa-fw.fa-info-circle 45 | %strong= @app.name 46 | hasn't been verified by Resolute. 47 | 48 | = form_with data: { turbo: false } do |form| 49 | = form.hidden_field :client_id, value: @app.client_id 50 | = form.hidden_field :scope, value: @scopes.to_json 51 | = form.hidden_field :state, value: @state unless @state.blank? 52 | = form.hidden_field :response_type, value: @response_type 53 | .flex.gap-3.mt-6 54 | = form.submit "Connect", class: "btn" 55 | = form.submit "Cancel", class: "btn bg-gray-700" 56 | 57 | - if @app.nil? || @app.redirect_uri.blank? 58 | .w-96.text-left.text-gray-500 59 | %a.text-gray-500(href="/home") ← Home 60 | -------------------------------------------------------------------------------- /app/views/api/reminders/_reminder.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.id reminder.id 4 | json.text reminder.text 5 | json.title reminder.title 6 | json.url reminder.url 7 | json.author reminder.author 8 | json.author_avatar reminder.author_avatar 9 | json.created_at reminder.created_at 10 | -------------------------------------------------------------------------------- /app/views/api/reminders/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.ignore_nil! 4 | 5 | json.array! @reminders, partial: 'api/reminders/reminder', as: :reminder 6 | -------------------------------------------------------------------------------- /app/views/api/reminders/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.ignore_nil! 4 | 5 | json.partial! 'api/reminders/reminder', reminder: @reminder 6 | -------------------------------------------------------------------------------- /app/views/api/users/me.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.id @user.id 4 | 5 | if @token.scope&.include?('user:email') || @token.oauth_app.nil? 6 | json.email @user.email 7 | else 8 | json.email nil 9 | end 10 | -------------------------------------------------------------------------------- /app/views/auth/forgot_password.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Reset your password" 2 | 3 | = render "layouts/sm", title: "Reset your password", back_href: "/login", back_text: "Log in" do 4 | = form_with do |form| 5 | = form.email_field :email, placeholder: "Email", class: "input mb-6", required: true, autofocus: true 6 | 7 | .flex.justify-between.items-center 8 | = form.submit "Continue", class: "btn" 9 | -------------------------------------------------------------------------------- /app/views/auth/forgot_password_submit.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Reset your password" 2 | 3 | = render "layouts/sm", title: "Reset your password", back_href: "/forgot-password", back_text: "Back" do 4 | Check your email 5 | %span.text-gray-400 (#{@email}) 6 | for a verification link. 🔗 7 | -------------------------------------------------------------------------------- /app/views/auth/forgot_password_verify.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Reset your password" 2 | 3 | = render "layouts/sm", title: "Reset your password" do 4 | = form_with do |form| 5 | = form.hidden_field :code, value: @code 6 | = form.email_field :email, disabled: true, placeholder: "Email", class: "input mb-4", required: true, value: @email 7 | = form.password_field :password, placeholder: "New password", class: "input mb-6", required: true, autofocus: true 8 | 9 | .flex.justify-between.items-center 10 | = form.submit "Reset", class: "btn" 11 | -------------------------------------------------------------------------------- /app/views/auth/login.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Log in" 2 | 3 | = render "layouts/sm", title: "Log in", back_href: "/", back_text: "Home" do 4 | = form_with do |form| 5 | = form.hidden_field(:redirect_to, value: @redirect_to) unless @redirect_to.blank? 6 | = form.email_field :email, placeholder: "Email", class: "input mb-4", autofocus: !@focus_password, required: true, value: @prefill_email 7 | = form.password_field :password, placeholder: "Password", class: "input mb-6", autofocus: @focus_password, required: true 8 | .flex.justify-between.items-center 9 | = form.submit "Log in", class: "btn" 10 | %a.text-sm(href="/forgot-password") Forgot password? 11 | 12 | %h3.text-gray-500.mt-10.mb-4.text-center Continue with 13 | 14 | .flex.gap-3 15 | %a.flex-1{href: github_authorization_url(@redirect_to)} 16 | %button.w-full.btn.bg-black GitHub 17 | -------------------------------------------------------------------------------- /app/views/authorizations/_authorization.html.haml: -------------------------------------------------------------------------------- 1 | .bg-black.p-5.rounded-md.flex.justify-between.items-center.gap-1 2 | .flex.flex-col.gap-1 3 | .flex.gap-2.items-center 4 | - if authorization.oauth_app.icon.attached? 5 | = image_tag authorization.oauth_app.icon, class: "h-5 w-auto rounded-md" 6 | 7 | = authorization.oauth_app.name 8 | .text-sm.text-gray-400 Last authorized #{time_ago_in_words authorization.last_authorized_at} ago 9 | = form_with url: authorization_path(authorization.oauth_app.id), method: :delete, data: { controller: "form-confirm", action: "form-confirm#submit", "form-confirm-title-value" => "Revoke #{authorization.oauth_app.name}'s access to your account?", "form-confirm-text-value" => "This can't be undone.", "form-confirm-action-text-value" => "Revoke" } do 10 | %button.btn.bg-gray-700.shrink-0.hover:bg-red-500.transition-colors(type="submit") Revoke 11 | -------------------------------------------------------------------------------- /app/views/errors/internal_server_error.html.haml: -------------------------------------------------------------------------------- 1 | .p-10.mt-20.text-center.border-t-4.bg-gray-800.border-red-500.rounded-md.w-fit.mx-auto 2 | %h1.text-8xl.font-mono.text-gray-300 500 3 | %h3.text-4xl.text-gray-300.font-light.mb-5 Something unexpected happened. 4 | 5 | %p.text-gray-300 We track these errors automatically and are working to resolve this. 6 | -------------------------------------------------------------------------------- /app/views/errors/not_found.html.haml: -------------------------------------------------------------------------------- 1 | .text-center.pt-40 2 | %h1.text-8xl.font-mono.text-gray-400 404 3 | %h3.text-4xl.text-gray-400.font-light Page not found -------------------------------------------------------------------------------- /app/views/hovercard/unfurl.html.haml: -------------------------------------------------------------------------------- 1 | .flex.items-center.gap-3.mb-3 2 | .bg-white.rounded-md.p-1.shrink-0 3 | %img.h-5.w-auto{src: @favicon} 4 | %a.text-xl.text-bold.text-green-500.whitespace-nowrap.overflow-hidden.text-ellipsis{href: @url, target: "_blank"}= @title 5 | %p.mb-2.flex.align-stretch.before:bg-gray-600.before:block.before:mr-3.before:rounded-full.before:shrink-0.cursor-default{class: "before:content-[''] before:w-1"}= @description 6 | %a.text-sm.text-gray-400.text-ellipsis.whitespace-nowrap.overflow-hidden{href: @url, target: "_blank"}= @url 7 | -------------------------------------------------------------------------------- /app/views/integrations/index.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.mx-auto 2 | .text-4xl.font-heading.mb-10.bg-green-600.rounded-md.text-center.py-20 Resolute works with the apps you already use. 3 | 4 | .grid.grid-cols-2.gap-5 5 | - @apps.each do |app| 6 | %a.text-white.p-5.bg-gray-800.rounded-md.flex.items-center.justify-between.transition-transform.hover:no-underline{href: app.installation_url, class: "hover:-translate-y-0.5", target: "_blank"} 7 | .flex.gap-4.items-center 8 | - if app.icon.attached? 9 | .relative 10 | = image_tag app.icon, class: "h-10 w-auto rounded-md" 11 | - if app.official 12 | .absolute.-bottom-2.-right-2.z-50.shadow-md.rounded-md.text-sm{class: "py-0.5 px-1 bg-gray-700/75", title: "This is a trusted integration created by Resolute."} 13 | %i.fa-solid.fa-shield.fa-fw 14 | 15 | .font-heading.font-heading.font-semibold.text-2xl 16 | = app.name 17 | - if @current_user.api_tokens.where(oauth_app: app).count > 0 18 | %button.btn.bg-transparent.ring-1.ring-green-600.ring-inset.text-green-500 Installed 19 | - else 20 | %button.btn Install 21 | -------------------------------------------------------------------------------- /app/views/landing/_message.html.haml: -------------------------------------------------------------------------------- 1 | - if selected 2 | .flex.gap-3.items-start.relative.rounded-md 3 | %img.h-11.rounded-md(src="https://cloud-5txrrpkms-hack-club-bot.vercel.app/0avataaars.png") 4 | .flex.flex-col.gap-1 5 | .flex.gap-2.items-center(class="-mt-1") 6 | .font-bold.text-sm= user 7 | .text-gray-500.text-xs= Time.now.strftime "%l:%M %p" 8 | %div= text 9 | .absolute.top-0.right-1{"class" => "-mt-5", "x-data" => "{ open: false }"} 10 | .absolute{class: "-top-1 -left-1"} 11 | .w-3.h-3.bg-blue-500.absolute.top-0.left-0.rounded-full 12 | .w-3.h-3.bg-blue-500.absolute.top-0.left-0.rounded-full.animate-ping 13 | .bg-gray-800.px-2.rounded-md.border.border-gray-700.cursor-pointer.hover:bg-gray-700{"class" => "py-1.5", "@click" => "open = true"} 14 | %i.fa-solid.fa-ellipsis-v.fa-fw 15 | %ul.absolute.top-full.right-0.bg-gray-800.rounded-md.p-2.w-52.mt-2.border.border-gray-700.z-10.shadow-lg{"x-show" => "open", "x-cloak" => true, "@click.outside" => "open = false"} 16 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.cursor-not-allowed.text-gray-500 Mark unread 17 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.cursor-not-allowed.text-gray-500 Copy link 18 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.cursor-pointer.flex.items-center.gap-2{"@click" => "reminders.push({text: \"#{text}\", id: Math.random()}); open = false"} 19 | = image_tag "icon-square-128.png", size: "25x25", alt: "Resolute logo", class: "rounded-md" 20 | %span Add to Resolute 21 | - else 22 | .flex.gap-3.items-start.relative.rounded-md.group{"x-data" => "{ open: false }"} 23 | %img.h-11.rounded-md.group-hover:opacity-100.transition-opacity{"src" => "https://cloud-5txrrpkms-hack-club-bot.vercel.app/0avataaars.png", ":class" => "open ? 'opacity-100' : 'opacity-50'"} 24 | .flex.flex-col.gap-1.group-hover:opacity-100.transition-opacity{":class" => "open ? 'opacity-100' : 'opacity-50'"} 25 | .flex.gap-2.items-center(class="-mt-1") 26 | .font-bold.text-sm= user 27 | .text-gray-500.text-xs= Time.now.strftime "%l:%M %p" 28 | %div= text 29 | .absolute.top-0.right-1.group-hover:opacity-100.transition-opacity{"class" => "-mt-5", ":class" => "open ? 'opacity-100' : 'opacity-0'", "x-cloak" => true} 30 | .bg-gray-800.px-2.rounded-md.border.border-gray-700.cursor-pointer.hover:bg-gray-700{"class" => "py-1.5", "@click" => "open = true"} 31 | %i.fa-solid.fa-ellipsis-v.fa-fw 32 | %ul.absolute.top-full.right-0.bg-gray-800.rounded-md.p-2.w-52.mt-2.border.border-gray-700.z-10.shadow-lg{"x-show" => "open", "x-cloak" => true, "@click.outside" => "open = false"} 33 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.cursor-not-allowed.text-gray-500 Mark unread 34 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.cursor-not-allowed.text-gray-500 Copy link 35 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.cursor-pointer.flex.items-center.gap-2{"@click" => "reminders.push({text: \"#{text}\", id: Math.random()}); open = false"} 36 | = image_tag "icon-square-128.png", size: "25x25", alt: "Resolute logo", class: "rounded-md" 37 | %span Add to Resolute 38 | -------------------------------------------------------------------------------- /app/views/landing/_reminder.html.haml: -------------------------------------------------------------------------------- 1 | .group.bg-black.p-5.rounded-md.flex.justify-between.items-center.gap-5.border-2.border-transparent.relative.break-words.cursor-default 2 | .flex.flex-col.gap-3.min-w-0.grow 3 | .flex.gap-3.items-center.text-sm.text-gray-300 4 | %img.w-5.rounded-full{src: author_avatar} 5 | %span= author 6 | 7 | .break-words.min-w-0.text-white.hover:no-underline{"x-text" => "reminder.text"} 8 | 9 | .text-xs.text-gray-400 from #{app_name} 10 | 11 | %button.text-gray-500.hover:text-red-500.cursor-pointer.opacity-100.sm:opacity-0.group-hover:opacity-100.transition{"type" => "submit", "title" => "Delete", "@click" => "reminders = reminders.filter(r => r.id != reminder.id)"} 12 | %i.fa-solid.fa-fw.fa-trash.text-xl 13 | -------------------------------------------------------------------------------- /app/views/landing/index.html.haml: -------------------------------------------------------------------------------- 1 | = render "shared/toast" 2 | 3 | .bg-gray-800.flex.items-center.justify-center.px-10.py-28.sm:py-36.lg:py-40 4 | %h1.lg:text-6xl.text-5xl.font-heading.max-w-xl.text-center 5 | Forgetting to reply is a thing of the past. 6 | 7 | .px-10.mt-10 8 | .mx-auto.w-full.max-w-5xl 9 | .text-3xl.lg:text-4xl.font-heading.mb-10.text-center.border-2.border-gray-800.py-5.px-5.rounded-md 10 | .mb-2 Hey, look! You've just received a message. 11 | .text-xl.lg:text-2xl Can't reply right now? Add it to Resolute. 12 | .flex-col.md:flex-row.flex.gap-4{"x-data" => "{ reminders: $persist([]) }"} 13 | .flex-col.grow.basis-0 14 | .bg-gray-800.rounded-t-md.px-2.py-1.text-sm.flex.items-center.gap-1 15 | .h-3.w-3.bg-red-400.rounded-full 16 | .h-3.w-3.bg-yellow-400.rounded-full 17 | .h-3.w-3.bg-green-400.rounded-full 18 | .ml-3 Messaging App 9000 19 | .border-2.border-gray-800.p-7.rounded-b-md.flex.flex-col.gap-4 20 | = render "message", selected: false, text: "🏴‍☠️ Ahoy, matey!", user: "buccaneer123" 21 | .w-full.bg-red-500.rounded-full.h-px 22 | = render "message", selected: true, text: "Mind helpin' me with a little computer trouble? The printer not be printin' me treasure map ☠️", user: "buccaneer123" 23 | .flex-col.grow.basis-0 24 | .bg-gray-800.rounded-t-md.px-2.py-1.text-sm.flex.items-center.gap-1 25 | .h-3.w-3.bg-red-400.rounded-full 26 | .h-3.w-3.bg-yellow-400.rounded-full 27 | .h-3.w-3.bg-green-400.rounded-full 28 | .ml-3 Resolute 29 | .p-7.bg-gray-800.rounded-b-md.flex.flex-col.gap-5 30 | %template{"x-if" => "reminders.length == 0"} 31 | .text-gray-500.text-center.mb-4.mt-1 Nothing here, yet... 32 | %template{"x-for" => "reminder in reminders"} 33 | = render "reminder", author: "buccaneer123", author_avatar: "https://cloud-5txrrpkms-hack-club-bot.vercel.app/0avataaars.png", app_name: "Messaging App 9000" 34 | 35 | .px-10.py-12.mt-10.bg-gray-800 36 | .w-full.max-w-4xl.mx-auto.flex.justify-between.items-center.flex-col.md:flex-row.gap-5 37 | %h1.font-heading.font-bold.text-3xl.xs:text-4xl.text-center.md:text-left 38 | Available now for Slack. 39 | %br/ 40 | .text-gray-500.text-2xl.xs:text-3xl More apps coming soon! 41 | %a.btn.text-lg.xs:text-xl.py-3.px-6{href: "/signup"} Sign up 42 | 43 | .px-10.py-5.text-gray-500.text-center 44 | .border-r.border-r-gray-700.inline-block.pr-5.mr-3 45 | %a.text-inherit{href: "https://status.useresolute.com", target: "_blank"} Status 46 | · 47 | %a.text-inherit{href: "https://api.useresolute.com", target: "_blank"} API 48 | %a.hover:no-underline{href: "https://twitter.com/useresolute", target: "_blank"} 49 | %i.fab.fa-twitter.fa-fw 50 | %a.hover:no-underline{href: "https://github.com/resoluteapp", target: "_blank"} 51 | %i.fab.fa-github.fa-fw -------------------------------------------------------------------------------- /app/views/layouts/_sm.html.haml: -------------------------------------------------------------------------------- 1 | .px-5 2 | .flex.flex-col.gap-5.items-stretch.justify-center.w-full.max-w-sm.mx-auto.mt-20.mb-10 3 | .h-auto.bg-gray-800.border-t-4.rounded-md.p-7.text-white{class: alert.present? ? "border-red-500" : "border-green-400"} 4 | = render "shared/alert" 5 | = render "shared/notice" 6 | 7 | %h2.text-2xl.mb-6.ml-1.font-heading= title 8 | 9 | = yield 10 | 11 | - if local_assigns[:back_href].present? 12 | .text-left.text-gray-500 13 | %a.text-gray-500{href: back_href} ← #{local_assigns.fetch :back_text, "Back"} 14 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html(lang="en") 3 | %head 4 | %title= content_for(:page_title) || "Resolute" 5 | %meta(name="viewport" content="width=device-width,initial-scale=1")/ 6 | %meta(name="turbo-cache-control" content="no-preview")/ 7 | = csrf_meta_tags 8 | = csp_meta_tag 9 | 10 | %link(rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png") 11 | %link(rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png") 12 | %link(rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png") 13 | %link(rel="manifest" href="/site.webmanifest") 14 | 15 | :javascript 16 | window.User = { user_id: #{@current_user&.id.to_json.html_safe}, user_email: #{@current_user&.email.to_json.html_safe} } 17 | 18 | = stylesheet_link_tag 'fonts', media: 'all' 19 | = stylesheet_link_tag 'fontawesome/css/all.min.css', media: 'all' 20 | = stylesheet_link_tag 'application', media: 'all' 21 | = javascript_include_tag 'application', defer: true 22 | 23 | = javascript_include_tag 'https://analytics.useresolute.com/umami.js', 24 | async: true, 25 | defer: true, 26 | "data-website-id" => "be12769c-47ed-4f1b-8c03-f20cc06e4ec2", 27 | "data-domains" => "useresolute.com" if Rails.env.production? 28 | 29 | %body 30 | - unless @hide_header 31 | - if @current_user.nil? 32 | = render "shared/landing_header" 33 | - else 34 | = render "shared/header", selected: @header_selected 35 | 36 | = yield 37 | -------------------------------------------------------------------------------- /app/views/layouts/hovercard.html.haml: -------------------------------------------------------------------------------- 1 | .bg-gray-800.rounded-md.p-5.border.border-gray-700.max-w-md.shadow-xl= yield 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | %html 3 | %head 4 | %meta(http-equiv="Content-Type" content="text/html; charset=utf-8")/ 5 | :css 6 | body { 7 | font-family: "sans-serif"; 8 | } 9 | 10 | %body= yield -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.haml: -------------------------------------------------------------------------------- 1 | = yield 2 | -------------------------------------------------------------------------------- /app/views/oauth_apps/_layout.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md 2 | .flex.justify-between.items-center.mb-10 3 | %h1.text-xl.font-heading 4 | %span.text-gray-500 5 | %a.text-gray-500(href="/developer") Settings 6 | \/ 7 | %a.text-gray-500(href="/developer/apps") Apps 8 | \/ 9 | = @app.name 10 | -# - if @app.public 11 | -# %a.btn{href: integration_path(@app), target: "_blank"} Show public app page 12 | 13 | .flex.gap-7 14 | .w-64 15 | %ul.flex.flex-col 16 | = sidebar_link(title: "OAuth", href: oauth_app_path(@app), selected: selected == "credentials") 17 | = sidebar_link(title: "Settings", href: edit_oauth_app_path(@app), selected: selected == "settings") 18 | = sidebar_link(title: "Advanced", href: advanced_oauth_app_path(@app), selected: selected == "advanced") 19 | .grow 20 | = render "shared/alert" 21 | = render "shared/notice" 22 | 23 | = yield -------------------------------------------------------------------------------- /app/views/oauth_apps/advanced.html.haml: -------------------------------------------------------------------------------- 1 | = render "layout", selected: "advanced" do 2 | .border-2.border-red-500.rounded-md.p-5.flex.justify-between.items-center 3 | .flex.flex-col 4 | .text-lg Delete this app 5 | .text-sm.text-gray-400 This can't be undone. 6 | = form_with url: oauth_app_path(@app), method: :delete, data: { controller: "form-confirm", action: "form-confirm#submit", "form-confirm-title-value" => "Delete this app?", "form-confirm-text-value" => "This will permanently uninstall #{@app.name} for all users, and prevent them from installing it in the future." } do |form| 7 | %button.btn.bg-red-500(type="submit") Delete app 8 | -------------------------------------------------------------------------------- /app/views/oauth_apps/edit.html.haml: -------------------------------------------------------------------------------- 1 | = render "layout", selected: "settings" do 2 | = form_with model: @app do |form| 3 | = form.label :name, "App name", class: "block mb-2 text-sm text-gray-400" 4 | = form.text_field :name, class: "input mb-4", required: true, placeholder: "My Cool App", autofocus: true 5 | - if @app.public 6 | %label.block.mb-2.text-sm.text-gray-400(for="oauth_app_installation_url") Installation URL 7 | = form.text_field :installation_url, class: "input mb-4", placeholder: "https://my-cool-app.com/install" 8 | = form.label :icon, "Icon", class: "block mb-2 text-sm text-gray-400" 9 | = form.file_field :icon, accept: "image/*", class: "block mb-4 text-gray-400 file:cursor-pointer file:text-white file:rounded-md file:py-2 file:px-4 file:shadow-md file:bg-green-600 file:border-0 file:mr-4" 10 | - if @app.icon.attached? 11 | %div 12 | .relative.group.inline-block 13 | -# %button.absolute.top-1.right-1.py-1.rounded-md.cursor-pointer.transition-opacity.opacity-0.group-hover:opacity-100(class="bg-black/40 px-1.5") 14 | -# %i.fa-solid.fa-times.fa-fw 15 | = image_tag @app.icon, class: "h-40 w-auto mb-6 rounded-lg" 16 | 17 | = form.submit "Save", class: "btn" 18 | -------------------------------------------------------------------------------- /app/views/oauth_apps/index.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md 2 | .flex.justify-between.items-start.mb-10 3 | %h1.text-xl.font-heading 4 | %span.text-gray-500 5 | %a.text-gray-500(href="/developer") Settings 6 | \/ 7 | Apps 8 | %a.btn{href: new_oauth_app_path} New 9 | 10 | = render "shared/alert" 11 | = render "shared/notice" 12 | 13 | - if @apps.length == 0 14 | %h2.text-center.mb-10.mt-15.text-gray-500 Nothing here, yet... 15 | - else 16 | .grid.grid-cols-2.gap-5 17 | - @apps.each do |app| 18 | %a.text-white.text-xl.p-5.bg-black.font-heading.font-semibold.rounded-md.text-white.transition-transform.hover:no-underline{class: "hover:-translate-y-0.5", href: oauth_app_path(app)} 19 | - if app.public 20 | %i.fa-solid.fa-globe.text-green-500.inline-block.mr-1(title="This app is listed publicly.") 21 | - if app.official 22 | %i.fa-solid.fa-shield.text-green-500.inline-block.mr-1(title="Wahoo! This is an officially developed integration.") 23 | = app.name -------------------------------------------------------------------------------- /app/views/oauth_apps/new.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md 2 | %h1.text-xl.mb-10.font-heading 3 | %span.text-gray-500 4 | %a.text-gray-500(href="/developer") Settings 5 | \/ 6 | %a.text-gray-500(href="/developer/apps") Apps 7 | \/ 8 | New 9 | 10 | = form_with model: OauthApp.new, class: "block mx-auto" do |form| 11 | = form.text_field :name, class: "input mb-4", placeholder: "App name", autofocus: true, required: true 12 | = form.submit "Create", class: "btn" -------------------------------------------------------------------------------- /app/views/oauth_apps/show.html.haml: -------------------------------------------------------------------------------- 1 | = render "layout", selected: "credentials" do 2 | .mb-5 3 | .text-sm.text-gray-400 Client ID 4 | .flex.items-center.gap-3{"data-controller" => "copy", "data-copy-text-value" => @app.client_id} 5 | .text-lg.font-mono.select-all= @app.client_id 6 | %button.btn.text-sm.bg-gray-700(data-action="copy#copy" data-copy-target="button") Copy 7 | 8 | .mb-5 9 | .text-sm.text-gray-400 Client Secret 10 | .flex.items-center.gap-3{"data-controller" => "copy", "data-copy-text-value" => @app.client_secret} 11 | .text-lg.font-mono.select-all= @app.client_secret 12 | %button.btn.text-sm.bg-gray-700(data-action="copy#copy" data-copy-target="button") Copy 13 | 14 | = form_with model: @app, class: "mb-8" do |form| 15 | = form.label :redirect_uri, "Redirect URI", class: "block mb-2 text-sm text-gray-400" 16 | .flex.gap-3.items-center 17 | = form.url_field :redirect_uri, class: "input", placeholder: "https://my-cool-app.com/oauth/callback", autofocus: true 18 | 19 | = form.submit "Save", class: "btn" 20 | 21 | .text-sm.text-gray-400.mb-1 Installed by #{pluralize(@authorization_count, 'user')} 22 | .text-sm.text-gray-400 Created #{time_ago_in_words @app.created_at} ago 23 | -------------------------------------------------------------------------------- /app/views/personal_tokens/index.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md 2 | .flex.justify-between.items-start.mb-10 3 | %h1.text-xl.font-heading 4 | %span.text-gray-500 5 | %a.text-gray-500(href="/developer") Settings 6 | \/ 7 | Personal tokens 8 | %a.btn{href: new_personal_token_path} New 9 | 10 | = render "shared/alert" 11 | = render "shared/notice" 12 | 13 | - if flash[:personal_token] 14 | .border.border-green-500.bg-green-800.p-5.rounded-md.mb-10.flex.items-start{"data-controller" => "copy alert", "data-copy-text-value" => flash[:personal_token]["token"]} 15 | .flex-grow.flex.flex-col.items-start 16 | .bg-green-600.rounded-md.py-1.px-2= flash[:personal_token]["description"] 17 | .flex.items-center.gap-3 18 | .font-mono.text-lg.select-all= flash[:personal_token]["token"] 19 | %button.btn.text-sm(data-action="copy#copy" data-copy-target="button") Copy 20 | .opacity-50.mt-2.text-sm Remember to save this token, because you won't see it again. 21 | %button{"data-action" => "alert#dismiss"} 22 | %i.fa-solid.fa-times 23 | 24 | - if @tokens.length == 0 25 | %h2.text-center.mb-10.mt-15.text-gray-500 Nothing here, yet... 26 | - else 27 | .grid.grid-cols-2.gap-5 28 | - @tokens.each do |token| 29 | %li.bg-black.p-5.rounded-md.flex.justify-between.items-center.gap-1 30 | .flex.flex-col.gap-1 31 | = token.description 32 | .text-sm.text-gray-400 Created #{time_ago_in_words token.created_at} ago 33 | 34 | = form_with url: personal_token_path(token), method: :delete, data: { controller: "form-confirm", action: "form-confirm#submit", "form-confirm-title-value" => "Revoke this token?", "form-confirm-text-value" => "Any scripts or applications using this token will stop working.", "form-confirm-action-text-value" => "Revoke" } do 35 | %button.btn.bg-gray-700.shrink-0.hover:bg-red-500.transition-colors(type="submit") Revoke 36 | -------------------------------------------------------------------------------- /app/views/personal_tokens/new.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md 2 | %h1.text-xl.mb-10.font-heading 3 | %span.text-gray-500 4 | %a.text-gray-500(href="/developer") Settings 5 | \/ 6 | %a.text-gray-500{href: personal_tokens_path} Personal tokens 7 | \/ 8 | New 9 | 10 | = form_with url: personal_tokens_path, class: "block mx-auto" do |form| 11 | = form.text_field :description, class: "input mb-4", placeholder: "Description", autofocus: true, required: true 12 | = form.submit "Create", class: "btn" -------------------------------------------------------------------------------- /app/views/reminders/_reminder.html.haml: -------------------------------------------------------------------------------- 1 | %div{id: dom_id(reminder)} 2 | .group.bg-black.p-5.rounded-md.flex.justify-between.items-center.gap-5.border-2.border-transparent.relative.break-words.cursor-default.has-overlay-link{class: reminder.url.present? && "transition hover:-translate-y-0.5 hover:shadow-lg"} 3 | - if reminder.url.present? 4 | %a.absolute.inset-0.z-0.overlay-link{target: "_blank", href: reminder.url} 5 | .flex.flex-col.gap-3.min-w-0.grow 6 | - if reminder.title.present? 7 | %h2.font-semibold.font-heading.break-words.min-w-0.text-xl.text-green-500.group-2-hover:underline{class: "-mb-0.5"}= reminder.title 8 | 9 | - if reminder.author.present? 10 | .flex.gap-3.items-center.text-sm.text-gray-300 11 | - if reminder.author_avatar.present? 12 | %img.w-5.rounded-full{src: reminder.author_avatar} 13 | %span= reminder.author 14 | 15 | - if reminder.text.present? 16 | - if reminder.title.present? 17 | -# Add a li'l left border if there's a title 18 | .break-words.min-w-0.text-white.hover:no-underline.border-l-2.border-gray-500.pl-3= reminder.to_html 19 | - else 20 | .break-words.min-w-0.text-white.hover:no-underline= reminder.to_html 21 | 22 | - if reminder.oauth_app.present? || reminder.source.present? 23 | .text-xs.text-gray-400= [reminder.oauth_app&.pretty_name, reminder.source].compact.join(" · ") 24 | 25 | 26 | = form_with url: reminder_path(reminder), method: :delete, class: "shrink-0" do |form| 27 | %button.text-gray-500.hover:text-red-500.cursor-pointer.opacity-100.sm:opacity-0.group-hover:opacity-100.translate-x-0.group-hover:translate-x-0.transition{type: "submit", title: "Delete", class: reminder.url.blank? ? "sm:translate-x-2" : "sm:translate-x-4"} 28 | %i.fa-solid.fa-fw.fa-trash.text-xl 29 | -------------------------------------------------------------------------------- /app/views/reminders/create.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.prepend "reminders" do 2 | = render @reminder 3 | -------------------------------------------------------------------------------- /app/views/reminders/destroy.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.remove @reminder -------------------------------------------------------------------------------- /app/views/reminders/index.html.haml: -------------------------------------------------------------------------------- 1 | = render "shared/toast" 2 | 3 | .flex.flex-col.items-stretch.h-screen 4 | = turbo_stream_from @current_user, :reminders 5 | 6 | .px-0.sm:px-10.grow.sm:grow-0 7 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md.rounded-b-none.sm:rounded-b-md.h-full 8 | = form_with url: "/reminders", data: { controller: "form", action: "turbo:submit-end->form#reset" } do |form| 9 | .flex.flex-col.mb-10.items-stretch.gap-2 10 | .flex.gap-4.items-stretch 11 | %input.input.block(type="text" placeholder="Remind me about..." name="text" autofocus required) 12 | %button.btn.hidden.xs:inline-block(type="submit") Add 13 | 14 | .text-sm.text-gray-400.pl-1 15 | Or, 16 | %a(href="/integrations") find an integration 17 | 18 | -# - if @reminders.length == 0 19 | -# %h2.text-center.mb-10.mt-15.text-gray-500 Nothing here, yet... 20 | -# - else 21 | .grid.grid-cols-1.md:grid-cols-2.gap-5#reminders 22 | - @reminders.each do |reminder| 23 | = render reminder 24 | -------------------------------------------------------------------------------- /app/views/sessions/_session.html.haml: -------------------------------------------------------------------------------- 1 | .bg-black.p-5.rounded-md.flex.justify-between.items-center.gap-1 2 | .flex.flex-col.gap-1 3 | %div 4 | %i.inline-block.text-gray-500.mr-1.fab{class: ["fa-#{browser_to_icon(session.browser)}", session == @current_session && "text-green-500"]} 5 | %span #{session.browser.name} on #{session.browser.platform.name} 6 | %span.font-mono.text-gray-400.inline-block.ml-2= session.ip 7 | .text-sm.text-gray-400 8 | - if session == @current_session 9 | Active now · current session 10 | - else 11 | Active #{time_ago_in_words(session.last_active_at || session.created_at)} ago 12 | 13 | - unless session.login_method.blank? 14 | · via #{session.login_method} 15 | 16 | = form_with url: session_path(session), method: :delete, class: "shrink-0" do 17 | %button.btn.bg-gray-700.shrink-0.hover:bg-red-500.transition-colors(type="submit") Log out -------------------------------------------------------------------------------- /app/views/settings/_layout.html.haml: -------------------------------------------------------------------------------- 1 | .w-full.max-w-4xl.block.mx-auto.bg-gray-800.p-7.rounded-md 2 | %h1.text-xl.mb-10.font-heading Settings 3 | 4 | .flex.gap-7 5 | .w-64 6 | %ul.flex.flex-col.mb-5 7 | = sidebar_link(title: "General", href: "/settings", selected: selected == "general") 8 | = sidebar_link(title: "Security", href: "/settings/security", selected: selected == "security") 9 | 10 | %ul.flex.flex-col 11 | = sidebar_link(title: "Developer", href: "/developer", selected: selected == "developer") 12 | .grow 13 | = render "shared/alert" 14 | = render "shared/notice" 15 | 16 | = yield -------------------------------------------------------------------------------- /app/views/settings/developer.html.haml: -------------------------------------------------------------------------------- 1 | = render "layout", selected: "developer" do 2 | .grid.grid-cols-2.gap-5 3 | %a.bg-blue-600.text-xl.p-5.font-semibold.font-heading.rounded-md.text-white.transition-transform.hover:no-underline{class: "hover:-translate-y-0.5", href: oauth_apps_path} 4 | %i.fa-solid.fa-code.mr-2.fa-fw 5 | Your apps 6 | %span.text-sm.bg-blue-500.font-normal.font-sans.px-2.rounded-md.ml-1(class="py-0.5")= @app_count 7 | %a.bg-green-600.text-xl.p-5.font-semibold.font-heading.rounded-md.text-white.transition-transform.hover:no-underline{class: "hover:-translate-y-0.5", href: personal_tokens_path} 8 | %i.fa-solid.fa-key.mr-2.fa-fw 9 | Personal tokens 10 | %span.text-sm.bg-green-500.font-normal.font-sans.px-2.rounded-md.ml-1(class="py-0.5")= @personal_token_count 11 | %a.bg-yellow-600.text-xl.p-5.font-semibold.font-heading.rounded-md.text-white.transition-transform.hover:no-underline(class="hover:-translate-y-0.5" href="https://api.useresolute.com" data-turbo="false") 12 | %i.fa-solid.fa-book.mr-2.fa-fw 13 | Documentation 14 | %a.bg-rose-500.text-xl.p-5.font-semibold.font-heading.rounded-md.text-white.transition-transform.hover:no-underline(class="hover:-translate-y-0.5" href="https://github.com/resoluteapp/resolute/discussions" target="_blank" data-turbo="false") 15 | %i.fa-solid.fa-users.mr-2.fa-fw 16 | Community 17 | 18 | .text-gray-400.mt-5.text-sm 19 | Psssst... did you know that Resolute is open-source? 20 | -------------------------------------------------------------------------------- /app/views/settings/index.html.haml: -------------------------------------------------------------------------------- 1 | = render "layout", selected: "general" do 2 | .flex.items-center.gap-5.mb-10 3 | = gravatar @current_user.email, 100 4 | %div 5 | .text-xl= @current_user.email 6 | %em.text-sm Manage your avatar at Gravatar. 7 | 8 | %div{"x-data" => "{ open: #{flash[:change_password_open] ? 'true' : 'false'} }", "x-effect" => "open && setTimeout(() => $refs.currentPassword.focus(), 250)"} 9 | %button.btn.bg-gray-700{"@click" => "open = !open"} 10 | %i.fa-solid.mr-1.fa-fw{":class" => "open ? 'fa-lock-open' : 'fa-lock'"} 11 | Change your password 12 | 13 | %div{"x-show" => "open", "x-collapse" => true, "x-cloak" => true} 14 | = form_with url: "/settings", class: "mt-5" do |form| 15 | = form.password_field :current_password, required: true, class: "input mb-4", placeholder: "Current password", "x-ref" => "currentPassword", autofocus: flash[:change_password_open] 16 | = form.password_field :new_password, required: true, class: "input mb-4", placeholder: "New password" 17 | 18 | .flex.items-center 19 | = form.submit "Save", class: "btn mr-5" 20 | .text-gray-400.text-sm This won't log anyone out; visit the Security page to do that. 21 | -------------------------------------------------------------------------------- /app/views/settings/security.html.haml: -------------------------------------------------------------------------------- 1 | = render "layout", selected: "security" do 2 | .mb-5.flex.justify-between.items-center 3 | %h2.text-lg.text-gray-400 4 | Sessions 5 | .inline-block.text-sm.bg-gray-700.font-normal.font-sans.px-2.rounded-md(class="py-0.5")= @sessions.length 6 | = form_with url: destroy_all_sessions_path, data: { controller: "form-confirm", action: "form-confirm#submit", "form-confirm-title-value" => "Log out everywhere?", "form-confirm-text-value" => "This will log you out of every device except for your current one.", "form-confirm-action-text-value" => "Log out" } do 7 | %button.btn.bg-red-500{type: "submit", disabled: @sessions.length <= 1} Log out everywhere else 8 | 9 | .flex.flex-col.gap-3.mb-10{"x-data" => "{ unfurled: false }"} 10 | - @sessions[0, 2].each do |session| 11 | = render session 12 | - if @sessions.length > 2 13 | %div.flex.flex-col.gap-3{"x-show" => "unfurled", "x-cloak" => true} 14 | - @sessions[2..].each do |session| 15 | = render session 16 | %div 17 | .inline-block.link{"@click" => "unfurled = !unfurled", "x-text" => "unfurled ? 'See fewer' : 'See all'"} See all 18 | 19 | %h2.text-lg.text-gray-400.mb-5 20 | Authorized apps 21 | - if @authorizations.length > 0 22 | .inline-block.text-sm.bg-gray-700.font-normal.font-sans.px-2.rounded-md(class="py-0.5")= @authorizations.length 23 | 24 | - if @authorizations.length == 0 25 | %h2.text-center.mb-8.text-gray-500 You haven't connected any apps to your account. 26 | - else 27 | .flex.flex-col.gap-3{"x-data" => "{ unfurled: false }"} 28 | - @authorizations[0, 2].each do |authorization| 29 | = render "authorizations/authorization", authorization: authorization 30 | - if @authorizations.length > 2 31 | %div.flex.flex-col.gap-3{"x-show" => "unfurled", "x-cloak" => true} 32 | - @authorizations[2..].each do |authorization| 33 | = render "authorizations/authorization", authorization: authorization 34 | %div 35 | .inline-block.link{"@click" => "unfurled = !unfurled", "x-text" => "unfurled ? 'See fewer' : 'See all'"} See all 36 | -------------------------------------------------------------------------------- /app/views/shared/_alert.html.haml: -------------------------------------------------------------------------------- 1 | - if alert 2 | %div.alert.mb-6(data-controller="alert") 3 | = alert 4 | %button.bg-transparent(data-action="alert#dismiss") 5 | %i.fa-solid.fa-times -------------------------------------------------------------------------------- /app/views/shared/_header.html.haml: -------------------------------------------------------------------------------- 1 | .flex.py-5.px-5.sm:px-10.justify-between.bg-black.mb-5.z-40.sticky.top-0 2 | -# Desktop navbar 3 | .hidden.sm:flex.items-center.gap-5 4 | %a.mr-5.shrink-0(href="/home" title="Resolute") 5 | = image_tag "icon.svg", size: "50x50", alt: "Resolute logo" 6 | %a{href: "/home", class: [selected != :reminders && "text-gray-500"]} Reminders 7 | %a{href: "/integrations", class: [selected != :integrations && "text-gray-500"]} Integrations 8 | %a{href: "/settings", class: [selected != :settings && "text-gray-500"]} Settings 9 | 10 | 11 | -# Mobile navbar 12 | .flex.sm:hidden.items-center.gap-5 13 | %a.mr-5.sm:mr-5.shrink-0(href="/home" title="Resolute") 14 | = image_tag "icon.svg", size: "50x50", alt: "Resolute logo" 15 | %a.btn.px-3{href: "/home", class: [selected != :reminders && "bg-gray-700"]} 16 | %i.fa-solid.fa-home.fa-fw 17 | %a.btn.px-3{href: "/integrations", class: [selected != :integrations && "bg-gray-700"]} 18 | %i.fa-solid.fa-plug.fa-fw 19 | 20 | .flex.items-center.gap-5.relative{"x-data" => "{ open: false }"} 21 | .flex.items-center.gap-4.cursor-pointer.px-3{"@click" => "open = !open"} 22 | = gravatar @current_user.email, 40 23 | %i.fa-solid.fa-chevron-down 24 | %ul.absolute.top-full.right-0.bg-gray-800.rounded-md.p-2.w-52.mt-2.border.border-gray-700.z-10.shadow-lg{"x-show" => "open", "x-cloak" => true, "x-transition" => true, "@click.outside" => "open = false"} 25 | %a.hover:no-underline{href: "/settings"} 26 | %li.py-2.px-3.hover:bg-gray-700.rounded-md.text-white.flex.items-center.gap-2 27 | Settings 28 | = form_with url: "/logout", method: :post do |form| 29 | %li.hover:bg-gray-700.rounded-md.text-white.flex.items-center.gap-2 30 | %button.w-full.text-left.py-2.px-3{type: "submit"} 31 | Log out 32 | -------------------------------------------------------------------------------- /app/views/shared/_landing_header.html.haml: -------------------------------------------------------------------------------- 1 | .flex.py-5.px-5.sm:px-10.justify-between.sticky.top-0.bg-black.z-40.items-center 2 | %a.flex.items-start.gap-1.text-white.hover:no-underline{href: "/"} 3 | .flex.gap-5.items-center 4 | = image_tag "icon.svg", size: "50x50", alt: "Resolute logo" 5 | %h1.font-heading.text-4xl.font-light.hidden.sm:block resolute 6 | .bg-blue-600.rounded-md.text-xs.text-blue-100(class="py-0.5 px-1.5") Beta 7 | 8 | - if @current_user.nil? 9 | .flex.gap-5.items-center 10 | %a.btn.bg-gray-700(href="/login") 11 | %span.hidden.xs:inline-block Log in 12 | %i.fa-solid.fa-sign-in-alt.inline-block.xs:hidden 13 | %a.btn(href="/signup") Sign up 14 | - else 15 | .flex.gap-5.items-center 16 | %a.btn(href="/home") Launch app 17 | = gravatar @current_user.email 18 | -------------------------------------------------------------------------------- /app/views/shared/_notice.haml: -------------------------------------------------------------------------------- 1 | - if notice 2 | %div.notice.mb-6(data-controller="alert") 3 | = notice 4 | %button.bg-transparent(data-action="alert#dismiss") 5 | %i.fa-solid.fa-times -------------------------------------------------------------------------------- /app/views/shared/_toast.html.haml: -------------------------------------------------------------------------------- 1 | - if notice 2 | .notice.inline-flex.fixed.top-16.z-50.animate-toast{class: "left-1/2 -translate-x-1/2"}= notice 3 | 4 | - if alert 5 | .alert.inline-flex.fixed.top-16.z-50.animate-toast{class: "left-1/2 -translate-x-1/2"}= alert 6 | -------------------------------------------------------------------------------- /app/views/signup/index.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Sign up" 2 | 3 | = render "layouts/sm", title: "Sign up", back_href: "/", back_text: "Home" do 4 | = form_with do |form| 5 | = form.email_field :email, placeholder: "Email", class: "input mb-6", autofocus: true, required: true, value: @prefill_email 6 | 7 | .flex.justify-between.items-center 8 | = form.submit "Continue", class: "btn" 9 | -------------------------------------------------------------------------------- /app/views/signup/submit.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Sign up" 2 | 3 | = render "layouts/sm", title: "Almost there!", back_href: "/signup" do 4 | Check your email 5 | %span.text-gray-400 (#{@email}) 6 | for a verification link! 🔗 7 | -------------------------------------------------------------------------------- /app/views/signup/verify.html.haml: -------------------------------------------------------------------------------- 1 | - content_for :page_title, "Sign up" 2 | 3 | = render "layouts/sm", title: "Sign up" do 4 | = form_with do |form| 5 | = form.hidden_field :verification_code, value: @verification_code 6 | = form.email_field :email, disabled: true, placeholder: "Email", class: "input mb-4", required: true, value: @email 7 | = form.password_field :password, placeholder: "Password", class: "input mb-6", required: true, autofocus: true 8 | 9 | .flex.justify-between.items-center 10 | = form.submit "Sign up", class: "btn" 11 | -------------------------------------------------------------------------------- /app/views/user_mailer/password_changed.html.haml: -------------------------------------------------------------------------------- 1 | %p Just a heads up, your Resolute account password was just changed. 2 | 3 | %p If this wasn't you, please reset your password immediately. -------------------------------------------------------------------------------- /app/views/user_mailer/password_reset.html.haml: -------------------------------------------------------------------------------- 1 | %p You (or someone pretending to be you) just requested a password reset. Click here to reset your password. 2 | 3 | %p If this wasn't you, you can safely ignore this email. -------------------------------------------------------------------------------- /app/views/user_mailer/signup_verification.html.haml: -------------------------------------------------------------------------------- 1 | %p Welcome to Resolute, #{@email}! 2 | 3 | %p 4 | One more step: just click this link to verify your email address. -------------------------------------------------------------------------------- /app/views/user_mailer/signup_verification.text.haml: -------------------------------------------------------------------------------- 1 | Welcome to Resolute, #{@email}! 2 | 3 | One more step: just go to #{url_for controller: 'signup', action: 'verify', code: @verification_code} to verify your email address. -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! command -v foreman &> /dev/null 4 | then 5 | echo "Installing foreman..." 6 | gem install foreman 7 | fi 8 | 9 | foreman start -f Procfile.dev 10 | -------------------------------------------------------------------------------- /bin/esbuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const esbuild = require("esbuild"); 4 | const { pnpPlugin } = require("@yarnpkg/esbuild-plugin-pnp"); 5 | 6 | const util = require("util"); 7 | const exec = util.promisify(require("child_process").exec); 8 | 9 | const watch = process.argv.includes("--watch"); 10 | 11 | (async () => { 12 | const { stdout: revision } = await exec("git rev-parse HEAD 2> /dev/null"); 13 | 14 | await esbuild.build({ 15 | entryPoints: ["app/javascript/application.ts"], 16 | bundle: true, 17 | minify: process.env.NODE_ENV === "production", 18 | watch: watch 19 | ? { 20 | onRebuild(error, result) { 21 | if (!error) { 22 | console.log("Rebuilt successfully."); 23 | } 24 | }, 25 | } 26 | : undefined, 27 | inject: ["app/javascript/shim/jsx-shim.ts"], 28 | outdir: "app/assets/builds", 29 | jsxFactory: "jsxCreateElement", 30 | // plugins: [pnpPlugin()], 31 | define: { 32 | HONEYBADGER_API_KEY: JSON.stringify( 33 | process.env.HONEYBADGER_FRONTEND_API_KEY 34 | ), 35 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 36 | GIT_REVISION: JSON.stringify(revision.trim()), 37 | }, 38 | }); 39 | 40 | if (watch) { 41 | console.log("Watching for changes...\n"); 42 | } else { 43 | console.log("Done!"); 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) 3 | gem "bundler" 4 | require "bundler" 5 | 6 | # Load Spring without loading other gems in the Gemfile, for speed. 7 | Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| 8 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 9 | gem "spring", spring.version 10 | require "spring/binstub" 11 | rescue Gem::LoadError 12 | # Ignore when Spring is not installed. 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec 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 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails/all' 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | 11 | module Resolute 12 | class Application < Rails::Application 13 | # Initialize configuration defaults for originally generated Rails version. 14 | config.load_defaults 7.0 15 | 16 | # Configuration for the application, engines, and railties goes here. 17 | # 18 | # These settings can be overridden in specific environments using the files 19 | # in config/environments, which are processed later. 20 | # 21 | # config.time_zone = "Central Time (US & Canada)" 22 | # config.eager_load_paths << Rails.root.join("extras") 23 | 24 | config.session_store :cookie_store, key: '_resolute_session', expire_after: 30.days 25 | config.exceptions_app = routes 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 4 | 5 | require 'bundler/setup' # Set up gems listed in the Gemfile. 6 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 7 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: redis://localhost:6379 4 | 5 | test: 6 | adapter: test 7 | 8 | production: 9 | adapter: redis 10 | url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6379") %> 11 | channel_prefix: resolute_production 12 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | IJgpTOZ5u3JnGoYZaxVLGMXkY1W1bQXYM3C37Fbd3Wt5QUdpAKQqIL2SBstsbxRiydjvXdt1HC/y1bxTdfaX/bhw8DvOGopKpsLaKlfxuuGQTbUU+un1v3w52hqfPLaoEuP2xDfUTE0aRxY6pdd5WgtpAcwUDKP8iJTvZWewOf5xy7/adZXXDQusl7z3F2JiWDyOqIdS75yZTbS9cujY7LLJ7Llj9x0NLhnJL7NhTh/+RcDSbYCWIWvtC5NfwKgfbDuHcVdD5HY4jpzlvYPF9/uNh7SBTeYfkuN8wGb8R6mYMmjlJpCPicalPUhxJsecWYbaeYJLp7KGMTrEmjtTLK7vsei7zqDYRQM1tZJbcw3zjsYqZFiW3R6pvPdtbFKlwKrXKYXV+ypcrNtaVxYoSbJjsPID43BXKHNjS8e2RT3eRPSM3LTyDYDMYbblnYFBaiuyJ4veU81Li6av+kPSbhIH+jo1E0t3AH0saJkSsUidTA85q1to46WuRph9JL/hAtLOadQtbzJ6J52weXenVSLEkQiTS1zCwNcXnJ+cTvc9Xj+Lz4jPqBPivq2ZSHTl6LJIUQTRP8MsPXdyYMO8SUr5VwOQkXgYoSj4jl0NhhiPjtiyUj+gT70sqCQmVVpdyXIlUceAEHhTBUSEXfV+xsBx3lknZQ7zB5KSwcLZpiIaeHDsCZrKfona3jQuFIEOmjGg0khXIUQvbIdS/i6FjUUzFPJmGlwgxR7Of3kkMfQCxzNYxmAj+mJqxEChLfRH4aRyajlGa4/Dcu5Jse8e+v8o82nkHqxOJcFJAirhVvAOjkbayGeYn91oFc5IVUl2Rj/0UIXB+kxhLhqlI/jkCIwd6MV1/ddQdBpaZHk1zcvdA962RxVDwXLi5UOI0htzgFJ1rKZQodPHE7nHdSvY6tRo/Dpj2G0TQptMJ9ty15d1aPgw2lyaulchUeTTveQr68zuu2c6Co8r5jYEHPLQyzCnsUOkwryPn2+cNK5KetHyYIQa1QZsVEDNURc0cJcj3+tywpAi+alpxZMrCb4K+NSjReBz0wnixmZRWP0j3+dDYFod/kb1XNMEaw1rr1IOCOOBPEaYWV+xI+8c/DuAwZ4jRUgnkQzxdCOcP0r/vC2AE73LeHsv--+uxudSce16/btGEI--2xMotthp7+ax9B9dI5WYxA== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 3 | timeout: 5000 4 | 5 | development: 6 | <<: *default 7 | adapter: postgres 8 | url: postgres://localhost:5432/resolute 9 | 10 | # Warning: The database defined as "test" will be erased and 11 | # re-generated from your development database when you run "rake". 12 | # Do not set this db to the same as development or production. 13 | test: 14 | <<: *default 15 | adapter: sqlite3 16 | database: db/test.sqlite3 17 | 18 | production: 19 | <<: *default 20 | adapter: postgres 21 | url: <%= ENV["DATABASE_URL"] %> 22 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # In the development environment your application's code is reloaded any time 9 | # it changes. This slows down response time but is perfect for development 10 | # since you don't have to restart the web server when you make code changes. 11 | config.cache_classes = false 12 | 13 | # Do not eager load code on boot. 14 | config.eager_load = false 15 | 16 | # Show full error reports. 17 | config.consider_all_requests_local = true 18 | 19 | # Enable server timing 20 | config.server_timing = true 21 | 22 | # Enable/disable caching. By default caching is disabled. 23 | # Run rails dev:cache to toggle caching. 24 | if Rails.root.join('tmp/caching-dev.txt').exist? 25 | config.action_controller.perform_caching = true 26 | config.action_controller.enable_fragment_cache_logging = true 27 | 28 | config.cache_store = :memory_store 29 | config.public_file_server.headers = { 30 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 31 | } 32 | else 33 | config.action_controller.perform_caching = false 34 | 35 | config.cache_store = :null_store 36 | end 37 | 38 | # Store uploaded files on the local file system (see config/storage.yml for options). 39 | config.active_storage.service = :local 40 | 41 | # Don't care if the mailer can't send. 42 | config.action_mailer.raise_delivery_errors = false 43 | 44 | config.action_mailer.perform_caching = false 45 | 46 | config.action_mailer.delivery_method = :smtp 47 | config.action_mailer.smtp_settings = { 48 | port: 1025 49 | } 50 | 51 | config.action_mailer.default_url_options = { host: 'localhost:3000' } 52 | 53 | # Print deprecation notices to the Rails logger. 54 | config.active_support.deprecation = :log 55 | 56 | # Raise exceptions for disallowed deprecations. 57 | config.active_support.disallowed_deprecation = :raise 58 | 59 | # Tell Active Support which deprecation messages to disallow. 60 | config.active_support.disallowed_deprecation_warnings = [] 61 | 62 | # Raise an error on page load if there are pending migrations. 63 | config.active_record.migration_error = :page_load 64 | 65 | # Highlight code that triggered database queries in logs. 66 | config.active_record.verbose_query_logs = true 67 | 68 | # Suppress logger output for asset requests. 69 | config.assets.quiet = true 70 | 71 | # Raises error for missing translations. 72 | # config.i18n.raise_on_missing_translations = true 73 | 74 | # Annotate rendered view with file names. 75 | # config.action_view.annotate_rendered_view_with_filenames = true 76 | 77 | # Uncomment if you wish to allow Action Cable access from any origin. 78 | # config.action_cable.disable_request_forgery_protection = true 79 | end 80 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # Code is not reloaded between requests. 9 | config.cache_classes = true 10 | 11 | # Eager load code on boot. This eager loads most of Rails and 12 | # your application in memory, allowing both threaded web servers 13 | # and those relying on copy on write to perform better. 14 | # Rake tasks automatically ignore this option for performance. 15 | config.eager_load = true 16 | 17 | # Full error reports are disabled and caching is turned on. 18 | config.consider_all_requests_local = false 19 | config.action_controller.perform_caching = true 20 | 21 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 22 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 23 | config.require_master_key = false 24 | 25 | # Disable serving static files from the `/public` folder by default since 26 | # Apache or NGINX already handles this. 27 | config.public_file_server.enabled = true 28 | 29 | # Compress CSS using a preprocessor. 30 | # config.assets.css_compressor = :sass 31 | 32 | # Do not fallback to assets pipeline if a precompiled asset is missed. 33 | config.assets.compile = false 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.asset_host = "http://assets.example.com" 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 40 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 41 | 42 | # Store uploaded files on the local file system (see config/storage.yml for options). 43 | config.active_storage.service = :local 44 | 45 | # Mount Action Cable outside main process or domain. 46 | # config.action_cable.mount_path = nil 47 | # config.action_cable.url = "wss://example.com/cable" 48 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 49 | 50 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 51 | # config.force_ssl = true 52 | 53 | # Include generic and useful information about system operation, but avoid logging too much 54 | # information to avoid inadvertent exposure of personally identifiable information (PII). 55 | config.log_level = :info 56 | 57 | # Prepend all log lines with the following tags. 58 | config.log_tags = [:request_id] 59 | 60 | # Use a different cache store in production. 61 | # config.cache_store = :mem_cache_store 62 | 63 | # Use a real queuing backend for Active Job (and separate queues per environment). 64 | # config.active_job.queue_adapter = :resque 65 | # config.active_job.queue_name_prefix = "resolute_production" 66 | 67 | config.action_mailer.perform_caching = false 68 | 69 | # Ignore bad email addresses and do not raise email delivery errors. 70 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 71 | # config.action_mailer.raise_delivery_errors = false 72 | 73 | config.action_mailer.delivery_method = :smtp 74 | config.action_mailer.smtp_settings = { 75 | address: Rails.application.credentials.dig(:mail, :address), 76 | user_name: Rails.application.credentials.dig(:mail, :username), 77 | password: Rails.application.credentials.dig(:mail, :password) 78 | } 79 | 80 | config.action_mailer.default_url_options = { host: 'useresolute.com' } 81 | 82 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 83 | # the I18n.default_locale when a translation cannot be found). 84 | config.i18n.fallbacks = true 85 | 86 | # Don't log any deprecations. 87 | config.active_support.report_deprecations = false 88 | 89 | # Use default logging formatter so that PID and timestamp are not suppressed. 90 | config.log_formatter = ::Logger::Formatter.new 91 | 92 | # Use a different logger for distributed setups. 93 | # require "syslog/logger" 94 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 95 | 96 | if ENV['RAILS_LOG_TO_STDOUT'].present? 97 | logger = ActiveSupport::Logger.new($stdout) 98 | logger.formatter = config.log_formatter 99 | config.logger = ActiveSupport::TaggedLogging.new(logger) 100 | end 101 | 102 | # Do not dump schema after migrations. 103 | config.active_record.dump_schema_after_migration = false 104 | end 105 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | 10 | Rails.application.configure do 11 | # Settings specified here will take precedence over those in config/application.rb. 12 | 13 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 14 | config.cache_classes = true 15 | 16 | # Eager loading loads your whole application. When running a single test locally, 17 | # this probably isn't necessary. It's a good idea to do in a continuous integration 18 | # system, or in some way before deploying your code. 19 | config.eager_load = ENV['CI'].present? 20 | 21 | # Configure public file server for tests with Cache-Control for performance. 22 | config.public_file_server.enabled = true 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 25 | } 26 | 27 | # Show full error reports and disable caching. 28 | config.consider_all_requests_local = true 29 | config.action_controller.perform_caching = false 30 | config.cache_store = :null_store 31 | 32 | # Raise exceptions instead of rendering exception templates. 33 | config.action_dispatch.show_exceptions = false 34 | 35 | # Disable request forgery protection in test environment. 36 | config.action_controller.allow_forgery_protection = false 37 | 38 | # Store uploaded files on the local file system in a temporary directory. 39 | config.active_storage.service = :test 40 | 41 | config.action_mailer.perform_caching = false 42 | 43 | # Tell Action Mailer not to deliver emails to the real world. 44 | # The :test delivery method accumulates sent emails in the 45 | # ActionMailer::Base.deliveries array. 46 | config.action_mailer.delivery_method = :test 47 | 48 | # Print deprecation notices to the stderr. 49 | config.active_support.deprecation = :stderr 50 | 51 | # Raise exceptions for disallowed deprecations. 52 | config.active_support.disallowed_deprecation = :raise 53 | 54 | # Tell Active Support which deprecation messages to disallow. 55 | config.active_support.disallowed_deprecation_warnings = [] 56 | 57 | # Raises error for missing translations. 58 | # config.i18n.raise_on_missing_translations = true 59 | 60 | # Annotate rendered view with file names. 61 | # config.action_view.annotate_rendered_view_with_filenames = true 62 | end 63 | -------------------------------------------------------------------------------- /config/honeybadger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration 3 | 4 | api_key: "<%= Rails.application.credentials.honeybadger_api_key %>" 5 | 6 | # The environment your app is running in. 7 | env: "<%= Rails.env %>" 8 | 9 | # The absolute path to your project folder. 10 | root: "<%= Rails.root.to_s %>" 11 | 12 | # Honeybadger won't report errors in these environments. 13 | development_environments: 14 | - test 15 | - development 16 | - cucumber 17 | 18 | # By default, Honeybadger won't report errors in the development_environments. 19 | # You can override this by explicitly setting report_data to true or false. 20 | # report_data: true 21 | 22 | # The current Git revision of your project. Defaults to the last commit hash. 23 | # revision: null 24 | 25 | # Enable verbose debug logging (useful for troubleshooting). 26 | debug: false 27 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # ActiveSupport::Reloader.to_prepare do 5 | # ApplicationController.renderer.defaults.merge!( 6 | # http_host: 'example.org', 7 | # https: false 8 | # ) 9 | # end 10 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = '1.0' 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 9 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 10 | Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] 11 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Define an application-wide content security policy 5 | # For further information see the following documentation 6 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 7 | 8 | # Rails.application.configure do 9 | # config.content_security_policy do |policy| 10 | # policy.default_src :self, :https 11 | # policy.font_src :self, :https, :data 12 | # policy.img_src :self, :https, :data 13 | # policy.object_src :none 14 | # policy.script_src :self, :https 15 | # policy.style_src :self, :https 16 | # # Specify URI for violation reports 17 | # # policy.report_uri "/csp-violation-report-endpoint" 18 | # end 19 | # 20 | # # Generate session nonces for permitted importmap and inline scripts 21 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 22 | # config.content_security_policy_nonce_directives = %w(script-src) 23 | # 24 | # # Report CSP violations to a specified URI. See: 25 | # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 26 | # # config.content_security_policy_report_only = true 27 | # end 28 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Specify a serializer for the signed and encrypted cookie jars. 6 | # Valid options are :json, :marshal, and :hybrid. 7 | Rails.application.config.action_dispatch.cookies_serializer = :json 8 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += %i[ 7 | passw secret token _key crypt salt certificate otp ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/honeybadger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Honeybadger.configure do |config| 4 | config.before_notify do |notice| 5 | # Change "errors" to match your custom controller name. 6 | break if notice.component != 'errors' 7 | 8 | # Look up original route path and override controller/action 9 | # in Honeybadger. 10 | params = Rails.application.routes.recognize_path(notice.url) 11 | notice.component = params[:controller] 12 | notice.action = params[:action] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, "\\1en" 9 | # inflect.singular /^(ox)en/i, "\\1" 10 | # inflect.irregular "person", "people" 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym "RESTful" 17 | # end 18 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new mime types for use in respond_to blocks: 5 | # Mime::Type.register "text/richtext", :rtf 6 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define an application-wide HTTP permissions policy. For further 3 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 4 | # 5 | # Rails.application.config.permissions_policy do |f| 6 | # f.camera :none 7 | # f.gyroscope :none 8 | # f.microphone :none 9 | # f.usb :none 10 | # f.fullscreen :self 11 | # f.payment :self, "https://secure.example.com" 12 | # end 13 | -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $redis = Redis::Namespace.new( 4 | :resolute, 5 | redis: Redis.new( 6 | url: ENV.fetch('REDIS_URL', 'redis://localhost:6379') 7 | ) 8 | ) 9 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Sidekiq.configure_server do |config| 4 | # config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379') } 5 | # end 6 | 7 | # Sidekiq.configure_client do |config| 8 | # config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379') } 9 | # end 10 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than 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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 14 | # terminating a worker in development environments. 15 | # 16 | worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' 17 | 18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 19 | # 20 | port ENV.fetch('PORT', 3000) 21 | 22 | # Specifies the `environment` that Puma will run in. 23 | # 24 | environment ENV.fetch('RAILS_ENV', 'development') 25 | 26 | # Specifies the `pidfile` that Puma will use. 27 | pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') 28 | 29 | # Specifies the number of `workers` to boot in clustered mode. 30 | # Workers are forked web server processes. If using threads and workers together 31 | # the concurrency of the application would be max `threads` * `workers`. 32 | # Workers do not work on JRuby or Windows (both of which do not support 33 | # processes). 34 | # 35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 36 | 37 | # Use the `preload_app!` method when specifying a `workers` number. 38 | # This directive tells Puma to first boot the application and load code 39 | # before forking the application. This takes advantage of Copy On Write 40 | # process behavior so workers use less memory. 41 | # 42 | # preload_app! 43 | 44 | # Allow puma to be restarted by `rails restart` command. 45 | plugin :tmp_restart 46 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html 5 | root to: 'landing#index' 6 | 7 | get 'signup', to: 'signup#index' 8 | post 'signup', to: 'signup#submit' 9 | get 'verify', to: 'signup#verify' 10 | post 'verify', to: 'signup#finalize' 11 | 12 | get 'forgot-password', to: 'auth#forgot_password' 13 | post 'forgot-password', to: 'auth#forgot_password_submit' 14 | get 'forgot-password/verify', to: 'auth#forgot_password_verify' 15 | post 'forgot-password/verify', to: 'auth#forgot_password_finalize' 16 | 17 | scope '/callback' do 18 | get 'github', to: 'oauth_callback#github' 19 | end 20 | 21 | get 'login', to: 'auth#login' 22 | post 'login', to: 'auth#auth' 23 | 24 | post 'logout', to: 'auth#logout' 25 | 26 | get 'home', to: 'reminders#index' 27 | resources :reminders, only: %i[create destroy] 28 | 29 | resources :integrations, only: %i[index] 30 | 31 | scope '/settings' do 32 | get '/', to: 'settings#index' 33 | post '/', to: 'settings#update' 34 | get 'security', to: 'settings#security' 35 | 36 | resources :sessions, path: 'security/sessions', only: %i[destroy] do 37 | collection do 38 | post 'destroy_all' 39 | end 40 | end 41 | 42 | resources :authorizations, path: 'security/authorizations', only: %i[destroy] 43 | end 44 | 45 | scope '/developer' do 46 | get '/', to: 'settings#developer' 47 | 48 | resources :oauth_apps, path: 'apps', path_names: { edit: 'settings' } do 49 | member do 50 | get 'advanced' 51 | end 52 | end 53 | 54 | resources :personal_tokens, path: 'tokens', only: %i[index new create destroy] 55 | end 56 | 57 | scope '/hovercard' do 58 | get 'unfurl', to: 'hovercard#unfurl' 59 | end 60 | 61 | match '/404', to: 'errors#not_found', via: :all 62 | match '/500', to: 'errors#internal_server_error', via: :all 63 | 64 | # load routes/api.rb 65 | draw :api 66 | end 67 | -------------------------------------------------------------------------------- /config/routes/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :api do 4 | scope '/oauth' do 5 | get 'authorize', to: 'oauth#authorize' 6 | post 'authorize', to: 'oauth#authorize_submit' 7 | post 'token', to: 'oauth#token' 8 | end 9 | 10 | defaults format: :json do 11 | get 'me', to: 'users#me' 12 | resources :reminders, only: %i[index create destroy] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Spring.watch( 4 | '.ruby-version', 5 | '.rbenv-vars', 6 | 'tmp/restart.txt', 7 | 'tmp/caching-dev.txt' 8 | ) 9 | -------------------------------------------------------------------------------- /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 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 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 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 29 | 30 | # mirror: 31 | # service: Mirror 32 | # primary: local 33 | # mirrors: [ amazon, google, microsoft ] 34 | -------------------------------------------------------------------------------- /db/migrate/20211111021027_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :email 5 | t.string :hashed_password 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20211111022726_rename_hashed_password.rb: -------------------------------------------------------------------------------- 1 | class RenameHashedPassword < ActiveRecord::Migration[6.1] 2 | def change 3 | change_table :users do |t| 4 | t.rename :hashed_password, :password_digest 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20211111041143_create_sessions.rb: -------------------------------------------------------------------------------- 1 | class CreateSessions < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :sessions do |t| 4 | t.string :token 5 | t.references :user, null: false, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | add_index :sessions, :token, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20211111052623_create_reminders.rb: -------------------------------------------------------------------------------- 1 | class CreateReminders < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :reminders do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :title 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20211119030654_create_signup_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateSignupRequests < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :signup_requests do |t| 4 | t.string :email, null: false 5 | t.string :code, null: false 6 | t.boolean :fulfilled, default: false, null: false 7 | t.datetime :expires_at, null: false 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20211125033049_unique_fields.rb: -------------------------------------------------------------------------------- 1 | class UniqueFields < ActiveRecord::Migration[6.1] 2 | def change 3 | add_index :users, :email, unique: true 4 | add_index :signup_requests, :code, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20211125044511_create_oauth_apps.rb: -------------------------------------------------------------------------------- 1 | class CreateOauthApps < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :oauth_apps do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :client_id 6 | t.string :client_secret 7 | t.string :redirect_uri 8 | 9 | t.timestamps 10 | end 11 | add_index :oauth_apps, :client_id, unique: true 12 | add_index :oauth_apps, :client_secret, unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20211125050029_add_name_to_oauth_app.rb: -------------------------------------------------------------------------------- 1 | class AddNameToOauthApp < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :oauth_apps, :name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211126050826_add_official_to_oauth_app.rb: -------------------------------------------------------------------------------- 1 | class AddOfficialToOauthApp < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :oauth_apps, :official, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211128041643_create_oauth_grants.rb: -------------------------------------------------------------------------------- 1 | class CreateOauthGrants < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :oauth_grants do |t| 4 | t.string :code 5 | t.string :scope, array: true 6 | t.references :user, null: false, foreign_key: true 7 | t.references :oauth_app, null: false, foreign_key: true 8 | 9 | t.timestamps 10 | end 11 | add_index :oauth_grants, :code, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20211128044302_add_expires_at_to_oauth_grant.rb: -------------------------------------------------------------------------------- 1 | class AddExpiresAtToOauthGrant < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :oauth_grants, :expires_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211128191000_add_fulfilled_to_oauth_grants.rb: -------------------------------------------------------------------------------- 1 | class AddFulfilledToOauthGrants < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :oauth_grants, :fulfilled, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211129020838_create_api_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreateApiTokens < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :api_tokens do |t| 4 | t.references :oauth_app, null: false, foreign_key: true 5 | t.references :user, null: false, foreign_key: true 6 | t.string :token 7 | t.string :scope, array: true 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20211201171829_add_oauth_app_to_reminders.rb: -------------------------------------------------------------------------------- 1 | class AddOauthAppToReminders < ActiveRecord::Migration[6.1] 2 | def change 3 | add_reference :reminders, :oauth_app, null: true, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211201172253_reminder_oauth_app_optional.rb: -------------------------------------------------------------------------------- 1 | class ReminderOauthAppOptional < ActiveRecord::Migration[6.1] 2 | def change 3 | change_column_null :reminders, :oauth_app_id, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211202020630_add_ip_and_user_agent_to_sessions.rb: -------------------------------------------------------------------------------- 1 | class AddIpAndUserAgentToSessions < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :sessions, :ip, :string 4 | add_column :sessions, :user_agent, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20211202174211_add_login_method_to_session.rb: -------------------------------------------------------------------------------- 1 | class AddLoginMethodToSession < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :sessions, :login_method, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211202225129_add_description_to_api_token.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToApiToken < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :api_tokens, :description, :string 4 | change_column_null :api_tokens, :oauth_app_id, true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20211207152821_add_public_to_oauth_app.rb: -------------------------------------------------------------------------------- 1 | class AddPublicToOauthApp < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :oauth_apps, :public, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211209041857_create_password_reset_requests.rb: -------------------------------------------------------------------------------- 1 | class CreatePasswordResetRequests < ActiveRecord::Migration[6.1] 2 | def change 3 | create_table :password_reset_requests do |t| 4 | t.references :user, null: false, foreign_key: true 5 | t.string :code 6 | t.boolean :fulfilled 7 | t.datetime :expires_at 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20211214205049_password_reset_request_fulfilled_default.rb: -------------------------------------------------------------------------------- 1 | class PasswordResetRequestFulfilledDefault < ActiveRecord::Migration[6.1] 2 | def change 3 | change_column_default :password_reset_requests, :fulfilled, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211215035648_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.string :service_name, null: false 10 | t.bigint :byte_size, null: false 11 | t.string :checksum, null: false 12 | t.datetime :created_at, null: false 13 | 14 | t.index [ :key ], unique: true 15 | end 16 | 17 | create_table :active_storage_attachments do |t| 18 | t.string :name, null: false 19 | t.references :record, null: false, polymorphic: true, index: false 20 | t.references :blob, null: false 21 | 22 | t.datetime :created_at, null: false 23 | 24 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true 25 | t.foreign_key :active_storage_blobs, column: :blob_id 26 | end 27 | 28 | create_table :active_storage_variant_records do |t| 29 | t.belongs_to :blob, null: false, index: false 30 | t.string :variation_digest, null: false 31 | 32 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 33 | t.foreign_key :active_storage_blobs, column: :blob_id 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /db/migrate/20211217022916_add_fields_to_reminder.rb: -------------------------------------------------------------------------------- 1 | class AddFieldsToReminder < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :reminders, :description, :text 4 | add_column :reminders, :author_name, :text 5 | add_column :reminders, :author_avatar, :text 6 | add_column :reminders, :url, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20211217023036_author_name_to_author.rb: -------------------------------------------------------------------------------- 1 | class AuthorNameToAuthor < ActiveRecord::Migration[6.1] 2 | def change 3 | rename_column :reminders, :author_name, :author 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211217030224_title_to_description.rb: -------------------------------------------------------------------------------- 1 | class TitleToDescription < ActiveRecord::Migration[6.1] 2 | def change 3 | Reminder.connection.execute('UPDATE reminders SET description = title, title = NULL') 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211219051154_reminder_description_to_text.rb: -------------------------------------------------------------------------------- 1 | class ReminderDescriptionToText < ActiveRecord::Migration[6.1] 2 | def change 3 | rename_column :reminders, :description, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20211229214208_add_installation_url_to_oauth_apps.rb: -------------------------------------------------------------------------------- 1 | class AddInstallationUrlToOauthApps < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :oauth_apps, :installation_url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220122173730_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | unless column_exists?(:active_storage_blobs, :service_name) 5 | add_column :active_storage_blobs, :service_name, :string 6 | 7 | if configured_service = ActiveStorage::Blob.service.name 8 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 9 | end 10 | 11 | change_column :active_storage_blobs, :service_name, :string, null: false 12 | end 13 | end 14 | 15 | def down 16 | remove_column :active_storage_blobs, :service_name 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20220122173731_create_active_storage_variant_records.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20191206030411) 2 | class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] 3 | def change 4 | # Use Active Record's configured type for primary key 5 | create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| 6 | t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type 7 | t.string :variation_digest, null: false 8 | 9 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 10 | t.foreign_key :active_storage_blobs, column: :blob_id 11 | end 12 | end 13 | 14 | private 15 | def primary_key_type 16 | config = Rails.configuration.generators 17 | config.options[config.orm][:primary_key_type] || :primary_key 18 | end 19 | 20 | def blobs_primary_key_type 21 | pkey_name = connection.primary_key(:active_storage_blobs) 22 | pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } 23 | pkey_column.bigint? ? :bigint : pkey_column.type 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /db/migrate/20220122173732_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | change_column_null(:active_storage_blobs, :checksum, true) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20220123025226_add_source_to_reminder.rb: -------------------------------------------------------------------------------- 1 | class AddSourceToReminder < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :reminders, :source, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220224233219_add_discarded_at_to_oauth_apps.rb: -------------------------------------------------------------------------------- 1 | class AddDiscardedAtToOauthApps < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :oauth_apps, :discarded_at, :datetime 4 | add_index :oauth_apps, :discarded_at 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20220320193230_add_last_active_at_to_session.rb: -------------------------------------------------------------------------------- 1 | class AddLastActiveAtToSession < ActiveRecord::Migration[7.0] 2 | def change 3 | add_column :sessions, :last_active_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2022_03_20_193230) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "active_storage_attachments", force: :cascade do |t| 19 | t.string "name", null: false 20 | t.string "record_type", null: false 21 | t.bigint "record_id", null: false 22 | t.bigint "blob_id", null: false 23 | t.datetime "created_at", null: false 24 | t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" 25 | t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true 26 | end 27 | 28 | create_table "active_storage_blobs", force: :cascade do |t| 29 | t.string "key", null: false 30 | t.string "filename", null: false 31 | t.string "content_type" 32 | t.text "metadata" 33 | t.string "service_name", null: false 34 | t.bigint "byte_size", null: false 35 | t.string "checksum" 36 | t.datetime "created_at", null: false 37 | t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true 38 | end 39 | 40 | create_table "active_storage_variant_records", force: :cascade do |t| 41 | t.bigint "blob_id", null: false 42 | t.string "variation_digest", null: false 43 | t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true 44 | end 45 | 46 | create_table "api_tokens", force: :cascade do |t| 47 | t.bigint "oauth_app_id" 48 | t.bigint "user_id", null: false 49 | t.string "token" 50 | t.string "scope", array: true 51 | t.datetime "created_at", precision: 6, null: false 52 | t.datetime "updated_at", precision: 6, null: false 53 | t.string "description" 54 | t.index ["oauth_app_id"], name: "index_api_tokens_on_oauth_app_id" 55 | t.index ["user_id"], name: "index_api_tokens_on_user_id" 56 | end 57 | 58 | create_table "oauth_apps", force: :cascade do |t| 59 | t.bigint "user_id", null: false 60 | t.string "client_id" 61 | t.string "client_secret" 62 | t.string "redirect_uri" 63 | t.datetime "created_at", precision: 6, null: false 64 | t.datetime "updated_at", precision: 6, null: false 65 | t.string "name" 66 | t.boolean "official" 67 | t.boolean "public" 68 | t.string "installation_url" 69 | t.datetime "discarded_at", precision: 6 70 | t.index ["client_id"], name: "index_oauth_apps_on_client_id", unique: true 71 | t.index ["client_secret"], name: "index_oauth_apps_on_client_secret", unique: true 72 | t.index ["discarded_at"], name: "index_oauth_apps_on_discarded_at" 73 | t.index ["user_id"], name: "index_oauth_apps_on_user_id" 74 | end 75 | 76 | create_table "oauth_grants", force: :cascade do |t| 77 | t.string "code" 78 | t.string "scope", array: true 79 | t.bigint "user_id", null: false 80 | t.bigint "oauth_app_id", null: false 81 | t.datetime "created_at", precision: 6, null: false 82 | t.datetime "updated_at", precision: 6, null: false 83 | t.datetime "expires_at" 84 | t.boolean "fulfilled", default: false 85 | t.index ["code"], name: "index_oauth_grants_on_code", unique: true 86 | t.index ["oauth_app_id"], name: "index_oauth_grants_on_oauth_app_id" 87 | t.index ["user_id"], name: "index_oauth_grants_on_user_id" 88 | end 89 | 90 | create_table "password_reset_requests", force: :cascade do |t| 91 | t.bigint "user_id", null: false 92 | t.string "code" 93 | t.boolean "fulfilled", default: false 94 | t.datetime "expires_at" 95 | t.datetime "created_at", precision: 6, null: false 96 | t.datetime "updated_at", precision: 6, null: false 97 | t.index ["user_id"], name: "index_password_reset_requests_on_user_id" 98 | end 99 | 100 | create_table "reminders", force: :cascade do |t| 101 | t.bigint "user_id", null: false 102 | t.string "title" 103 | t.datetime "created_at", precision: 6, null: false 104 | t.datetime "updated_at", precision: 6, null: false 105 | t.bigint "oauth_app_id" 106 | t.text "text" 107 | t.text "author" 108 | t.text "author_avatar" 109 | t.string "url" 110 | t.text "source" 111 | t.index ["oauth_app_id"], name: "index_reminders_on_oauth_app_id" 112 | t.index ["user_id"], name: "index_reminders_on_user_id" 113 | end 114 | 115 | create_table "sessions", force: :cascade do |t| 116 | t.string "token" 117 | t.bigint "user_id", null: false 118 | t.datetime "created_at", precision: 6, null: false 119 | t.datetime "updated_at", precision: 6, null: false 120 | t.string "ip" 121 | t.string "user_agent" 122 | t.string "login_method" 123 | t.datetime "last_active_at", precision: 6 124 | t.index ["token"], name: "index_sessions_on_token", unique: true 125 | t.index ["user_id"], name: "index_sessions_on_user_id" 126 | end 127 | 128 | create_table "signup_requests", force: :cascade do |t| 129 | t.string "email", null: false 130 | t.string "code", null: false 131 | t.boolean "fulfilled", default: false, null: false 132 | t.datetime "expires_at", null: false 133 | t.datetime "created_at", precision: 6, null: false 134 | t.datetime "updated_at", precision: 6, null: false 135 | t.index ["code"], name: "index_signup_requests_on_code", unique: true 136 | end 137 | 138 | create_table "users", force: :cascade do |t| 139 | t.string "email" 140 | t.string "password_digest" 141 | t.datetime "created_at", precision: 6, null: false 142 | t.datetime "updated_at", precision: 6, null: false 143 | t.index ["email"], name: "index_users_on_email", unique: true 144 | end 145 | 146 | add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" 147 | add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" 148 | add_foreign_key "api_tokens", "oauth_apps" 149 | add_foreign_key "api_tokens", "users" 150 | add_foreign_key "oauth_apps", "users" 151 | add_foreign_key "oauth_grants", "oauth_apps" 152 | add_foreign_key "oauth_grants", "users" 153 | add_foreign_key "password_reset_requests", "users" 154 | add_foreign_key "reminders", "oauth_apps" 155 | add_foreign_key "reminders", "users" 156 | add_foreign_key "sessions", "users" 157 | end 158 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resolute", 3 | "private": true, 4 | "version": "0.1.0", 5 | "dependencies": { 6 | "@alpinejs/collapse": "^3.8.1", 7 | "@alpinejs/persist": "^3.7.1", 8 | "@floating-ui/dom": "^0.2.0", 9 | "@honeybadger-io/js": "^3.2.8", 10 | "@hotwired/stimulus": "^3.1.0", 11 | "@hotwired/turbo-rails": "^7.2.0", 12 | "@rails/actioncable": "^7.0.1", 13 | "@rails/activestorage": "^7.0.1", 14 | "@yarnpkg/esbuild-plugin-pnp": "^2.0.0", 15 | "alpinejs": "^3.7.0", 16 | "autoprefixer": "^10.4.0", 17 | "cssnano": "^5.0.12", 18 | "esbuild": "^0.14.2", 19 | "postcss": "^8.4.4", 20 | "postcss-cli": "^9.0.2", 21 | "postcss-import": "^14.0.2", 22 | "tailwindcss": "^3.0.0", 23 | "typescript": "^4.4.4" 24 | }, 25 | "scripts": { 26 | "build": "bin/esbuild", 27 | "build:css": "postcss ./app/assets/stylesheets/application.css -o ./app/assets/builds/application.css", 28 | "ts-check": "tsc --noEmit", 29 | "format:check": "prettier --check app/javascript/ app/assets/stylesheets/", 30 | "format:fix": "prettier --write app/javascript/ app/assets/stylesheets/", 31 | "lint": "eslint app/javascript/" 32 | }, 33 | "devDependencies": { 34 | "@types/alpinejs": "^3.7.0", 35 | "@types/rails__actioncable": "^6.1.6", 36 | "@types/tailwindcss": "^3.0.3", 37 | "@typescript-eslint/eslint-plugin": "^5.6.0", 38 | "@typescript-eslint/parser": "^5.6.0", 39 | "eslint": "^8.4.1", 40 | "prettier": "^2.5.1" 41 | }, 42 | "packageManager": "yarn@3.1.1" 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("tailwindcss/nesting"), 4 | require("tailwindcss"), 5 | require("autoprefixer"), 6 | ...(process.env.NODE_ENV === "production" ? [require("cssnano")] : []), 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/storage/.keep -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require("tailwindcss/colors"); 2 | 3 | /** @type {import("tailwindcss/tailwind-config").TailwindConfig} */ 4 | module.exports = { 5 | content: [ 6 | "./app/views/**/*.html.{haml,erb}", 7 | "./app/helpers/**/*.rb", 8 | "./app/javascript/controllers/*.tsx", 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | heading: ["Outfit", "sans-serif"], 14 | }, 15 | // Fix for a Tailwind 3 change (https://tailwindcss.com/docs/upgrade-guide#removed-color-aliases) 16 | colors: { 17 | green: colors.emerald, 18 | yellow: colors.amber, 19 | purple: colors.violet, 20 | }, 21 | screens: { 22 | xs: "475px", 23 | }, 24 | animation: { 25 | toast: "5600ms forwards toast", 26 | }, 27 | keyframes: { 28 | toast: { 29 | "0%": { opacity: "0", visibility: "visible" }, 30 | "7%": { opacity: "100", visibility: "visible" }, 31 | "93%": { opacity: "100", visibility: "visible" }, 32 | "100%": { opacity: "0", visibility: "hidden" }, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase 4 | # test "connects with cookies" do 5 | # cookies.signed[:user_id] = 42 6 | # 7 | # connect 8 | # 9 | # assert_equal connection.user_id, "42" 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/auth_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AuthControllerTest < ActionDispatch::IntegrationTest 4 | test "should get login" do 5 | get auth_login_url 6 | assert_response :success 7 | end 8 | 9 | test "should get auth" do 10 | get auth_auth_url 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/controllers/authorizations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AuthorizationsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/errors_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ErrorsControllerTest < ActionDispatch::IntegrationTest 4 | test "should get not_found" do 5 | get errors_not_found_url 6 | assert_response :success 7 | end 8 | 9 | test "should get internal_server_error" do 10 | get errors_internal_server_error_url 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/controllers/hovercard_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HovercardControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/integrations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class IntegrationsControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get integrations_index_url 6 | assert_response :success 7 | end 8 | 9 | test "should get show" do 10 | get integrations_show_url 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/controllers/landing_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LandingControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get landing_index_url 6 | assert_response :success 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/controllers/oauth_apps_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class OauthAppsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/oauth_callback_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class OauthCallbackControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/personal_tokens_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PersonalTokensControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/reminders_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RemindersControllerTest < ActionDispatch::IntegrationTest 4 | test "should get index" do 5 | get reminders_index_url 6 | assert_response :success 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/controllers/settings_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SettingsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/signup_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SignupControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/api_tokens.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | oauth_app: one 5 | user: one 6 | token: MyString 7 | scope: MyString 8 | 9 | two: 10 | oauth_app: two 11 | user: two 12 | token: MyString 13 | scope: MyString 14 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/oauth_apps.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | client_id: MyString 6 | client_secret: MyString 7 | redirect_uri: MyString 8 | 9 | two: 10 | user: two 11 | client_id: MyString 12 | client_secret: MyString 13 | redirect_uri: MyString 14 | -------------------------------------------------------------------------------- /test/fixtures/oauth_grants.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | code: MyString 5 | scope: MyString 6 | user: one 7 | oauth_app: one 8 | 9 | two: 10 | code: MyString 11 | scope: MyString 12 | user: two 13 | oauth_app: two 14 | -------------------------------------------------------------------------------- /test/fixtures/password_reset_requests.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | code: MyString 6 | fulfilled: false 7 | expires_at: 2021-12-08 23:18:57 8 | 9 | two: 10 | user: two 11 | code: MyString 12 | fulfilled: false 13 | expires_at: 2021-12-08 23:18:57 14 | -------------------------------------------------------------------------------- /test/fixtures/reminders.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | user: one 5 | title: MyString 6 | 7 | two: 8 | user: two 9 | title: MyString 10 | -------------------------------------------------------------------------------- /test/fixtures/sessions.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | token: MyString 5 | user: one 6 | 7 | two: 8 | token: MyString 9 | user: two 10 | -------------------------------------------------------------------------------- /test/fixtures/signup_requests.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | email: MyString 5 | code: MyString 6 | 7 | two: 8 | email: MyString 9 | code: MyString 10 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | one: 4 | email: MyString 5 | password_digest: MyString 6 | 7 | two: 8 | email: MyString 9 | password_digest: MyString 10 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/integration/.keep -------------------------------------------------------------------------------- /test/jobs/cache_link_unfurl_job_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class CacheLinkUnfurlJobTest < ActiveJob::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/mailers/.keep -------------------------------------------------------------------------------- /test/mailers/previews/user_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/user_mailer 2 | class UserMailerPreview < ActionMailer::Preview 3 | 4 | end 5 | -------------------------------------------------------------------------------- /test/mailers/user_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserMailerTest < ActionMailer::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/models/.keep -------------------------------------------------------------------------------- /test/models/api_token_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApiTokenTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/oauth_app_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class OauthAppTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/oauth_grant_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class OauthGrantTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/password_reset_request_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PasswordResetRequestTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/reminder_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ReminderTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/session_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SessionTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/signup_request_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SignupRequestTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/test/system/.keep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | class ActiveSupport::TestCase 6 | # Run tests in parallel with specified workers 7 | parallelize(workers: :number_of_processors) 8 | 9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/tmp/pids/.keep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["es6", "dom"], 7 | "module": "es6", 8 | "moduleResolution": "node", 9 | "baseUrl": ".", 10 | "paths": { 11 | "*": ["node_modules/*", "app/javascript/*"] 12 | }, 13 | "sourceMap": true, 14 | "target": "es6", 15 | "noEmit": true, 16 | "jsx": "react", 17 | "jsxFactory": "jsxCreateElement", 18 | "strictPropertyInitialization": false, 19 | "strictNullChecks": true 20 | }, 21 | "exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"], 22 | "compileOnSave": false 23 | } 24 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resoluteapp/resolute/2559593003b209392f5d8f5e1ea12c3e66c74c2e/vendor/.keep --------------------------------------------------------------------------------