diff --git a/app/controllers/account/bot_access_tokens_controller.rb b/app/controllers/account/bot_access_tokens_controller.rb new file mode 100644 index 000000000..db397244e --- /dev/null +++ b/app/controllers/account/bot_access_tokens_controller.rb @@ -0,0 +1,37 @@ +class Account::BotAccessTokensController < ApplicationController + before_action :ensure_admin + before_action :set_bot + + def new + @access_token = bot_access_tokens.new + end + + def create + access_token = bot_access_tokens.create!(access_token_params) + expiring_id = token_verifier.generate(access_token.id, expires_in: 30.seconds) + + redirect_to account_bot_path(@bot, token: expiring_id) + end + + def destroy + bot_access_tokens.find(params[:id]).destroy! + redirect_to account_bot_path(@bot) + end + + private + def set_bot + @bot = Current.account.users.where(role: :bot).find(params[:bot_id]) + end + + def bot_access_tokens + @bot.identity.access_tokens + end + + def access_token_params + params.expect(access_token: %i[ description permission ]) + end + + def token_verifier + Rails.application.message_verifier(:bot_tokens) + end +end diff --git a/app/controllers/account/bots_controller.rb b/app/controllers/account/bots_controller.rb new file mode 100644 index 000000000..8ddac3a52 --- /dev/null +++ b/app/controllers/account/bots_controller.rb @@ -0,0 +1,70 @@ +class Account::BotsController < ApplicationController + before_action :ensure_admin + before_action :set_bot, only: %i[ show update destroy ] + + def new + end + + def create + bot, access_token = ActiveRecord::Base.transaction do + identity = Identity.create!(email_address: "bot+#{SecureRandom.hex(4)}@fizzy.internal") + bot = Current.account.users.create!( + name: name, + role: :bot, + identity: identity, + verified_at: Time.current + ) + access_token = identity.access_tokens.create!( + description: "Initial token", + permission: :write + ) + [ bot, access_token ] + end + + respond_to do |format| + format.html { redirect_to account_bot_path(bot, token: token_verifier.generate(access_token.id, expires_in: 30.seconds)) } + format.json { render json: { user: { id: bot.id, name: bot.name, role: bot.role }, token: access_token.token }, status: :created } + end + end + + def show + @access_tokens = @bot.identity.access_tokens.order(created_at: :desc) + + if params[:token] + @new_access_token = Identity::AccessToken.find(token_verifier.verify(params[:token])) + end + rescue ActiveSupport::MessageVerifier::InvalidSignature + @new_access_token = nil + end + + def update + @bot.update!(name: name) + + respond_to do |format| + format.html { redirect_to account_bot_path(@bot), notice: "#{@bot.name} has been updated" } + format.json { render json: { user: { id: @bot.id, name: @bot.name, role: @bot.role } } } + end + end + + def destroy + @bot.deactivate + + respond_to do |format| + format.html { redirect_to account_settings_path, notice: "#{@bot.name} has been removed" } + format.json { head :no_content } + end + end + + private + def set_bot + @bot = Current.account.users.where(role: :bot).find(params[:id]) + end + + def name + params.expect(:name) + end + + def token_verifier + Rails.application.message_verifier(:bot_tokens) + end +end diff --git a/app/models/comment/eventable.rb b/app/models/comment/eventable.rb index 7fe7abebb..a730cf1d6 100644 --- a/app/models/comment/eventable.rb +++ b/app/models/comment/eventable.rb @@ -13,7 +13,7 @@ def event_was_created(event) private def should_track_event? - !creator.system? + !creator.system? && !creator.bot? end def track_creation diff --git a/app/models/notification/pushable.rb b/app/models/notification/pushable.rb index e02f3a8de..34b292668 100644 --- a/app/models/notification/pushable.rb +++ b/app/models/notification/pushable.rb @@ -39,7 +39,7 @@ def payload private def pushable? - !creator.system? && user.active? && account.active? + !creator.system? && !creator.bot? && user.active? && account.active? end def push_to(target) diff --git a/app/models/notifier.rb b/app/models/notifier.rb index f6f356fa4..3eb83a2ed 100644 --- a/app/models/notifier.rb +++ b/app/models/notifier.rb @@ -43,6 +43,6 @@ def initialize(source) end def should_notify? - !creator.system? + !creator.system? && !creator.bot? end end diff --git a/app/models/user.rb b/app/models/user.rb index 842f7f6c8..663eb3f80 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -25,7 +25,7 @@ def deactivate end def setup? - name != identity.email_address + bot? || name != identity&.email_address end def verified? diff --git a/app/models/user/configurable.rb b/app/models/user/configurable.rb index c9c5adf53..4fd4f5f8c 100644 --- a/app/models/user/configurable.rb +++ b/app/models/user/configurable.rb @@ -5,7 +5,7 @@ module User::Configurable has_one :settings, class_name: "User::Settings", dependent: :destroy has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all - after_create :create_settings, unless: :system? + after_create :create_settings, unless: -> { system? || bot? } delegate :timezone, to: :settings, allow_nil: true end diff --git a/app/models/user/role.rb b/app/models/user/role.rb index e103e94d9..21ddc0a35 100644 --- a/app/models/user/role.rb +++ b/app/models/user/role.rb @@ -2,12 +2,13 @@ module User::Role extend ActiveSupport::Concern included do - enum :role, %i[ owner admin member system ].index_by(&:itself), scopes: false + enum :role, %i[ owner admin member system bot ].index_by(&:itself), scopes: false scope :owner, -> { where(active: true, role: :owner) } scope :admin, -> { where(active: true, role: %i[ owner admin ]) } scope :member, -> { where(active: true, role: :member) } - scope :active, -> { where(active: true, role: %i[ owner admin member ]) } + scope :bot, -> { where(active: true, role: :bot) } + scope :active, -> { where(active: true, role: %i[ owner admin member bot ]) } def admin? super || owner? diff --git a/app/models/user/settings.rb b/app/models/user/settings.rb index 598ede9c5..2d0e04523 100644 --- a/app/models/user/settings.rb +++ b/app/models/user/settings.rb @@ -21,7 +21,7 @@ def bundle_aggregation_period end def bundling_emails? - !bundle_email_never? && !user.system? && user.active? && user.verified? + !bundle_email_never? && !user.system? && !user.bot? && user.active? && user.verified? end def timezone diff --git a/app/views/account/bot_access_tokens/new.html.erb b/app/views/account/bot_access_tokens/new.html.erb new file mode 100644 index 000000000..c97d8c904 --- /dev/null +++ b/app/views/account/bot_access_tokens/new.html.erb @@ -0,0 +1,29 @@ +<% @page_title = "Generate an access token for #{@bot.name}" %> + +<% content_for :header do %> +
Bots can access Fizzy via API using a personal access token
+Be sure to save this access token now because you won't be able to see it again.
+ + <%= tag.button class: "btn btn--link center", data: { + controller: "copy-to-clipboard", action: "copy-to-clipboard#copy", + copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: @new_access_token.token } do %> + <%= icon_tag "copy-paste" %> + Copy access token + <% end %> + +Access tokens for this bot.
+| Description | +Permission | +Created | ++ |
|---|---|---|---|
| <%= access_token.description %> | +<%= access_token.permission.humanize %> | +<%= local_datetime_tag access_token.created_at, style: :datetime %> | ++ <%= button_to account_bot_access_token_path(@bot, access_token), method: :delete, + class: "btn txt-negative btn--circle txt-x-small borderless fill-transparent", + data: { turbo_confirm: "Are you sure you want to permanently revoke this access token?" } do %> + <%= icon_tag "trash" %> + Revoke this token + <% end %> + | +
This bot has no access tokens. Generate one to allow API access.
+ <% end %> + + <%= link_to new_account_bot_access_token_path(@bot), class: "btn btn--link center" do %> + <%= icon_tag "add" %> + Generate a new access token + <% end %> + +