diff --git a/Gemfile b/Gemfile index d3508be167f..0570ad9b938 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem 'data_migrate' gem 'diffy' gem 'discord-notifier' gem 'discordrb', '~> 3.5', require: false +gem 'doorkeeper' gem 'good_job', '~> 3.14', github: 'komagata/good_job' gem 'google-cloud-storage', '~> 1.25', require: false gem 'holiday_jp' diff --git a/Gemfile.lock b/Gemfile.lock index 739f355c390..e4b904a8117 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -161,6 +161,8 @@ GEM rest-client (>= 2.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) + doorkeeper (5.8.0) + railties (>= 5) erubi (1.12.0) et-orbi (1.2.11) tzinfo @@ -621,6 +623,7 @@ DEPENDENCIES diffy discord-notifier discordrb (~> 3.5) + doorkeeper foreman good_job (~> 3.14)! google-cloud-storage (~> 1.25) diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index c8e4549d3e0..8c57f60ad8c 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -3,6 +3,8 @@ class API::UsersController < API::BaseController before_action :set_user, only: %i[show update] before_action :require_login_for_api + before_action :require_login_for_api, except: :show + before_action :doorkeeper_authorize!, if: -> { doorkeeper_token.present? }, only: :show PAGER_NUMBER = 24 def index @@ -72,7 +74,11 @@ def target_users end def set_user - @user = User.find(params[:id]) + @user = if params[:id] == 'show' + User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token + else + User.find(params[:id]) + end end def user_params diff --git a/app/models/user.rb b/app/models/user.rb index 44b2e75653f..4ad22ca5b0b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -172,6 +172,18 @@ class User < ApplicationRecord through: :regular_event_participations, source: :regular_event + has_many :access_grants, + class_name: 'Doorkeeper::AccessGrant', + foreign_key: :resource_owner_id, + inverse_of: 'resource_owner', + dependent: :delete_all + + has_many :access_tokens, + class_name: 'Doorkeeper::AccessToken', + foreign_key: :resource_owner_id, + inverse_of: 'resource_owner', + dependent: :delete_all + has_one_attached :avatar has_one_attached :profile_image diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000000..4d471507013 --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +Doorkeeper.configure do +orm :active_record + resource_owner_authenticator do + current_user || redirect_to(login_path) + end + + admin_authenticator do + if current_user + redirect_to root_path, alert: '管理者としてログインしてください' unless current_user.admin? + else + redirect_to login_path + end + end + + # デフォルトのスコープを read に設定 + # これにより承認されたアプリケーションは API から公開データの「読み取り」が可能 + default_scopes :read + enforce_configured_scopes +end diff --git a/config/routes/api.rb b/config/routes/api.rb index 52dca543380..f88ccbd85e6 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true Rails.application.routes.draw do + use_doorkeeper namespace 'api' do namespace 'admin' do resource :count, controller: 'count', only: %i(show) diff --git a/db/migrate/20241209132613_create_doorkeeper_tables.rb b/db/migrate/20241209132613_create_doorkeeper_tables.rb new file mode 100644 index 00000000000..4a5ce78f537 --- /dev/null +++ b/db/migrate/20241209132613_create_doorkeeper_tables.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class CreateDoorkeeperTables < ActiveRecord::Migration[6.1] + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.boolean :confidential, null: false, default: true + t.timestamps null: false + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.references :resource_owner, null: false + t.references :application, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.datetime :created_at, null: false + t.datetime :revoked_at + end + + add_index :oauth_access_grants, :token, unique: true + add_foreign_key( + :oauth_access_grants, + :oauth_applications, + column: :application_id + ) + + create_table :oauth_access_tokens do |t| + t.references :resource_owner, index: true + t.references :application, null: false + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.string :scopes + t.datetime :created_at, null: false + t.datetime :revoked_at + t.string :previous_refresh_token, null: false, default: "" + end + + add_index :oauth_access_tokens, :token, unique: true + + # See https://github.com/doorkeeper-gem/doorkeeper/issues/1592 + if ActiveRecord::Base.connection.adapter_name == "SQLServer" + execute <<~SQL.squish + CREATE UNIQUE NONCLUSTERED INDEX index_oauth_access_tokens_on_refresh_token ON oauth_access_tokens(refresh_token) + WHERE refresh_token IS NOT NULL + SQL + else + add_index :oauth_access_tokens, :refresh_token, unique: true + end + + add_foreign_key( + :oauth_access_tokens, + :oauth_applications, + column: :application_id + ) + + add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id + add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id + end +end diff --git a/db/schema.rb b/db/schema.rb index e1db00f306f..f27a6c647bb 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.define(version: 2024_11_03_082456) do +ActiveRecord::Schema.define(version: 2024_12_09_132613) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -454,6 +454,48 @@ t.index ["user_id"], name: "index_notifications_on_user_id" end + create_table "oauth_access_grants", force: :cascade do |t| + t.bigint "resource_owner_id", null: false + t.bigint "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" + t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true + end + + create_table "oauth_access_tokens", force: :cascade do |t| + t.bigint "resource_owner_id" + t.bigint "application_id", null: false + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.string "scopes" + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + end + create_table "organizers", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "regular_event_id", null: false @@ -846,6 +888,10 @@ add_foreign_key "micro_reports", "users" add_foreign_key "notifications", "users" add_foreign_key "notifications", "users", column: "sender_id" + add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" add_foreign_key "organizers", "regular_events" add_foreign_key "organizers", "users" add_foreign_key "pages", "practices"