]> gitweb.mndrdr.org Git - arelpe.git/commitdiff
API keys now belong to users and have access dates for auditing; extant keys assigned...
authorAidan Cornelius-Bell <[email protected]>
Thu, 9 Jan 2025 03:17:18 +0000 (13:47 +1030)
committerAidan Cornelius-Bell <[email protected]>
Thu, 9 Jan 2025 03:17:18 +0000 (13:47 +1030)
app/controllers/api/v2/api_controller.rb
app/controllers/api_keys_controller.rb
app/views/api_keys/index.html.erb
app/views/api_keys/new.html.erb
app/views/api_keys/show.html.erb
db/migrate/20250109031618_assign_api_keys_to_first_user.rb [new file with mode: 0644]
db/schema.rb

index 7d5f526bca15ac4a4604d3d88a14a8fd234345c3..f8051a3b723d72582916906ed37a1ffa3390ab88 100644 (file)
@@ -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
index 012a0f52bbcc5eef38b9c37ffe8c5df325711992..f60d0e7a23aaa6031d2e6d03757e33c993fce5c3 100644 (file)
@@ -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
index 72bec46bf80c8c2132cb80572e08a597091c74fc..0a3a87907861c83d10735ba5253713c2fa374c99 100644 (file)
@@ -1,35 +1,37 @@
 <div class="container">
-<h1>API Keys</h1>
-
-<%= link_to 'New API Key', new_api_key_path, class: "button" %>
-<%= link_to 'Home', root_path, class: "button" %>
+  <h1>API Keys</h1>
+  <%= link_to 'New API Key', new_api_key_path, class: "button" %>
+  <%= link_to 'Home', root_path, class: "button" %>
 </div>
-
 <%= render partial: "layouts/alert" %>
-
-
 <div class="post">
-       <div class="container">
-               <table>
-               <thead>
-                       <tr>
-                       <th>Key</th>
-                       <th>Created At</th>
-                       <th>Actions</th>
-                       </tr>
-               </thead>
-               <tbody>
-                       <% @api_keys.each do |api_key| %>
-                       <tr>
-                               <td><%= api_key.key %></td>
-                               <td><%= api_key.created_at %></td>
-                               <td>
-                                 <%= link_to 'View', api_key, class: "button" %>
-                                 <%= button_to 'Destroy', api_key, method: :delete %>
-                               </td>
-                       </tr>
-                       <% end %>
-               </tbody>
-         </table>
-       </div>
+  <div class="container">
+    <table>
+      <thead>
+        <tr>
+          <th>Key</th>
+          <th>User</th>
+          <th>Description</th>
+          <th>Last Used</th>
+          <th>Created</th>
+          <th>Actions</th>
+        </tr>
+      </thead>
+      <tbody>
+        <% @api_keys.each do |api_key| %>
+          <tr>
+            <td><code><%= api_key.key %></code></td>
+            <td><%= api_key.user&.email || 'No user assigned' %></td>
+            <td><%= api_key.description.presence || 'No description' %></td>
+            <td><%= api_key.last_used_at ? time_ago_in_words(api_key.last_used_at) + " ago" : "Never used" %></td>
+            <td><%= time_ago_in_words(api_key.created_at) %> ago</td>
+            <td>
+              <%= link_to 'View', api_key, class: "button" %>
+              <%= button_to 'Destroy', api_key, method: :delete, class: "button danger" %>
+            </td>
+          </tr>
+        <% end %>
+      </tbody>
+    </table>
+  </div>
 </div>
index d9bae94e92afc672f57020a4b907a357af06c1d4..18e9fda7e11948ae8e54332b05ad44b112e975a5 100644 (file)
@@ -1,30 +1,35 @@
 <div class="container">
-       <h1>Generate New API Key</h1>
-       <%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
+  <h1>Generate New API Key</h1>
+  <%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
 </div>
-
 <%= render partial: "layouts/alert" %>
-
 <div class="post">
-       <div class="container">
-               <p>Click the button below to generate a new API key.</p>
+  <div class="container">
+    <%= form_with(model: @api_key, local: true) do |form| %>
+      <% if @api_key.errors.any? %>
+        <div id="error_explanation">
+          <h2><%= pluralize(@api_key.errors.count, "error") %> prohibited this API key from being saved:</h2>
+          <ul>
+            <% @api_key.errors.full_messages.each do |message| %>
+              <li><%= message %></li>
+            <% end %>
+          </ul>
+        </div>
+      <% end %>
 
-               <%= form_with(model: @api_key, local: true) do |form| %>
-               <% if @api_key.errors.any? %>
-                       <div id="error_explanation">
-                       <h2><%= pluralize(@api_key.errors.count, "error") %> prohibited this API key from being saved:</h2>
+      <div class="field">
+        <%= form.label :user_id, "Assign to User (Optional)" %>
+        <%= form.collection_select :user_id, User.all, :id, :email, { include_blank: "No user" } %>
+      </div>
 
-                       <ul>
-                               <% @api_key.errors.full_messages.each do |message| %>
-                               <li><%= message %></li>
-                               <% end %>
-                       </ul>
-                       </div>
-               <% end %>
+      <div class="field">
+        <%= form.label :description, "Description (Optional)" %>
+        <%= form.text_field :description, placeholder: "e.g., API key for mobile app" %>
+      </div>
 
-               <div class="actions">
-                       <%= form.submit "Generate API Key" %>
-               </div>
-               <% end %>
-       </div>
+      <div class="actions">
+        <%= form.submit "Generate API Key" %>
+      </div>
+    <% end %>
+  </div>
 </div>
index 17b741163fadaa4382ae233ae6a3ea7c78f7fb24..996e953e5625ed167ce227e8e44268a837181729 100644 (file)
@@ -1,20 +1,47 @@
 <div class="container">
-       <h1>API Key Details</h1>
-       <%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
+  <h1>API Key Details</h1>
+  <%= link_to 'Back to API Keys', api_keys_path, class: "button" %>
 </div>
-
 <%= render partial: "layouts/alert" %>
-
 <div class="post">
-       <div class="container">
-               <p>
-               <strong>Key:</strong>
-               <%= @api_key.key %>
-               </p>
+  <div class="container">
+    <div class="api-key-details">
+      <h3>Key Information</h3>
+      <p>
+        <strong>API Key:</strong><br>
+        <code><%= @api_key.key %></code>
+      </p>
+
+      <p>
+        <strong>Assigned User:</strong><br>
+        <%= @api_key.user&.email || 'No user assigned' %>
+      </p>
+
+      <p>
+        <strong>Description:</strong><br>
+        <%= @api_key.description.presence || 'No description provided' %>
+      </p>
+
+      <h3>Usage Information</h3>
+      <p>
+        <strong>Created:</strong><br>
+        <%= time_ago_in_words(@api_key.created_at) %> ago
+        (<%= @api_key.created_at.strftime("%B %d, %Y at %I:%M %p") %>)
+      </p>
+
+      <p>
+        <strong>Last Used:</strong><br>
+        <% 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 %>
+      </p>
 
-               <p>
-               <strong>Created at:</strong>
-               <%= @api_key.created_at %>
-               </p>
-       </div>
+      <div class="actions" style="margin-top: 2em;">
+        <%= button_to 'Delete API Key', @api_key, method: :delete, data: { confirm: 'Are you sure you want to delete this API key?' }, class: "button danger" %>
+      </div>
+    </div>
+  </div>
 </div>
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 (file)
index 0000000..3a5ed55
--- /dev/null
@@ -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
index 2482fcdd83ec5ee91e5af1c72f5b4b947ecfb8b1..6593373e831d636dc9d232ca031033a65bf9c8a3 100644 (file)
 #
 # 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