From f598ec51990d2ab52a95682d2cd4a07ddc617a79 Mon Sep 17 00:00:00 2001 From: Aidan Cornelius-Bell Date: Thu, 9 Jan 2025 13:47:18 +1030 Subject: [PATCH] API keys now belong to users and have access dates for auditing; extant keys assigned to user 1 --- app/controllers/api/v2/api_controller.rb | 30 +++++---- app/controllers/api_keys_controller.rb | 34 +++++----- app/views/api_keys/index.html.erb | 62 ++++++++++--------- app/views/api_keys/new.html.erb | 49 ++++++++------- app/views/api_keys/show.html.erb | 55 +++++++++++----- ...109031618_assign_api_keys_to_first_user.rb | 40 ++++++++++++ db/schema.rb | 13 +++- 7 files changed, 189 insertions(+), 94 deletions(-) create mode 100644 db/migrate/20250109031618_assign_api_keys_to_first_user.rb diff --git a/app/controllers/api/v2/api_controller.rb b/app/controllers/api/v2/api_controller.rb index 7d5f526..f8051a3 100644 --- a/app/controllers/api/v2/api_controller.rb +++ b/app/controllers/api/v2/api_controller.rb @@ -1,17 +1,25 @@ module Api module V2 - class ApiController < ApplicationController - skip_before_action :verify_authenticity_token - before_action :authenticate_api_key + class ApiController < ApplicationController + skip_before_action :verify_authenticity_token + before_action :authenticate_api_key - private + private - def authenticate_api_key - api_key = request.headers['X-API-Key'] || params[:api_key] - unless ApiKey.exists?(key: api_key) - render json: { errors: [{ status: '401', title: 'Invalid API key' }] }, status: :unauthorized - end - end - end + def authenticate_api_key + api_key = ApiKey.includes(:user).find_by(key: request.headers['X-API-Key']) + + if api_key + api_key.track_usage! + @current_api_user = api_key.user + else + render json: { errors: [{ status: '401', title: 'Invalid API key' }] }, status: :unauthorized + end + end + + def current_api_user + @current_api_user + end + end end end diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb index 012a0f5..f60d0e7 100644 --- a/app/controllers/api_keys_controller.rb +++ b/app/controllers/api_keys_controller.rb @@ -1,40 +1,42 @@ class ApiKeysController < ApplicationController before_action :set_api_key, only: [:show, :destroy] - #before_action :authenticate_user! include AdminAuthenticatable def index - @api_keys = ApiKey.all + @api_keys = ApiKey.includes(:user).all end def show end - + def new - @api_key = ApiKey.new + @api_key = ApiKey.new + @users = User.all end def create - @api_key = ApiKey.new - if @api_key.save - redirect_to @api_key, notice: 'API key was successfully created.' - else - render :new - end + @api_key = ApiKey.new(api_key_params) + + if @api_key.save + redirect_to @api_key, notice: 'API key was successfully created.' + else + @users = User.all + render :new + end end def destroy - @api_key.destroy - redirect_to api_keys_url, notice: 'API key was successfully destroyed.' + @api_key.destroy + redirect_to api_keys_url, notice: 'API key was successfully destroyed.' end private def set_api_key - @api_key = ApiKey.find(params[:id]) + @api_key = ApiKey.includes(:user).find(params[:id]) end - def authorize_admin - redirect_to root_path, alert: 'Not authorized.' unless current_user.admin? + def api_key_params + params.require(:api_key).permit(:user_id, :description) end -end \ No newline at end of file +end diff --git a/app/views/api_keys/index.html.erb b/app/views/api_keys/index.html.erb index 72bec46..0a3a879 100644 --- a/app/views/api_keys/index.html.erb +++ b/app/views/api_keys/index.html.erb @@ -1,35 +1,37 @@
-

API Keys

- -<%= link_to 'New API Key', new_api_key_path, class: "button" %> -<%= link_to 'Home', root_path, class: "button" %> +

API Keys

+ <%= link_to 'New API Key', new_api_key_path, class: "button" %> + <%= link_to 'Home', root_path, class: "button" %>
- <%= render partial: "layouts/alert" %> - -
-
- - - - - - - - - - <% @api_keys.each do |api_key| %> - - - - - - <% end %> - -
KeyCreated AtActions
<%= api_key.key %><%= api_key.created_at %> - <%= link_to 'View', api_key, class: "button" %> - <%= button_to 'Destroy', api_key, method: :delete %> -
-
+
+ + + + + + + + + + + + + <% @api_keys.each do |api_key| %> + + + + + + + + + <% end %> + +
KeyUserDescriptionLast UsedCreatedActions
<%= api_key.key %><%= api_key.user&.email || 'No user assigned' %><%= api_key.description.presence || 'No description' %><%= api_key.last_used_at ? time_ago_in_words(api_key.last_used_at) + " ago" : "Never used" %><%= time_ago_in_words(api_key.created_at) %> ago + <%= link_to 'View', api_key, class: "button" %> + <%= button_to 'Destroy', api_key, method: :delete, class: "button danger" %> +
+
diff --git a/app/views/api_keys/new.html.erb b/app/views/api_keys/new.html.erb index d9bae94..18e9fda 100644 --- a/app/views/api_keys/new.html.erb +++ b/app/views/api_keys/new.html.erb @@ -1,30 +1,35 @@
-

Generate New API Key

- <%= link_to 'Back to API Keys', api_keys_path, class: "button" %> +

Generate New API Key

+ <%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
- <%= render partial: "layouts/alert" %> -
-
-

Click the button below to generate a new API key.

+
+ <%= form_with(model: @api_key, local: true) do |form| %> + <% if @api_key.errors.any? %> +
+

<%= pluralize(@api_key.errors.count, "error") %> prohibited this API key from being saved:

+
    + <% @api_key.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> - <%= form_with(model: @api_key, local: true) do |form| %> - <% if @api_key.errors.any? %> -
-

<%= pluralize(@api_key.errors.count, "error") %> prohibited this API key from being saved:

+
+ <%= form.label :user_id, "Assign to User (Optional)" %> + <%= form.collection_select :user_id, User.all, :id, :email, { include_blank: "No user" } %> +
-
    - <% @api_key.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> +
+ <%= form.label :description, "Description (Optional)" %> + <%= form.text_field :description, placeholder: "e.g., API key for mobile app" %> +
-
- <%= form.submit "Generate API Key" %> -
- <% end %> -
+
+ <%= form.submit "Generate API Key" %> +
+ <% end %> +
diff --git a/app/views/api_keys/show.html.erb b/app/views/api_keys/show.html.erb index 17b7411..996e953 100644 --- a/app/views/api_keys/show.html.erb +++ b/app/views/api_keys/show.html.erb @@ -1,20 +1,47 @@
-

API Key Details

- <%= link_to 'Back to API Keys', api_keys_path, class: "button" %> +

API Key Details

+ <%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
- <%= render partial: "layouts/alert" %> -
-
-

- Key: - <%= @api_key.key %> -

+
+
+

Key Information

+

+ API Key:
+ <%= @api_key.key %> +

+ +

+ Assigned User:
+ <%= @api_key.user&.email || 'No user assigned' %> +

+ +

+ Description:
+ <%= @api_key.description.presence || 'No description provided' %> +

+ +

Usage Information

+

+ Created:
+ <%= time_ago_in_words(@api_key.created_at) %> ago + (<%= @api_key.created_at.strftime("%B %d, %Y at %I:%M %p") %>) +

+ +

+ Last Used:
+ <% if @api_key.last_used_at %> + <%= time_ago_in_words(@api_key.last_used_at) %> ago + (<%= @api_key.last_used_at.strftime("%B %d, %Y at %I:%M %p") %>) + <% else %> + Never used + <% end %> +

-

- Created at: - <%= @api_key.created_at %> -

-
+
+ <%= button_to 'Delete API Key', @api_key, method: :delete, data: { confirm: 'Are you sure you want to delete this API key?' }, class: "button danger" %> +
+
+
diff --git a/db/migrate/20250109031618_assign_api_keys_to_first_user.rb b/db/migrate/20250109031618_assign_api_keys_to_first_user.rb new file mode 100644 index 0000000..3a5ed55 --- /dev/null +++ b/db/migrate/20250109031618_assign_api_keys_to_first_user.rb @@ -0,0 +1,40 @@ +class AssignApiKeysToFirstUser < ActiveRecord::Migration[7.2] + def up + user = User.find_by(id: 1) + + if user + # Store original associations for potential rollback + execute <<-SQL + CREATE TABLE IF NOT EXISTS api_keys_backup ( + id int, + user_id int + ) + SQL + + execute <<-SQL + INSERT INTO api_keys_backup (id, user_id) + SELECT id, user_id FROM api_keys + SQL + + # Update all API keys to belong to user 1 + ApiKey.where(user_id: nil).update_all(user_id: 1) + + puts "Assigned #{ApiKey.where(user_id: 1).count} API keys to user #{user.email}" + else + puts "Warning: User with ID 1 not found. Migration aborted." + raise ActiveRecord::IrreversibleMigration + end + end + + def down + # Restore original associations + execute <<-SQL + UPDATE api_keys + SET user_id = backup.user_id + FROM api_keys_backup backup + WHERE api_keys.id = backup.id + SQL + + execute "DROP TABLE IF EXISTS api_keys_backup" + end +end diff --git a/db/schema.rb b/db/schema.rb index 2482fcd..6593373 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,12 +10,21 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_01_09_015845) do +ActiveRecord::Schema[7.2].define(version: 2025_01_09_031618) do create_table "api_keys", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "key" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id" + t.datetime "last_used_at" + t.string "description" t.index ["key"], name: "index_api_keys_on_key", unique: true + t.index ["user_id"], name: "index_api_keys_on_user_id" + end + + create_table "api_keys_backup", id: false, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.integer "id" + t.integer "user_id" end create_table "pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -82,4 +91,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_01_09_015845) do t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + + add_foreign_key "api_keys", "users" end -- 2.39.5