rails_api_keys-ui 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +26 -0
  3. data/LICENSE +21 -0
  4. data/README.md +130 -0
  5. data/app/assets/stylesheets/rails_api_keys/ui.css +138 -0
  6. data/app/controllers/rails_api_keys/ui/api_keys_controller.rb +152 -0
  7. data/app/controllers/rails_api_keys/ui/application_controller.rb +10 -0
  8. data/app/helpers/rails_api_keys/ui/api_keys_helper.rb +43 -0
  9. data/app/javascript/rails_api_keys/ui/controllers/clipboard_controller.js +19 -0
  10. data/app/javascript/rails_api_keys/ui/controllers/modal_controller.js +43 -0
  11. data/app/policies/rails_api_keys/ui/api_key_policy.rb +49 -0
  12. data/app/views/rails_api_keys/ui/api_keys/_form_modal.html.erb +49 -0
  13. data/app/views/rails_api_keys/ui/api_keys/_list.html.erb +28 -0
  14. data/app/views/rails_api_keys/ui/api_keys/_panel.html.erb +27 -0
  15. data/app/views/rails_api_keys/ui/api_keys/_reveal_modal.html.erb +48 -0
  16. data/app/views/rails_api_keys/ui/api_keys/index.html.erb +5 -0
  17. data/config/locales/en.yml +42 -0
  18. data/config/routes.rb +4 -0
  19. data/lib/generators/rails_api_keys_ui/controllers/controllers_generator.rb +17 -0
  20. data/lib/generators/rails_api_keys_ui/controllers/templates/api_keys_controller.rb.tt +20 -0
  21. data/lib/generators/rails_api_keys_ui/install/install_generator.rb +52 -0
  22. data/lib/generators/rails_api_keys_ui/install/templates/initializer.rb.tt +10 -0
  23. data/lib/generators/rails_api_keys_ui/install/templates/javascript/clipboard_controller.js +19 -0
  24. data/lib/generators/rails_api_keys_ui/install/templates/javascript/modal_controller.js +43 -0
  25. data/lib/generators/rails_api_keys_ui/locales/locales_generator.rb +17 -0
  26. data/lib/generators/rails_api_keys_ui/views/views_generator.rb +17 -0
  27. data/lib/rails_api_keys/ui/configuration.rb +24 -0
  28. data/lib/rails_api_keys/ui/engine.rb +39 -0
  29. data/lib/rails_api_keys/ui/mount_registry.rb +28 -0
  30. data/lib/rails_api_keys/ui/route_mapper.rb +55 -0
  31. data/lib/rails_api_keys/ui/version.rb +7 -0
  32. data/lib/rails_api_keys/ui.rb +39 -0
  33. data/lib/rails_api_keys_ui.rb +3 -0
  34. metadata +127 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ab6d1ec68b766d9b9fd979711ff740f3210cdb11e986828c5c83b165112e2046
4
+ data.tar.gz: 59550b7f9f41957e5e3ec1bc6505661bed631430b6f4a248fddc1f1522f8004f
5
+ SHA512:
6
+ metadata.gz: 0af47fee8d5d4207d3890291075b7792b79e503190a4e0f7d50459756828a10a94411a1bac68cff8b293bf9404baf3a87bd8db02ecc41ff47521b80cd57557af
7
+ data.tar.gz: 6c66545baa6c3bf38f43ecb68feb53bd8a607d92c83fc4bc52f98832b695b821cc98abf0cbb886b254402b148cb1b8694cf0b61ee5411299d35222294e449645
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-08-11
11
+
12
+ ### Added
13
+
14
+ - Mountable `rails_api_keys_ui` draw helper (nested or flat mounts, explicit owner vs context)
15
+ - `RailsApiKeys::UI::ApiKeysController` — HTML + JSON `index` / `create` / `destroy`
16
+ - Tailwind-compatible markup + Stimulus defaults (`modal` / `clipboard`) with reveal-once + copy UX
17
+ - Shipped scoped stylesheet (`rails_api_keys/ui.css`) inlined by the panel; `config.include_stylesheet` to opt out
18
+ - CSS variables `--rak-ui-primary-600` / `--rak-ui-primary-700` for brand overrides
19
+ - Embeddable panel helper/partial
20
+ - Generators: `install`, `controllers`, `views`, `locales`
21
+ - Soft Pundit integration (default owner policy; no-op authorize when Pundit is absent)
22
+ - Configurable `current_owner`, `parent_controller`, flash key, `include_stylesheet`
23
+ - HTML redirects via `redirect_back` (fallback: mount index)
24
+
25
+ [Unreleased]: https://github.com/rubyroidlabs/rails_api_keys_ui/compare/v0.1.0...HEAD
26
+ [0.1.0]: https://github.com/rubyroidlabs/rails_api_keys_ui/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rubyroid Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # rails_api_keys-ui
2
+
3
+ [![CI](https://github.com/rubyroidlabs/rails_api_keys-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/rubyroidlabs/rails_api_keys_ui/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/rails_api_keys-ui.svg?icon=si%3Arubygems&icon_color=%23ee3f2d)](https://badge.fury.io/rb/rails_api_keys-ui)
5
+
6
+ Optional HTML/JSON UI for [rails_api_keys](https://github.com/rubyroidlabs/rails_api_keys): create (reveal once + copy), list active keys, revoke. Host apps draw routes for each owner mount.
7
+
8
+ ## What this gem includes
9
+
10
+ - `rails_api_keys_ui` draw helper — nested or flat mounts, explicit owner resolution
11
+ - `RailsApiKeys::UI::ApiKeysController` — HTML + JSON `index` / `create` / `destroy`
12
+ - Tailwind-compatible markup + shipped scoped stylesheet (no host Tailwind required)
13
+ - Embeddable panel partial/helper
14
+ - Generators for install, controllers, views, and locales
15
+
16
+ Depends on `rails_api_keys` for the model/auth layer.
17
+
18
+ ## Installation
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem "rails_api_keys"
23
+ gem "rails_api_keys-ui"
24
+ ```
25
+
26
+ ```bash
27
+ bundle install
28
+ bin/rails generate rails_api_keys:install # if not already
29
+ bin/rails generate rails_api_keys_ui:install
30
+ bin/rails db:migrate
31
+ ```
32
+
33
+ Views expect host Stimulus controllers named `modal` and `clipboard` (install generator can copy examples if missing). The panel inlines a scoped stylesheet by default (`config.include_stylesheet`). Then draw routes.
34
+
35
+ ## Routes
36
+
37
+ ```ruby
38
+ # Keys owned by current_user; nested URL is UI context only
39
+ resources :employees do
40
+ rails_api_keys_ui owner: :current_user, controller: "api_keys"
41
+ end
42
+
43
+ # Keys owned by the Company from params[:company_id]
44
+ resources :companies do
45
+ rails_api_keys_ui owner: :company
46
+ end
47
+
48
+ # Flat mount
49
+ rails_api_keys_ui owner: :current_user, path: "account/api_keys", as: :account_api_keys
50
+ ```
51
+
52
+ Per-mount options: `owner`, `authorize`, `controller`, `path`, `name`, `as`.
53
+
54
+ ## Customization
55
+
56
+ ```bash
57
+ bin/rails generate rails_api_keys_ui:controllers
58
+ bin/rails generate rails_api_keys_ui:views
59
+ bin/rails generate rails_api_keys_ui:locales
60
+ ```
61
+
62
+ Generated controllers subclass `RailsApiKeys::UI::ApiKeysController`. Until you generate views/locales, the gem defaults are used.
63
+
64
+ ## Configuration
65
+
66
+ ```ruby
67
+ # config/initializers/rails_api_keys_ui.rb
68
+ RailsApiKeys::UI.configure do |config|
69
+ # config.parent_controller = "ApplicationController"
70
+ # config.current_owner = ->(controller) { controller.current_user }
71
+ # config.policy_class = "RailsApiKeys::UI::ApiKeyPolicy"
72
+ # config.raw_token_flash_key = :rails_api_keys_raw_token
73
+ # config.include_stylesheet = true
74
+ # config.authorize = ->(controller, action, record) { ... }
75
+ end
76
+ ```
77
+
78
+ Defaults assume Devise `current_user` and Pundit when present. Without Pundit, authorization soft-allows.
79
+
80
+ Brand colors for the shipped stylesheet: set `--rak-ui-primary-600` / `--rak-ui-primary-700` on `.rails-api-keys-ui`, or disable the stylesheet and style the Tailwind-compatible classes yourself.
81
+
82
+ ## Embed in a host page
83
+
84
+ ```erb
85
+ <%= rails_api_keys_ui_panel(
86
+ api_keys: current_user.api_keys.active.order(created_at: :desc),
87
+ create_path: employee_api_keys_path(@employee),
88
+ destroy_path: ->(key) { employee_api_key_path(@employee, key) }
89
+ ) %>
90
+ ```
91
+
92
+ ## JSON
93
+
94
+ - `GET …/api_keys.json` — active keys (no raw tokens)
95
+ - `POST …/api_keys.json` — creates key; response includes `raw_token` once
96
+ - `DELETE …/api_keys/:id.json` — revokes; `204`
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ bundle install
102
+ bundle exec rspec
103
+ bin/rubocop
104
+ ```
105
+
106
+ See [CHANGELOG.md](./CHANGELOG.md). Agent notes: [`AGENTS.md`](./AGENTS.md).
107
+
108
+ ## Contributing
109
+
110
+ Bug reports and pull requests are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) and the [Code of Conduct](./CODE_OF_CONDUCT.md).
111
+
112
+ To report a security vulnerability privately, see [SECURITY.md](./SECURITY.md).
113
+
114
+ ## License
115
+
116
+ This project is licensed under the [MIT License](./LICENSE).
117
+
118
+ ---
119
+
120
+ <p align="center">
121
+ <a href="https://rubyroidlabs.com">
122
+ <img src="./docs/logo.svg" alt="Rubyroid Labs" width="100%" />
123
+ </a>
124
+ </p>
125
+
126
+ **[Rubyroid Labs](https://rubyroidlabs.com)** — full-cycle software development company for businesses delivering scalable web and mobile apps, dedicated developers, and full-cycle teams in Ruby on Rails, React Native, and UX/UI. 98% on-time delivery.
127
+
128
+ - Website: [rubyroidlabs.com](https://rubyroidlabs.com)
129
+ - Email: [hi@rubyroidlabs.com](mailto:hi@rubyroidlabs.com)
130
+ - References: [Clutch](https://clutch.co/profile/rubyroid-labs)
@@ -0,0 +1,138 @@
1
+ /* Prebuilt styles for rails_api_keys-ui views (Tailwind-compatible class names).
2
+ Scoped under .rails-api-keys-ui. Override brand via --rak-ui-primary-*. */
3
+
4
+ .rails-api-keys-ui {
5
+ --rak-ui-primary-600: #2563eb;
6
+ --rak-ui-primary-700: #1d4ed8;
7
+ --rak-ui-gray-50: #f9fafb;
8
+ --rak-ui-gray-200: #e5e7eb;
9
+ --rak-ui-gray-300: #d1d5db;
10
+ --rak-ui-gray-400: #9ca3af;
11
+ --rak-ui-gray-500: #6b7280;
12
+ --rak-ui-gray-700: #374151;
13
+ --rak-ui-gray-900: #111827;
14
+ --rak-ui-red-50: #fef2f2;
15
+ --rak-ui-red-300: #fca5a5;
16
+ --rak-ui-red-700: #b91c1c;
17
+ --rak-ui-amber-50: #fffbeb;
18
+ --rak-ui-amber-200: #fde68a;
19
+ --rak-ui-amber-700: #b45309;
20
+ box-sizing: border-box;
21
+ color: var(--rak-ui-gray-900);
22
+ font-size: 1rem;
23
+ line-height: 1.5;
24
+ }
25
+
26
+ .rails-api-keys-ui *,
27
+ .rails-api-keys-ui *::before,
28
+ .rails-api-keys-ui *::after {
29
+ box-sizing: inherit;
30
+ }
31
+
32
+ .rails-api-keys-ui .hidden { display: none; }
33
+ .rails-api-keys-ui .block { display: block; }
34
+ .rails-api-keys-ui .flex { display: flex; }
35
+ .rails-api-keys-ui .inline-flex { display: inline-flex; }
36
+ .rails-api-keys-ui .flex-col { flex-direction: column; }
37
+ .rails-api-keys-ui .flex-1 { flex: 1 1 0%; }
38
+ .rails-api-keys-ui .items-center { align-items: center; }
39
+ .rails-api-keys-ui .justify-center { justify-content: center; }
40
+ .rails-api-keys-ui .justify-between { justify-content: space-between; }
41
+ .rails-api-keys-ui .justify-end { justify-content: flex-end; }
42
+ .rails-api-keys-ui .gap-2 { gap: 0.5rem; }
43
+ .rails-api-keys-ui .gap-3 { gap: 0.75rem; }
44
+ .rails-api-keys-ui .space-x-3 > :not([hidden]) ~ :not([hidden]) { margin-left: 0.75rem; }
45
+ .rails-api-keys-ui .space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
46
+ .rails-api-keys-ui .space-y-4 > :not([hidden]) ~ :not([hidden]) { margin-top: 1rem; }
47
+
48
+ .rails-api-keys-ui .fixed { position: fixed; }
49
+ .rails-api-keys-ui .relative { position: relative; }
50
+ .rails-api-keys-ui .inset-0 { inset: 0; }
51
+ .rails-api-keys-ui .z-10 { z-index: 10; }
52
+ .rails-api-keys-ui .z-50 { z-index: 50; }
53
+ .rails-api-keys-ui .overflow-y-auto { overflow-y: auto; }
54
+ .rails-api-keys-ui .min-h-screen { min-height: 100vh; }
55
+ .rails-api-keys-ui .w-full { width: 100%; }
56
+ .rails-api-keys-ui .max-w-md { max-width: 28rem; }
57
+
58
+ .rails-api-keys-ui .m-0 { margin: 0; }
59
+ .rails-api-keys-ui .mt-1 { margin-top: 0.25rem; }
60
+ .rails-api-keys-ui .mb-2 { margin-bottom: 0.5rem; }
61
+ .rails-api-keys-ui .mb-4 { margin-bottom: 1rem; }
62
+ .rails-api-keys-ui .mb-6 { margin-bottom: 1.5rem; }
63
+ .rails-api-keys-ui .p-3 { padding: 0.75rem; }
64
+ .rails-api-keys-ui .p-4 { padding: 1rem; }
65
+ .rails-api-keys-ui .p-6 { padding: 1.5rem; }
66
+ .rails-api-keys-ui .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
67
+ .rails-api-keys-ui .px-4 { padding-left: 1rem; padding-right: 1rem; }
68
+ .rails-api-keys-ui .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
69
+ .rails-api-keys-ui .pt-4 { padding-top: 1rem; }
70
+ .rails-api-keys-ui .pt-6 { padding-top: 1.5rem; }
71
+
72
+ .rails-api-keys-ui .rounded-md { border-radius: 0.375rem; }
73
+ .rails-api-keys-ui .rounded-lg { border-radius: 0.5rem; }
74
+ .rails-api-keys-ui .border { border-width: 1px; border-style: solid; }
75
+ .rails-api-keys-ui .border-transparent { border-color: transparent; }
76
+ .rails-api-keys-ui .border-gray-200 { border-color: var(--rak-ui-gray-200); }
77
+ .rails-api-keys-ui .border-gray-300 { border-color: var(--rak-ui-gray-300); }
78
+ .rails-api-keys-ui .border-red-300 { border-color: var(--rak-ui-red-300); }
79
+ .rails-api-keys-ui .border-amber-200 { border-color: var(--rak-ui-amber-200); }
80
+
81
+ .rails-api-keys-ui .bg-white { background-color: #fff; }
82
+ .rails-api-keys-ui .bg-gray-500 { background-color: rgb(107 114 128 / var(--rak-ui-bg-opacity, 1)); }
83
+ .rails-api-keys-ui .bg-opacity-75 { --rak-ui-bg-opacity: 0.75; }
84
+ .rails-api-keys-ui .bg-primary-600 { background-color: var(--rak-ui-primary-600); }
85
+ .rails-api-keys-ui .bg-amber-50 { background-color: var(--rak-ui-amber-50); }
86
+ .rails-api-keys-ui .hover\:bg-primary-700:hover { background-color: var(--rak-ui-primary-700); }
87
+ .rails-api-keys-ui .hover\:bg-gray-50:hover { background-color: var(--rak-ui-gray-50); }
88
+ .rails-api-keys-ui .hover\:bg-red-50:hover { background-color: var(--rak-ui-red-50); }
89
+
90
+ .rails-api-keys-ui .shadow { box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); }
91
+ .rails-api-keys-ui .shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
92
+ .rails-api-keys-ui .shadow-xl { box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); }
93
+ .rails-api-keys-ui .transition-opacity { transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
94
+
95
+ .rails-api-keys-ui .text-xs { font-size: 0.75rem; line-height: 1rem; }
96
+ .rails-api-keys-ui .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
97
+ .rails-api-keys-ui .text-lg { font-size: 1.125rem; line-height: 1.75rem; }
98
+ .rails-api-keys-ui .font-medium { font-weight: 500; }
99
+ .rails-api-keys-ui .font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
100
+ .rails-api-keys-ui .text-white { color: #fff; }
101
+ .rails-api-keys-ui .text-gray-400 { color: var(--rak-ui-gray-400); }
102
+ .rails-api-keys-ui .text-gray-500 { color: var(--rak-ui-gray-500); }
103
+ .rails-api-keys-ui .text-gray-700 { color: var(--rak-ui-gray-700); }
104
+ .rails-api-keys-ui .text-gray-900 { color: var(--rak-ui-gray-900); }
105
+ .rails-api-keys-ui .text-red-700 { color: var(--rak-ui-red-700); }
106
+ .rails-api-keys-ui .text-amber-700 { color: var(--rak-ui-amber-700); }
107
+ .rails-api-keys-ui .text-primary-600 { color: var(--rak-ui-primary-600); }
108
+ .rails-api-keys-ui .hover\:text-gray-500:hover { color: var(--rak-ui-gray-500); }
109
+
110
+ .rails-api-keys-ui .cursor-pointer { cursor: pointer; }
111
+
112
+ .rails-api-keys-ui .form-input {
113
+ appearance: none;
114
+ background-color: #fff;
115
+ border-width: 1px;
116
+ border-style: solid;
117
+ border-color: var(--rak-ui-gray-300);
118
+ border-radius: 0.375rem;
119
+ padding: 0.5rem 0.75rem;
120
+ font-size: 0.875rem;
121
+ line-height: 1.25rem;
122
+ color: var(--rak-ui-gray-900);
123
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
124
+ }
125
+
126
+ .rails-api-keys-ui .form-input:focus {
127
+ outline: 2px solid transparent;
128
+ outline-offset: 2px;
129
+ border-color: var(--rak-ui-primary-600);
130
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--rak-ui-primary-600) 25%, transparent);
131
+ }
132
+
133
+ @media (min-width: 640px) {
134
+ .rails-api-keys-ui .sm\:flex-row { flex-direction: row; }
135
+ .rails-api-keys-ui .sm\:items-start { align-items: flex-start; }
136
+ .rails-api-keys-ui .sm\:items-center { align-items: center; }
137
+ .rails-api-keys-ui .sm\:justify-between { justify-content: space-between; }
138
+ }
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ module UI
5
+ # HTML + JSON API key management (index / create / destroy).
6
+ class ApiKeysController < ApplicationController
7
+ before_action :set_api_key_owner
8
+ before_action :set_api_key, only: :destroy
9
+
10
+ def index
11
+ authorize_api_key!(:index, RailsApiKeys::ApiKey)
12
+ @api_keys = api_keys_scope.order(created_at: :desc)
13
+
14
+ respond_to do |format|
15
+ format.html
16
+ format.json { render json: @api_keys.map { |key| serialize_key(key) } }
17
+ end
18
+ end
19
+
20
+ def create
21
+ authorize_api_key!(:create, RailsApiKeys::ApiKey.new(owner: @api_key_owner))
22
+
23
+ api_key, raw_token = @api_key_owner.create_api_key!(
24
+ name: api_key_params[:name],
25
+ permission: api_key_params[:permission]
26
+ )
27
+
28
+ respond_to do |format|
29
+ format.html do
30
+ flash[raw_token_flash_key] = raw_token
31
+ redirect_back_after_change notice: t(".created")
32
+ end
33
+ format.json do
34
+ render json: serialize_key(api_key).merge(raw_token: raw_token), status: :created
35
+ end
36
+ end
37
+ rescue ActiveRecord::RecordInvalid => e
38
+ respond_to do |format|
39
+ format.html do
40
+ redirect_back_after_change alert: e.record.errors.full_messages.join(", ")
41
+ end
42
+ format.json do
43
+ render json: { errors: e.record.errors.full_messages }, status: :unprocessable_content
44
+ end
45
+ end
46
+ end
47
+
48
+ def destroy
49
+ authorize_api_key!(:destroy, @api_key)
50
+ @api_key.revoke!
51
+
52
+ respond_to do |format|
53
+ format.html do
54
+ redirect_back_after_change notice: t(".revoked")
55
+ end
56
+ format.json { head :no_content }
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def mount_options
63
+ @mount_options ||= RailsApiKeys::UI.mounts.fetch(params[:rails_api_keys_ui_mount])
64
+ end
65
+
66
+ def set_api_key_owner
67
+ @api_key_owner = resolve_owner(mount_options[:owner])
68
+ end
69
+
70
+ def resolve_owner(owner_option)
71
+ case owner_option
72
+ when nil, :current_user, "current_user"
73
+ RailsApiKeys::UI.configuration.current_owner.call(self)
74
+ when Proc
75
+ owner_option.call(self)
76
+ when Symbol, String
77
+ resolve_named_owner(owner_option.to_s)
78
+ else
79
+ raise ArgumentError, "Unsupported rails_api_keys_ui owner: #{owner_option.inspect}"
80
+ end
81
+ end
82
+
83
+ def resolve_named_owner(name)
84
+ param_key = "#{name}_id"
85
+ if params[param_key].present?
86
+ name.camelize.constantize.find(params[param_key])
87
+ elsif respond_to?(name, true)
88
+ send(name)
89
+ else
90
+ raise ArgumentError,
91
+ "Cannot resolve owner #{name.inspect}: missing params[#{param_key}] " \
92
+ "and ##{name}"
93
+ end
94
+ end
95
+
96
+ def set_api_key
97
+ @api_key = api_keys_scope.find(params[:id])
98
+ end
99
+
100
+ def api_keys_scope
101
+ @api_key_owner.api_keys.active
102
+ end
103
+
104
+ def api_key_params
105
+ params.require(:api_key).permit(:name, :permission)
106
+ end
107
+
108
+ def raw_token_flash_key
109
+ RailsApiKeys::UI.configuration.raw_token_flash_key
110
+ end
111
+
112
+ def redirect_back_after_change(**flash_options)
113
+ redirect_back fallback_location: mount_index_path, allow_other_host: false, **flash_options
114
+ end
115
+
116
+ def mount_index_path
117
+ path = request.path
118
+ path = path.sub(%r{/[^/]+\z}, "") if action_name == "destroy"
119
+ path
120
+ end
121
+
122
+ def authorize_api_key!(action, record)
123
+ mount_auth = mount_options[:authorize]
124
+ return mount_auth.call(self, action, record) if mount_auth
125
+
126
+ config_auth = RailsApiKeys::UI.configuration.authorize
127
+ return config_auth.call(self, action, record) if config_auth
128
+
129
+ if defined?(Pundit)
130
+ policy_class = RailsApiKeys::UI.configuration.policy_class.constantize
131
+ query = :"#{action}?"
132
+ user = respond_to?(:pundit_user, true) ? pundit_user : current_user
133
+ Pundit.authorize(user, record, query, policy_class: policy_class)
134
+ else
135
+ true
136
+ end
137
+ end
138
+
139
+ def serialize_key(api_key)
140
+ {
141
+ id: api_key.id,
142
+ name: api_key.name,
143
+ permission: api_key.permission,
144
+ token_display_prefix: api_key.token_display_prefix,
145
+ last_used_at: api_key.last_used_at,
146
+ created_at: api_key.created_at,
147
+ revoked_at: api_key.revoked_at
148
+ }
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ module UI
5
+ # Inherits the host ApplicationController (Devise session, layout, CSRF).
6
+ # Override via +config.parent_controller+ only when generating a host controller.
7
+ class ApplicationController < ::ApplicationController
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ module UI
5
+ module ApiKeysHelper
6
+ # Renders the embeddable API keys panel (list + create/reveal modals).
7
+ #
8
+ # Reads the reveal-once raw token from flash when +raw_token+ is omitted.
9
+ #
10
+ # @param api_keys [Enumerable]
11
+ # @param create_path [String]
12
+ # @param destroy_path [Proc, #call] receives an api_key, returns path
13
+ # @param raw_token [String, nil]
14
+ def rails_api_keys_ui_panel(api_keys:, create_path:, destroy_path:, raw_token: nil)
15
+ render partial: "rails_api_keys/ui/api_keys/panel",
16
+ locals: {
17
+ api_keys: api_keys,
18
+ create_path: create_path,
19
+ destroy_path: destroy_path,
20
+ raw_token: raw_token.nil? ? flash[RailsApiKeys::UI.configuration.raw_token_flash_key] : raw_token
21
+ }
22
+ end
23
+
24
+ # Inlines the gem stylesheet once per render (no asset pipeline required).
25
+ # Disable with +config.include_stylesheet = false+ when the host supplies styles.
26
+ def rails_api_keys_ui_stylesheet
27
+ return "".html_safe unless RailsApiKeys::UI.configuration.include_stylesheet
28
+ return "".html_safe if @_rails_api_keys_ui_stylesheet_rendered
29
+
30
+ @_rails_api_keys_ui_stylesheet_rendered = true
31
+ content_tag(:style, ApiKeysHelper.stylesheet_contents, type: "text/css")
32
+ end
33
+
34
+ def self.stylesheet_path
35
+ Engine.root.join("app/assets/stylesheets/rails_api_keys/ui.css")
36
+ end
37
+
38
+ def self.stylesheet_contents
39
+ @stylesheet_contents ||= File.read(stylesheet_path).html_safe
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["source", "feedback"]
5
+
6
+ async copy() {
7
+ const value = this.sourceTarget.value
8
+ try {
9
+ await navigator.clipboard.writeText(value)
10
+ } catch (_error) {
11
+ this.sourceTarget.select()
12
+ document.execCommand("copy")
13
+ }
14
+
15
+ if (this.hasFeedbackTarget) {
16
+ this.feedbackTarget.classList.remove("hidden")
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,43 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["modal", "backdrop", "panel"]
5
+ static values = { openOnConnect: Boolean }
6
+
7
+ connect() {
8
+ if (this.openOnConnectValue) {
9
+ this.open()
10
+ return
11
+ }
12
+
13
+ if (this.hasModalTarget) {
14
+ this.modalTarget.classList.add("hidden")
15
+ }
16
+ }
17
+
18
+ open() {
19
+ if (this.hasModalTarget) {
20
+ this.modalTarget.classList.remove("hidden")
21
+ document.body.classList.add("overflow-hidden")
22
+ }
23
+ }
24
+
25
+ close() {
26
+ if (this.hasModalTarget) {
27
+ this.modalTarget.classList.add("hidden")
28
+ document.body.classList.remove("overflow-hidden")
29
+ }
30
+ }
31
+
32
+ closeOnBackdropClick(event) {
33
+ if (event.target === this.backdropTarget) {
34
+ this.close()
35
+ }
36
+ }
37
+
38
+ closeOnEscape(event) {
39
+ if (event.key === "Escape") {
40
+ this.close()
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ module UI
5
+ # Default Pundit policy: Devise +current_user+ may manage their own keys.
6
+ class ApiKeyPolicy
7
+ attr_reader :user, :record
8
+
9
+ def initialize(user, record)
10
+ @user = user
11
+ @record = record
12
+ end
13
+
14
+ def index?
15
+ user.present?
16
+ end
17
+
18
+ def create?
19
+ user.present? && owner_matches_user?
20
+ end
21
+
22
+ def destroy?
23
+ user.present? && owner_matches_user?
24
+ end
25
+
26
+ class Scope
27
+ def initialize(user, scope)
28
+ @user = user
29
+ @scope = scope
30
+ end
31
+
32
+ def resolve
33
+ return @scope.none unless @user
34
+
35
+ @scope.where(owner: @user)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def owner_matches_user?
42
+ owner = record.respond_to?(:owner) ? record.owner : nil
43
+ return true if owner.nil?
44
+
45
+ owner == user
46
+ end
47
+ end
48
+ end
49
+ end