From 0db914018c45763de2aeb0df7ddf896bab4da653 Mon Sep 17 00:00:00 2001 From: Celina Lopez Date: Wed, 22 Oct 2025 12:04:32 -0700 Subject: [PATCH 1/7] Add API Token interface --- app/controllers/api_tokens_controller.rb | 29 ++++++++++ app/models/api_token.rb | 37 ++++++++++++ app/models/user.rb | 1 + app/views/api_tokens/_form.html.erb | 14 +++++ app/views/api_tokens/_index.html.erb | 40 +++++++++++++ app/views/api_tokens/index.html.erb | 10 ++++ app/views/api_tokens/new.html.erb | 8 +++ app/views/providers/index.html.erb | 28 +++++++-- config/routes.rb | 1 + .../20251022183720_create_api_tokens.rb | 11 ++++ db/schema.rb | 14 ++++- spec/factories/api_tokens.rb | 27 +++++++++ spec/helpers/api_tokens_helper_spec.rb | 15 +++++ spec/models/api_token_spec.rb | 57 +++++++++++++++++++ 14 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 app/controllers/api_tokens_controller.rb create mode 100644 app/models/api_token.rb create mode 100644 app/views/api_tokens/_form.html.erb create mode 100644 app/views/api_tokens/_index.html.erb create mode 100644 app/views/api_tokens/index.html.erb create mode 100644 app/views/api_tokens/new.html.erb create mode 100644 db/migrate/20251022183720_create_api_tokens.rb create mode 100644 spec/factories/api_tokens.rb create mode 100644 spec/helpers/api_tokens_helper_spec.rb create mode 100644 spec/models/api_token_spec.rb diff --git a/app/controllers/api_tokens_controller.rb b/app/controllers/api_tokens_controller.rb new file mode 100644 index 00000000..9275b513 --- /dev/null +++ b/app/controllers/api_tokens_controller.rb @@ -0,0 +1,29 @@ +class ApiTokensController < ApplicationController + def index + end + + def new + @api_token = ApiToken.new(user: current_user) + end + + def create + if ApiToken.create(api_token_params.merge(user: current_user)) + redirect_to api_tokens_path, notice: "API token saved" + else + render "new", status: :unprocessable_entity + end + end + + def destroy + @api_token = current_user.api_tokens.find(params[:id]) + if @api_token.destroy + redirect_to api_tokens_path, notice: "API token deleted" + else + redirect_to api_tokens_path, alert: "Failed to delete API token" + end + end + + def api_token_params + params.require(:api_token).permit(:expires_at) + end +end diff --git a/app/models/api_token.rb b/app/models/api_token.rb new file mode 100644 index 00000000..bdb7e758 --- /dev/null +++ b/app/models/api_token.rb @@ -0,0 +1,37 @@ +# == Schema Information +# +# Table name: api_tokens +# +# id :bigint not null, primary key +# access_token :string not null +# expires_at :datetime +# last_used_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_api_tokens_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +class ApiToken < ApplicationRecord + belongs_to :user + validates :user, :access_token, presence: true + validates :access_token, uniqueness: { scope: :user_id } + + before_validation :generate_token, if: :new_record? + + def generate_token + self.access_token = SecureRandom.hex(16) + end + + def expired? + return false if expires_at.nil? + + expires_at < Time.zone.now + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 4248de9e..2eda0050 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,7 @@ class User < ApplicationRecord has_many :projects, through: :accounts has_many :add_ons, through: :accounts has_many :services, through: :accounts + has_many :api_tokens, dependent: :destroy attr_readonly :admin # has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification" diff --git a/app/views/api_tokens/_form.html.erb b/app/views/api_tokens/_form.html.erb new file mode 100644 index 00000000..e1c4f684 --- /dev/null +++ b/app/views/api_tokens/_form.html.erb @@ -0,0 +1,14 @@ +<%= form_with model: api_token do |form| %> + <%= render "shared/error_messages", resource: form.object %> +
+ + <%= form.datetime_field :expires_at, class: "input input-bordered" %> +
+ + +<% end %> diff --git a/app/views/api_tokens/_index.html.erb b/app/views/api_tokens/_index.html.erb new file mode 100644 index 00000000..29a49944 --- /dev/null +++ b/app/views/api_tokens/_index.html.erb @@ -0,0 +1,40 @@ +
+ + + + + + + + + + + + <% current_user.api_tokens.each do |api_token| %> + + + + + + + <% end %> + +
API TokenLast UsedExpires AtActions
+ <% if api_token.created_at == api_token.updated_at %> +
+ <%= api_token.access_token %> + +
+ (You can only see this once) + <% api_token.touch(:updated_at) %> + <% else %> + <%= api_token.access_token.first(4) %><%= "*" * [8, api_token.access_token.length - 16].min %><%= api_token.access_token.last(4) %> + <% end %> +
+ <%= (api_token.last_used_at.strftime("%b %d, %Y") if api_token.last_used_at) || "Never" %> + + <%= (api_token.expires_at.strftime("%b %d, %Y") if api_token.expires_at) || "Never" %> + + <%= link_to "Delete", api_token, method: :delete, class: "btn btn-error btn-sm" %> +
+
diff --git a/app/views/api_tokens/index.html.erb b/app/views/api_tokens/index.html.erb new file mode 100644 index 00000000..86800f30 --- /dev/null +++ b/app/views/api_tokens/index.html.erb @@ -0,0 +1,10 @@ +<%= settings_layout do %> +

API Tokens

+
+ <%= turbo_frame_tag "api_token" do %> + <%= render "api_tokens/index" %> +
+ <%= link_to "+ API Token", new_api_token_path, class: "btn btn-primary btn-sm" %> +
+ <% end %> +<% end %> diff --git a/app/views/api_tokens/new.html.erb b/app/views/api_tokens/new.html.erb new file mode 100644 index 00000000..09b8b48f --- /dev/null +++ b/app/views/api_tokens/new.html.erb @@ -0,0 +1,8 @@ +<%= settings_layout do %> + <%= turbo_frame_tag "api_token" do %> +
+ Add API Token +
+ <%= render "api_tokens/form", api_token: @api_token %> + <% end %> +<% end %> diff --git a/app/views/providers/index.html.erb b/app/views/providers/index.html.erb index 3a1e5197..fdf67cae 100644 --- a/app/views/providers/index.html.erb +++ b/app/views/providers/index.html.erb @@ -4,10 +4,22 @@ <%= turbo_frame_tag "provider" do %> <%= render "providers/index" %>
- <%= link_to "+ Github Credentials", new_provider_path(provider_type: Provider::GITHUB_PROVIDER), class: "btn btn-primary btn-sm" %> - <%= link_to "+ Gitlab Credentials", new_provider_path(provider_type: Provider::GITLAB_PROVIDER), class: "btn btn-primary btn-sm" %> - <%= link_to "+ Container Registry Credentials", new_provider_path(provider_type: Provider::CUSTOM_REGISTRY_PROVIDER), class: "btn btn-primary btn-sm" %> - + <% if current_account.stack_manager&.stack&.provides_registries? %> <%= button_to sync_registries_stack_manager_path, class: "btn btn-outline btn-sm" do %> @@ -16,4 +28,10 @@ <% end %>
<% end %> -<% end %> \ No newline at end of file + <%= turbo_frame_tag "api_token" do %> + <%= render "api_tokens/index" %> +
+ <%= link_to "+ API Token", new_api_token_path, class: "btn btn-primary btn-sm" %> +
+ <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 8a58662a..9f4ada1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,6 +62,7 @@ Rails.application.routes.draw do end resources :providers, only: %i[index new create destroy] + resources :api_tokens, only: %i[index new create destroy] resources :projects do member do post :restart diff --git a/db/migrate/20251022183720_create_api_tokens.rb b/db/migrate/20251022183720_create_api_tokens.rb new file mode 100644 index 00000000..919c07a8 --- /dev/null +++ b/db/migrate/20251022183720_create_api_tokens.rb @@ -0,0 +1,11 @@ +class CreateApiTokens < ActiveRecord::Migration[7.2] + def change + create_table :api_tokens do |t| + t.references :user, null: false, foreign_key: true + t.string :access_token, null: false + t.datetime :last_used_at + t.datetime :expires_at + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index dbc7bd77..8bd41033 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_10_09_003742) do +ActiveRecord::Schema[7.2].define(version: 2025_10_22_183720) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -84,6 +84,17 @@ ActiveRecord::Schema[7.2].define(version: 2025_10_09_003742) do t.datetime "updated_at", null: false end + create_table "api_tokens", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "access_token", null: false + t.datetime "last_used_at" + t.datetime "expires_at" + t.jsonb "scopes", default: [], null: false, array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_api_tokens_on_user_id" + end + create_table "build_clouds", force: :cascade do |t| t.bigint "cluster_id", null: false t.string "namespace", default: "canine-k8s-builder", null: false @@ -491,6 +502,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_10_09_003742) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "add_ons", "clusters" + add_foreign_key "api_tokens", "users" add_foreign_key "build_clouds", "clusters" add_foreign_key "build_configurations", "build_clouds" add_foreign_key "build_configurations", "projects" diff --git a/spec/factories/api_tokens.rb b/spec/factories/api_tokens.rb new file mode 100644 index 00000000..c5cceaa4 --- /dev/null +++ b/spec/factories/api_tokens.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: api_tokens +# +# id :bigint not null, primary key +# access_token :string not null +# expires_at :datetime +# last_used_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_api_tokens_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +FactoryBot.define do + factory :api_token do + user + expires_at { nil } + last_used_at { nil } + end +end diff --git a/spec/helpers/api_tokens_helper_spec.rb b/spec/helpers/api_tokens_helper_spec.rb new file mode 100644 index 00000000..c73a967c --- /dev/null +++ b/spec/helpers/api_tokens_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the ApiTokensHelper. For example: +# +# describe ApiTokensHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe ApiTokensHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/api_token_spec.rb b/spec/models/api_token_spec.rb new file mode 100644 index 00000000..1d534233 --- /dev/null +++ b/spec/models/api_token_spec.rb @@ -0,0 +1,57 @@ +# == Schema Information +# +# Table name: api_tokens +# +# id :bigint not null, primary key +# access_token :string not null +# expires_at :datetime +# last_used_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_api_tokens_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# +require 'rails_helper' + +RSpec.describe ApiToken, type: :model do + context "#generate_token" do + it "generates a random access token on create" do + api_token = create(:api_token) + expect(api_token.access_token).not_to be_nil + end + end + + context "#expired?" do + let(:api_token) { create(:api_token, expires_at:) } + let(:expires_at) { nil } + + context "when expires_at is nil" do + it "returns false" do + expect(api_token.expired?).to be_falsey + end + end + + context "when expires_at is in the future" do + let(:expires_at) { 1.day.from_now } + + it "returns false" do + expect(api_token.expired?).to be_falsey + end + end + + context "when expires_at is in the past" do + let(:expires_at) { 1.day.ago } + + it "returns true" do + expect(api_token.expired?).to be_truthy + end + end + end +end From ef6a83642b9a6ff00fa619cfd68c4b71f324a1c0 Mon Sep 17 00:00:00 2001 From: Celina Lopez Date: Wed, 22 Oct 2025 16:03:36 -0700 Subject: [PATCH 2/7] add docs --- Gemfile | 68 +++++------ Gemfile.lock | 13 +++ app/controllers/api_tokens_controller.rb | 3 - app/controllers/application_controller.rb | 16 +++ app/controllers/docs_controller.rb | 12 ++ app/views/api_tokens/_index.html.erb | 2 +- app/views/docs/docs.html.erb | 10 ++ config/initializers/rswag_api.rb | 13 +++ config/routes.rb | 3 + spec/helpers/api_tokens_helper_spec.rb | 15 --- spec/integration/api/models/build.rb | 0 spec/integration/api/models/project.rb | 44 +++++++ spec/integration/api/models/projects.rb | 6 + spec/integration/api/swagger_schemas.rb | 19 ++++ spec/integration/api/swagger_schemas_spec.rb | 10 ++ spec/integration/api/v1/deployments_spec.rb | 21 ++++ spec/integration/api/v1/projects_spec.rb | 49 ++++++++ spec/rails_helper.rb | 2 + spec/swagger_helper.rb | 57 ++++++++++ swagger/v1/swagger.yaml | 114 +++++++++++++++++++ 20 files changed, 418 insertions(+), 59 deletions(-) create mode 100644 app/controllers/docs_controller.rb create mode 100644 app/views/docs/docs.html.erb create mode 100644 config/initializers/rswag_api.rb delete mode 100644 spec/helpers/api_tokens_helper_spec.rb create mode 100644 spec/integration/api/models/build.rb create mode 100644 spec/integration/api/models/project.rb create mode 100644 spec/integration/api/models/projects.rb create mode 100644 spec/integration/api/swagger_schemas.rb create mode 100644 spec/integration/api/swagger_schemas_spec.rb create mode 100644 spec/integration/api/v1/deployments_spec.rb create mode 100644 spec/integration/api/v1/projects_spec.rb create mode 100644 spec/swagger_helper.rb create mode 100644 swagger/v1/swagger.yaml diff --git a/Gemfile b/Gemfile index b595e0a1..fdccffdf 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ group :development, :test do gem "simplecov", require: false gem "pry", "~> 0.15.2" gem "rspec-rails", "~> 8.0" + gem 'rswag-specs' gem "factory_bot_rails" gem "webmock" @@ -59,57 +60,44 @@ group :test do gem 'shoulda-matchers', '~> 6.5' end +gem "actioncable-enhanced-postgresql-adapter", "~> 1.0" +gem "annotate", "~> 3.2" +gem "avo", "~> 3.25" +gem "cron2english", "~> 0.1.7" gem "cssbundling-rails" gem "devise", "~> 4.9" -gem "friendly_id", "~> 5.4" -gem "jsbundling-rails" -gem "name_of_person", github: "basecamp/name_of_person" -gem "noticed", "~> 2.9" -gem "omniauth-github", "~> 2.0" -gem "pretender", "~> 0.6.0" -gem "pundit", "~> 2.5" -gem "good_job", "~> 4.12" -gem "sitemap_generator", "~> 6.1" -gem "responders", github: "heartcombo/responders", branch: "main" gem "dotenv", "~> 3.1" +gem "friendly_id", "~> 5.4" +gem "good_job", "~> 4.12" +gem "httparty", "~> 0.23.2" gem "image_processing", "~> 1.13" +gem "jsbundling-rails" gem "k8s-ruby", "~> 0.17.2" gem "kubeclient", "~> 4.12" gem "light-service", "~> 0.20.0" +gem "name_of_person", github: "basecamp/name_of_person" +gem "noticed", "~> 2.9" gem "octokit", "~> 10.0" -gem "omniauth-digitalocean", "~> 0.3.2" -gem "pagy", "~> 9.4" gem "oj", "~> 3.16" gem "omniauth", "~> 2.1" -gem "omniauth-rails_csrf_protection", "~> 1.0" - -gem "annotate", "~> 3.2" - -gem "rack", "~> 3.1" - -gem "tailwindcss-rails", "~> 2.7" - -gem "httparty", "~> 0.23.2" - -gem "redcarpet", "~> 3.6" - -gem "rubyzip", "~> 3.2" - -gem "sassc-rails", "~> 2.1" - -gem "cron2english", "~> 0.1.7" - -gem "avo", "~> 3.25" - -gem "sentry-ruby", "~> 5.28" -gem "sentry-rails", "~> 5.27" - -gem "sys-proctable", "~> 1.3" - +gem "omniauth-digitalocean", "~> 0.3.2" +gem "omniauth-github", "~> 2.0" gem "omniauth-gitlab", "~> 4.1" - -gem "actioncable-enhanced-postgresql-adapter", "~> 1.0" - +gem "omniauth-rails_csrf_protection", "~> 1.0" +gem "pagy", "~> 9.4" +gem "pretender", "~> 0.6.0" +gem "pundit", "~> 2.5" +gem "rack", "~> 3.1" +gem "redcarpet", "~> 3.6" +gem 'rswag-api', '~> 2.10', '>= 2.10.1' +gem "responders", github: "heartcombo/responders", branch: "main" +gem "rubyzip", "~> 3.2" +gem "sassc-rails", "~> 2.1" +gem "sentry-rails", "~> 5.27" +gem "sentry-ruby", "~> 5.28" +gem "sitemap_generator", "~> 6.1" +gem "sys-proctable", "~> 1.3" +gem "tailwindcss-rails", "~> 2.7" gem 'flipper', '~> 1.2.2' gem 'flipper-active_record', '~> 1.2.2' gem 'flipper-ui', '~> 1.2.2' diff --git a/Gemfile.lock b/Gemfile.lock index b5d4a82f..a9ff1ae5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,6 +284,9 @@ GEM jsbundling-rails (1.3.1) railties (>= 6.0.0) json (2.13.2) + json-schema (5.2.2) + addressable (~> 2.8) + bigdecimal (~> 3.1) jsonpath (1.1.5) multi_json jwt (2.9.1) @@ -506,6 +509,14 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.4) + rswag-api (2.16.0) + activesupport (>= 5.2, < 8.1) + railties (>= 5.2, < 8.1) + rswag-specs (2.16.0) + activesupport (>= 5.2, < 8.1) + json-schema (>= 2.2, < 6.0) + railties (>= 5.2, < 8.1) + rspec-core (>= 2.14) rubocop (1.79.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -694,6 +705,8 @@ DEPENDENCIES redcarpet (~> 3.6) responders! rspec-rails (~> 8.0) + rswag-api (~> 2.10, >= 2.10.1) + rswag-specs rubocop-rails-omakase rubyzip (~> 3.2) sassc-rails (~> 2.1) diff --git a/app/controllers/api_tokens_controller.rb b/app/controllers/api_tokens_controller.rb index 9275b513..43d9a3e1 100644 --- a/app/controllers/api_tokens_controller.rb +++ b/app/controllers/api_tokens_controller.rb @@ -1,7 +1,4 @@ class ApiTokensController < ApplicationController - def index - end - def new @api_token = ApiToken.new(user: current_user) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 77580c27..8dd6c76c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,6 +14,22 @@ class ApplicationController < ActionController::Base rescue_from ActiveRecord::RecordNotFound, with: :record_not_found + def authenticate_user! + if request.headers["X-API-Key"].present? + authenticate_with_api_token! + else + super + end + end + + def authenticate_with_api_token! + token = request.headers["X-API-Key"] + api_token = ApiToken.find_by(access_token: token) + if api_token.present? + @current_user = api_token.user + end + end + protected def current_account return nil unless user_signed_in? diff --git a/app/controllers/docs_controller.rb b/app/controllers/docs_controller.rb new file mode 100644 index 00000000..f471f7a2 --- /dev/null +++ b/app/controllers/docs_controller.rb @@ -0,0 +1,12 @@ +class DocsController < ApplicationController + skip_before_action :authenticate_user!, only: [ :swagger, :docs ] + + layout false + + def swagger + render plain: File.read(Rails.root.join('swagger', 'v1', 'swagger.yaml')) + end + + def index + end +end diff --git a/app/views/api_tokens/_index.html.erb b/app/views/api_tokens/_index.html.erb index 29a49944..b74fd23c 100644 --- a/app/views/api_tokens/_index.html.erb +++ b/app/views/api_tokens/_index.html.erb @@ -10,7 +10,7 @@ - <% current_user.api_tokens.each do |api_token| %> + <% current_user.api_tokens.order(created_at: :desc).each do |api_token| %> <% if api_token.created_at == api_token.updated_at %> diff --git a/app/views/docs/docs.html.erb b/app/views/docs/docs.html.erb new file mode 100644 index 00000000..13811a5f --- /dev/null +++ b/app/views/docs/docs.html.erb @@ -0,0 +1,10 @@ + + + Canine API Documentation + + + + + + diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 00000000..c0196bd2 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,13 @@ +Rswag::Api.configure do |c| + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.openapi_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/routes.rb b/config/routes.rb index 9f4ada1a..80526866 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -138,6 +138,9 @@ Rails.application.routes.draw do get "manifest" => "rails/pwa#manifest", as: :pwa_manifest get "async_render" => "async_renderer#async_render" + get "/api-docs", to: "docs#index" + get "/swagger", to: "docs#swagger" + get "/calculator", to: "static#calculator" # Public marketing homepage if Rails.application.config.local_mode diff --git a/spec/helpers/api_tokens_helper_spec.rb b/spec/helpers/api_tokens_helper_spec.rb deleted file mode 100644 index c73a967c..00000000 --- a/spec/helpers/api_tokens_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the ApiTokensHelper. For example: -# -# describe ApiTokensHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe ApiTokensHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/integration/api/models/build.rb b/spec/integration/api/models/build.rb new file mode 100644 index 00000000..e69de29b diff --git a/spec/integration/api/models/project.rb b/spec/integration/api/models/project.rb new file mode 100644 index 00000000..aed1e6dd --- /dev/null +++ b/spec/integration/api/models/project.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +SwaggerSchemas::PROJECT = { + type: :object, + required: %w[id name repository_url branch cluster_id subfolder updated_at created_at], + properties: { + id: { + type: :integer, + example: '1' + }, + name: { + type: :string, + example: 'example-project' + }, + repository_url: { + type: :string, + example: 'https://github.com/example/example-project' + }, + branch: { + type: :string, + example: 'main' + }, + cluster_id: { + type: :integer, + example: '1' + }, + subfolder: { + type: :string, + example: 'example-subfolder' + }, + updated_at: { + type: :string, + example: '2021-01-01T00:00:00Z' + }, + created_at: { + type: :string, + example: '2021-01-01T00:00:00Z' + }, + url: { + type: :string, + example: 'https://example.com/projects/1' + } + } +}.freeze diff --git a/spec/integration/api/models/projects.rb b/spec/integration/api/models/projects.rb new file mode 100644 index 00000000..692b041a --- /dev/null +++ b/spec/integration/api/models/projects.rb @@ -0,0 +1,6 @@ +SwaggerSchemas::PROJECTS = { + type: :array, + items: { + '$ref' => '#/components/schemas/project' + } +}.freeze diff --git a/spec/integration/api/swagger_schemas.rb b/spec/integration/api/swagger_schemas.rb new file mode 100644 index 00000000..10366815 --- /dev/null +++ b/spec/integration/api/swagger_schemas.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module SwaggerSchemas + def self.schemas + constants.to_h do |constant| + [ constant.to_s.downcase, const_get(constant) ] + end + end + + def self.validate! + schemas.each do |schema_name, schema| + next unless const_defined?(schema_name) + end + end +end + +Dir["#{File.dirname(__FILE__)}/models/**/*.rb"].each do |file| + require file +end diff --git a/spec/integration/api/swagger_schemas_spec.rb b/spec/integration/api/swagger_schemas_spec.rb new file mode 100644 index 00000000..205796e7 --- /dev/null +++ b/spec/integration/api/swagger_schemas_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'rails_helper' +require './spec/integration/api/swagger_schemas' + +RSpec.describe SwaggerSchemas do + it 'matches' do + expect { described_class.validate! }.not_to raise_error + end +end diff --git a/spec/integration/api/v1/deployments_spec.rb b/spec/integration/api/v1/deployments_spec.rb new file mode 100644 index 00000000..5122a09d --- /dev/null +++ b/spec/integration/api/v1/deployments_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'swagger_helper' + +RSpec.describe Projects::DeploymentsController, :swagger, type: :request do + include ApplicationHelper + let(:api_token) { create :api_token } + let(:'X-API-Key') { api_token.access_token } + path '/projects/{project_id}/deployments/deploy' do + post('Deploy Project') do + tags 'Deployments' + operationId 'deployProject' + produces 'application/json' + security [ x_api_key: [] ] + + response(200, 'successful') do + run_test! + end + end + end +end diff --git a/spec/integration/api/v1/projects_spec.rb b/spec/integration/api/v1/projects_spec.rb new file mode 100644 index 00000000..5a3b5bcf --- /dev/null +++ b/spec/integration/api/v1/projects_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'swagger_helper' + +RSpec.describe ProjectsController, :swagger, type: :request do + include ApplicationHelper + let(:api_token) { create :api_token } + let(:'X-API-Key') { api_token.access_token } + path '/projects' do + get('List Projects') do + tags 'Projects' + operationId 'listProjects' + produces 'application/json' + security [ x_api_key: [] ] + + response(200, 'successful') do + schema '$ref' => '#/components/schemas/projects' + run_test! + end + end + end + + path '/projects/{id}' do + get('Show Project') do + tags 'Projects' + operationId 'showProject' + produces 'application/json' + security [ x_api_key: [] ] + + response(200, 'successful') do + schema '$ref' => '#/components/schemas/project' + run_test! + end + end + end + + path '/projects/{id}/restart' do + post('Restart Project') do + tags 'Projects' + operationId 'restartProject' + produces 'application/json' + security [ x_api_key: [] ] + + response(200, 'successful') do + run_test! + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index ba38704a..9c42defc 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -57,6 +57,8 @@ RSpec.configure do |config| # instead of true. config.use_transactional_fixtures = false + + config.swagger_dry_run = false # You can uncomment this line to turn off ActiveRecord support entirely. # config.use_active_record = false diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 00000000..f08e5379 --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'integration/api/swagger_schemas' + +servers = [ + { + url: 'https://canine.sh', + description: 'Production' + } +] + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.swagger_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under swagger_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a swagger_doc tag to the + # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' + config.swagger_docs = { + 'v1/swagger.yaml' => { + openapi: '3.0.1', + info: { + title: 'Canine API V1', + version: 'v1', + description: "Canine API" + }, + components: { + schemas: SwaggerSchemas.schemas, + securitySchemes: { + x_api_key: { + type: :apiKey, + name: 'X-API-Key', + in: :header + } + } + }, + security: [ + { x_api_key: [] } + ], + paths: {}, + servers: + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml + config.swagger_strict_schema_validation = true +end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml new file mode 100644 index 00000000..8b00dda4 --- /dev/null +++ b/swagger/v1/swagger.yaml @@ -0,0 +1,114 @@ +--- +openapi: 3.0.1 +info: + title: Canine API V1 + version: v1 + description: Canine API +components: + schemas: + projects: + type: array + items: + "$ref": "#/components/schemas/project" + project: + type: object + required: + - id + - name + - repository_url + - branch + - cluster_id + - subfolder + - updated_at + - created_at + properties: + id: + type: integer + example: '1' + name: + type: string + example: example-project + repository_url: + type: string + example: https://github.com/example/example-project + branch: + type: string + example: main + cluster_id: + type: integer + example: '1' + subfolder: + type: string + example: example-subfolder + updated_at: + type: string + example: '2021-01-01T00:00:00Z' + created_at: + type: string + example: '2021-01-01T00:00:00Z' + url: + type: string + example: https://example.com/projects/1 + securitySchemes: + x_api_key: + type: apiKey + name: X-API-Key + in: header +security: +- x_api_key: [] +paths: + "/projects/{project_id}/deployments/deploy": + post: + summary: Deploy Project + tags: + - Deployments + operationId: deployProject + security: + - x_api_key: [] + responses: + '200': + description: successful + "/projects": + get: + summary: List Projects + tags: + - Projects + operationId: listProjects + security: + - x_api_key: [] + responses: + '200': + description: successful + content: + application/json: + schema: + "$ref": "#/components/schemas/projects" + "/projects/{id}": + get: + summary: Show Project + tags: + - Projects + operationId: showProject + security: + - x_api_key: [] + responses: + '200': + description: successful + content: + application/json: + schema: + "$ref": "#/components/schemas/project" + "/projects/{id}/restart": + post: + summary: Restart Project + tags: + - Projects + operationId: restartProject + security: + - x_api_key: [] + responses: + '200': + description: successful +servers: +- url: https://canine.sh + description: Production From 14f121687002bb31d9cf32760feeee8f55ac9553 Mon Sep 17 00:00:00 2001 From: Celina Lopez Date: Thu, 23 Oct 2025 13:19:24 -0700 Subject: [PATCH 3/7] fix specs --- app/actions/projects/deploy_latest_commit.rb | 1 - app/controllers/api_tokens_controller.rb | 2 + .../projects/deployments_controller.rb | 12 ++-- app/controllers/projects_controller.rb | 13 +++-- config/environments/test.rb | 1 + spec/integration/api/models/build.rb | 0 spec/integration/api/swagger_schemas.rb | 2 +- spec/integration/api/v1/deployments_spec.rb | 14 ++++- spec/integration/api/v1/projects_spec.rb | 27 ++++----- spec/rails_helper.rb | 2 +- spec/swagger_helper.rb | 6 +- swagger/v1/swagger.yaml | 57 +++++++++++-------- 12 files changed, 83 insertions(+), 54 deletions(-) delete mode 100644 spec/integration/api/models/build.rb diff --git a/app/actions/projects/deploy_latest_commit.rb b/app/actions/projects/deploy_latest_commit.rb index 9800d503..b9a445a3 100644 --- a/app/actions/projects/deploy_latest_commit.rb +++ b/app/actions/projects/deploy_latest_commit.rb @@ -11,7 +11,6 @@ class Projects::DeployLatestCommit project = context.project current_user = context.current_user || project.account.owner if project.git? - project_credential_provider = project.project_credential_provider client = Git::Client.from_project(project) commit = client.commits(project.branch).first build = project.builds.create!( diff --git a/app/controllers/api_tokens_controller.rb b/app/controllers/api_tokens_controller.rb index 43d9a3e1..417f3bc7 100644 --- a/app/controllers/api_tokens_controller.rb +++ b/app/controllers/api_tokens_controller.rb @@ -20,6 +20,8 @@ class ApiTokensController < ApplicationController end end + private + def api_token_params params.require(:api_token).permit(:expires_at) end diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index 739df629..9499bd80 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -27,10 +27,14 @@ class Projects::DeploymentsController < Projects::BaseController current_user:, skip_build: params[:skip_build] == "true" ) - if result.success? - redirect_to @project, notice: "Deploying project #{@project.name}. Follow deployment".html_safe - else - redirect_to @project, alert: "Failed to deploy project" + respond_to do |format| + if result.success? + format.html { redirect_to @project, notice: "Deploying project #{@project.name}. Follow deployment".html_safe } + format.json { render json: { message: "Deploying project #{@project.name}." }, status: :ok } + else + format.html { redirect_to @project, alert: "Failed to deploy project" } + format.json { render json: { message: "Failed to deploy project" }, status: :unprocessable_entity } + end end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 1ec2972a..1e9cbf9f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,14 +10,19 @@ class ProjectsController < ApplicationController # Uncomment to authorize with Pundit # authorize @projects + # end def restart result = Projects::Restart.execute(connection: K8::Connection.new(@project, current_user)) - if result.success? - redirect_to project_url(@project), notice: "All services have been restarted" - else - redirect_to project_url(@project), alert: "Failed to restart all services" + respond_to do |format| + if result.success? + format.html { redirect_to project_url(@project), notice: "All services have been restarted" } + format.json { render json: { message: "All services have been restarted" }, status: :ok } + else + format.html { redirect_to project_url(@project), alert: "Failed to restart all services" } + format.json { render json: { message: "Failed to restart all services" }, status: :unprocessable_entity } + end end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 816ad487..ee1ba2ab 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -58,6 +58,7 @@ Rails.application.configure do # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true + config.hosts = nil # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true diff --git a/spec/integration/api/models/build.rb b/spec/integration/api/models/build.rb deleted file mode 100644 index e69de29b..00000000 diff --git a/spec/integration/api/swagger_schemas.rb b/spec/integration/api/swagger_schemas.rb index 10366815..cea36a82 100644 --- a/spec/integration/api/swagger_schemas.rb +++ b/spec/integration/api/swagger_schemas.rb @@ -9,7 +9,7 @@ module SwaggerSchemas def self.validate! schemas.each do |schema_name, schema| - next unless const_defined?(schema_name) + next unless const_defined?(schema_name.upcase) end end end diff --git a/spec/integration/api/v1/deployments_spec.rb b/spec/integration/api/v1/deployments_spec.rb index 5122a09d..9e58c3f6 100644 --- a/spec/integration/api/v1/deployments_spec.rb +++ b/spec/integration/api/v1/deployments_spec.rb @@ -4,14 +4,24 @@ require 'swagger_helper' RSpec.describe Projects::DeploymentsController, :swagger, type: :request do include ApplicationHelper - let(:api_token) { create :api_token } + let(:api_token) { create :api_token, user: } let(:'X-API-Key') { api_token.access_token } + let(:account) { create :account } + let(:user) { create :user } + let!(:account_user) { create :account_user, account:, user: } + let!(:cluster) { create :cluster, account: } + let(:project) { create :project, :container_registry, cluster:, account: } + path '/projects/{project_id}/deployments/deploy' do + let(:project_id) { project.id } + post('Deploy Project') do tags 'Deployments' operationId 'deployProject' + consumes 'application/json' produces 'application/json' - security [ x_api_key: [] ] + parameter name: 'X-API-Key', in: :header, type: :string, description: 'API Key' + parameter name: :project_id, in: :path, type: :integer, description: 'Project ID' response(200, 'successful') do run_test! diff --git a/spec/integration/api/v1/projects_spec.rb b/spec/integration/api/v1/projects_spec.rb index 5a3b5bcf..3c04da83 100644 --- a/spec/integration/api/v1/projects_spec.rb +++ b/spec/integration/api/v1/projects_spec.rb @@ -6,12 +6,18 @@ RSpec.describe ProjectsController, :swagger, type: :request do include ApplicationHelper let(:api_token) { create :api_token } let(:'X-API-Key') { api_token.access_token } + let(:account) { create :account } + let(:project) { create :project, account: } + before do + api_token.user.accounts << account + end path '/projects' do get('List Projects') do tags 'Projects' operationId 'listProjects' + produces 'application/json' - security [ x_api_key: [] ] + parameter name: 'X-API-Key', in: :header, type: :string, description: 'API Key' response(200, 'successful') do schema '$ref' => '#/components/schemas/projects' @@ -20,26 +26,17 @@ RSpec.describe ProjectsController, :swagger, type: :request do end end - path '/projects/{id}' do - get('Show Project') do - tags 'Projects' - operationId 'showProject' - produces 'application/json' - security [ x_api_key: [] ] - - response(200, 'successful') do - schema '$ref' => '#/components/schemas/project' - run_test! - end - end - end - path '/projects/{id}/restart' do + let(:id) { project.id } post('Restart Project') do tags 'Projects' operationId 'restartProject' + consumes 'application/json' produces 'application/json' security [ x_api_key: [] ] + parameter name: :id, in: :path, type: :integer, description: 'Project ID' + parameter name: 'X-API-Key', in: :header, type: :string, description: 'API Key' + response(200, 'successful') do run_test! diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9c42defc..4d2ac00f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -58,7 +58,7 @@ RSpec.configure do |config| config.use_transactional_fixtures = false - config.swagger_dry_run = false + config.rswag_dry_run = false # You can uncomment this line to turn off ActiveRecord support entirely. # config.use_active_record = false diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index f08e5379..c3ac6676 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -14,7 +14,7 @@ RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated # NOTE: If you're using the rswag-api to serve API descriptions, you'll need # to ensure that it's configured to serve Swagger from the same folder - config.swagger_root = Rails.root.join('swagger').to_s + config.openapi_root = Rails.root.join('swagger').to_s # Define one or more Swagger documents and provide global metadata for each one # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will @@ -22,7 +22,7 @@ RSpec.configure do |config| # By default, the operations defined in spec files are added to the first # document below. You can override this behavior by adding a swagger_doc tag to the # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' - config.swagger_docs = { + config.openapi_specs = { 'v1/swagger.yaml' => { openapi: '3.0.1', info: { @@ -52,6 +52,6 @@ RSpec.configure do |config| # The swagger_docs configuration option has the filename including format in # the key, this may want to be changed to avoid putting yaml in json files. # Defaults to json. Accepts ':json' and ':yaml'. - config.swagger_format = :yaml + config.openapi_format = :yaml config.swagger_strict_schema_validation = true end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 8b00dda4..f7f41af3 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -6,10 +6,6 @@ info: description: Canine API components: schemas: - projects: - type: array - items: - "$ref": "#/components/schemas/project" project: type: object required: @@ -49,6 +45,10 @@ components: url: type: string example: https://example.com/projects/1 + projects: + type: array + items: + "$ref": "#/components/schemas/project" securitySchemes: x_api_key: type: apiKey @@ -63,8 +63,18 @@ paths: tags: - Deployments operationId: deployProject - security: - - x_api_key: [] + parameters: + - name: X-API-Key + in: header + description: API Key + schema: + type: string + - name: project_id + in: path + description: Project ID + required: true + schema: + type: integer responses: '200': description: successful @@ -74,8 +84,12 @@ paths: tags: - Projects operationId: listProjects - security: - - x_api_key: [] + parameters: + - name: X-API-Key + in: header + description: API Key + schema: + type: string responses: '200': description: successful @@ -83,21 +97,6 @@ paths: application/json: schema: "$ref": "#/components/schemas/projects" - "/projects/{id}": - get: - summary: Show Project - tags: - - Projects - operationId: showProject - security: - - x_api_key: [] - responses: - '200': - description: successful - content: - application/json: - schema: - "$ref": "#/components/schemas/project" "/projects/{id}/restart": post: summary: Restart Project @@ -106,6 +105,18 @@ paths: operationId: restartProject security: - x_api_key: [] + parameters: + - name: id + in: path + description: Project ID + required: true + schema: + type: integer + - name: X-API-Key + in: header + description: API Key + schema: + type: string responses: '200': description: successful From 774d64b98c1b166ed9544e9eb17aa22851426505 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 12 Nov 2025 00:53:19 -0500 Subject: [PATCH 4/7] added ui updates --- .../controllers/expiry_date_controller.js | 30 +++++++++++++ .../api_tokens/_expiry_date_field.html.erb | 43 +++++++++++++++++++ app/views/api_tokens/_form.html.erb | 7 +-- app/views/providers/index.html.erb | 17 +++++--- 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 app/javascript/controllers/expiry_date_controller.js create mode 100644 app/views/api_tokens/_expiry_date_field.html.erb diff --git a/app/javascript/controllers/expiry_date_controller.js b/app/javascript/controllers/expiry_date_controller.js new file mode 100644 index 00000000..25cb3aed --- /dev/null +++ b/app/javascript/controllers/expiry_date_controller.js @@ -0,0 +1,30 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["select", "dateInput"] + + connect() { + this.updateExpiryDate() + } + + updateExpiryDate() { + const selectedOption = this.selectTarget.options[this.selectTarget.selectedIndex] + const expiryValue = selectedOption.dataset.expiryValue + if (expiryValue === "custom") { + this.dateInputTarget.classList.remove("hidden") + } else { + this.dateInputTarget.classList.add("hidden") + } + + if (!expiryValue || expiryValue === "custom") { + this.setDateValue(""); + } else { + const date = new Date(expiryValue); + this.setDateValue(date.toISOString().slice(0, 10)); + } + } + + setDateValue(value) { + this.dateInputTarget.querySelector("input").value = value; + } +} diff --git a/app/views/api_tokens/_expiry_date_field.html.erb b/app/views/api_tokens/_expiry_date_field.html.erb new file mode 100644 index 00000000..5e198150 --- /dev/null +++ b/app/views/api_tokens/_expiry_date_field.html.erb @@ -0,0 +1,43 @@ +<% + seven_days = 7.days.from_now + thirty_days = 30.days.from_now + sixty_days = 60.days.from_now + ninety_days = 90.days.from_now +%> + +
+
+ + <%= form.select :expiry_preset, + [ + ["7 days (#{seven_days.strftime('%b %d, %Y')})", "7_days", { data: { expiry_value: seven_days.strftime('%Y-%m-%dT%H:%M') } }], + ["30 days (#{thirty_days.strftime('%b %d, %Y')})", "30_days", { data: { expiry_value: thirty_days.strftime('%Y-%m-%dT%H:%M') } }], + ["60 days (#{sixty_days.strftime('%b %d, %Y')})", "60_days", { data: { expiry_value: sixty_days.strftime('%Y-%m-%dT%H:%M') } }], + ["90 days (#{ninety_days.strftime('%b %d, %Y')})", "90_days", { data: { expiry_value: ninety_days.strftime('%Y-%m-%dT%H:%M') } }], + ["No Expiration", "never", { data: { expiry_value: "" } }], + ["Custom", "custom", { data: { expiry_value: "custom" } }] + ], + { selected: "7_days" }, + { + class: "select select-bordered", + data: { + expiry_date_target: "select", + action: "change->expiry-date#updateExpiryDate" + } + } + %> +
+ +
+ + <%= form.date_field :expires_at, + value: seven_days.strftime('%Y-%m-%d'), + class: "input input-bordered", + data: { "expiry-date-target": "dateInput" } + %> +
+
diff --git a/app/views/api_tokens/_form.html.erb b/app/views/api_tokens/_form.html.erb index e1c4f684..e64f631e 100644 --- a/app/views/api_tokens/_form.html.erb +++ b/app/views/api_tokens/_form.html.erb @@ -1,11 +1,6 @@ <%= form_with model: api_token do |form| %> <%= render "shared/error_messages", resource: form.object %> -
- - <%= form.datetime_field :expires_at, class: "input input-bordered" %> -
+ <%= render "api_tokens/expiry_date_field", form: form %> <% end %> - <%= turbo_frame_tag "api_token" do %> - <%= render "api_tokens/index" %> -
- <%= link_to "+ API Token", new_api_token_path, class: "btn btn-primary btn-sm" %> -
- <% end %> + +
+

API Tokens

+
+ <%= turbo_frame_tag "api_token" do %> + <%= render "api_tokens/index" %> +
+ <%= link_to "+ API Token", new_api_token_path, class: "btn btn-primary btn-sm" %> +
+ <% end %> +
<% end %> From 43237df2b87de9e2d72d543d15df38cb0408e7a1 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 12 Nov 2025 01:01:03 -0500 Subject: [PATCH 5/7] update tokens --- app/models/api_token.rb | 4 +++- app/views/api_tokens/_form.html.erb | 4 ++++ app/views/api_tokens/new.html.erb | 2 +- db/migrate/20251022183720_create_api_tokens.rb | 2 ++ spec/factories/api_tokens.rb | 4 +++- spec/models/api_token_spec.rb | 4 +++- 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/models/api_token.rb b/app/models/api_token.rb index bdb7e758..7731ce8a 100644 --- a/app/models/api_token.rb +++ b/app/models/api_token.rb @@ -6,13 +6,15 @@ # access_token :string not null # expires_at :datetime # last_used_at :datetime +# name :string not null # created_at :datetime not null # updated_at :datetime not null # user_id :bigint not null # # Indexes # -# index_api_tokens_on_user_id (user_id) +# index_api_tokens_on_access_token (access_token) UNIQUE +# index_api_tokens_on_user_id (user_id) # # Foreign Keys # diff --git a/app/views/api_tokens/_form.html.erb b/app/views/api_tokens/_form.html.erb index e64f631e..9fcd7093 100644 --- a/app/views/api_tokens/_form.html.erb +++ b/app/views/api_tokens/_form.html.erb @@ -1,5 +1,9 @@ <%= form_with model: api_token do |form| %> <%= render "shared/error_messages", resource: form.object %> +
+ <%= form.label :name %> + <%= form.text_field :name, class: "input input-bordered w-full max-w-sm", required: true, placeholder: "my-api-token" %> +
<%= render "api_tokens/expiry_date_field", form: form %>