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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE +21 -0
- data/README.md +130 -0
- data/app/assets/stylesheets/rails_api_keys/ui.css +138 -0
- data/app/controllers/rails_api_keys/ui/api_keys_controller.rb +152 -0
- data/app/controllers/rails_api_keys/ui/application_controller.rb +10 -0
- data/app/helpers/rails_api_keys/ui/api_keys_helper.rb +43 -0
- data/app/javascript/rails_api_keys/ui/controllers/clipboard_controller.js +19 -0
- data/app/javascript/rails_api_keys/ui/controllers/modal_controller.js +43 -0
- data/app/policies/rails_api_keys/ui/api_key_policy.rb +49 -0
- data/app/views/rails_api_keys/ui/api_keys/_form_modal.html.erb +49 -0
- data/app/views/rails_api_keys/ui/api_keys/_list.html.erb +28 -0
- data/app/views/rails_api_keys/ui/api_keys/_panel.html.erb +27 -0
- data/app/views/rails_api_keys/ui/api_keys/_reveal_modal.html.erb +48 -0
- data/app/views/rails_api_keys/ui/api_keys/index.html.erb +5 -0
- data/config/locales/en.yml +42 -0
- data/config/routes.rb +4 -0
- data/lib/generators/rails_api_keys_ui/controllers/controllers_generator.rb +17 -0
- data/lib/generators/rails_api_keys_ui/controllers/templates/api_keys_controller.rb.tt +20 -0
- data/lib/generators/rails_api_keys_ui/install/install_generator.rb +52 -0
- data/lib/generators/rails_api_keys_ui/install/templates/initializer.rb.tt +10 -0
- data/lib/generators/rails_api_keys_ui/install/templates/javascript/clipboard_controller.js +19 -0
- data/lib/generators/rails_api_keys_ui/install/templates/javascript/modal_controller.js +43 -0
- data/lib/generators/rails_api_keys_ui/locales/locales_generator.rb +17 -0
- data/lib/generators/rails_api_keys_ui/views/views_generator.rb +17 -0
- data/lib/rails_api_keys/ui/configuration.rb +24 -0
- data/lib/rails_api_keys/ui/engine.rb +39 -0
- data/lib/rails_api_keys/ui/mount_registry.rb +28 -0
- data/lib/rails_api_keys/ui/route_mapper.rb +55 -0
- data/lib/rails_api_keys/ui/version.rb +7 -0
- data/lib/rails_api_keys/ui.rb +39 -0
- data/lib/rails_api_keys_ui.rb +3 -0
- 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
|
+
[](https://github.com/rubyroidlabs/rails_api_keys_ui/actions/workflows/ci.yml)
|
|
4
|
+
[](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
|