Application prefs section (#2758)
* Add code for creating/managing apps to settings section * Add specs for app changes * Fix controller spec * Fix view file I pasted over by mistake * Add locale strings. Add 'my apps' to nav * Add Client ID/Secret to App page. Add some visual separation * Fix rubocop warnings * Fix embarrassing typo I lost an `end` statement while fixing a merge conflict. * Add code for creating/managing apps to settings section - Add specs for app changes - Add locale strings. Add 'my apps' to nav - Add Client ID/Secret to App page. Add some visual separation - Fix some bugs/warnings * Update to match code standards * Trigger notification * Add warning about not sharing API secrets * Tweak spec a bit * Cleanup fixture creation by using let! * Remove unused key * Add foreign key for application<->user
This commit is contained in:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							11a7507318
						
					
				
				
					commit
					871c0d251a
				
			
							
								
								
									
										65
									
								
								app/controllers/settings/applications_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								app/controllers/settings/applications_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class Settings::ApplicationsController < ApplicationController | ||||
|   layout 'admin' | ||||
|  | ||||
|   before_action :authenticate_user! | ||||
|  | ||||
|   def index | ||||
|     @applications = current_user.applications.page(params[:page]) | ||||
|   end | ||||
|  | ||||
|   def new | ||||
|     @application = Doorkeeper::Application.new( | ||||
|       redirect_uri: Doorkeeper.configuration.native_redirect_uri, | ||||
|       scopes: 'read write follow' | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def show | ||||
|     @application = current_user.applications.find(params[:id]) | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @application = current_user.applications.build(application_params) | ||||
|     if @application.save | ||||
|       redirect_to settings_applications_path, notice: I18n.t('application.created') | ||||
|     else | ||||
|       render :new | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update | ||||
|     @application = current_user.applications.find(params[:id]) | ||||
|     if @application.update_attributes(application_params) | ||||
|       redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg') | ||||
|     else | ||||
|       render :show | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy | ||||
|     @application = current_user.applications.find(params[:id]) | ||||
|     @application.destroy | ||||
|     redirect_to settings_applications_path, notice: t('application.destroyed') | ||||
|   end | ||||
|  | ||||
|   def regenerate | ||||
|     @application = current_user.applications.find(params[:application_id]) | ||||
|     @access_token = current_user.token_for_app(@application) | ||||
|     @access_token.destroy | ||||
|  | ||||
|     redirect_to settings_application_path(@application), notice: t('access_token.regenerated') | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def application_params | ||||
|     params.require(:doorkeeper_application).permit( | ||||
|       :name, | ||||
|       :redirect_uri, | ||||
|       :scopes, | ||||
|       :website | ||||
|     ) | ||||
|   end | ||||
| end | ||||
| @@ -46,6 +46,8 @@ class User < ApplicationRecord | ||||
|   belongs_to :account, inverse_of: :user, required: true | ||||
|   accepts_nested_attributes_for :account | ||||
|  | ||||
|   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner | ||||
|  | ||||
|   validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? | ||||
|   validates_with BlacklistedEmailValidator, if: :email_changed? | ||||
|  | ||||
| @@ -108,6 +110,17 @@ class User < ApplicationRecord | ||||
|     settings.noindex | ||||
|   end | ||||
|  | ||||
|   def token_for_app(a) | ||||
|     return nil if a.nil? || a.owner != self | ||||
|     Doorkeeper::AccessToken | ||||
|       .find_or_create_by(application_id: a.id, resource_owner_id: id) do |t| | ||||
|  | ||||
|       t.scopes = a.scopes | ||||
|       t.expires_in = Doorkeeper.configuration.access_token_expires_in | ||||
|       t.use_refresh_token = Doorkeeper.configuration.refresh_token_enabled? | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def activate_session(request) | ||||
|     session_activations.activate(session_id: SecureRandom.hex, | ||||
|                                  user_agent: request.user_agent, | ||||
|   | ||||
							
								
								
									
										4
									
								
								app/views/settings/applications/_fields.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/views/settings/applications/_fields.html.haml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| = f.input :name, hint: t('activerecord.attributes.doorkeeper/application.name') | ||||
| = f.input :website, hint: t('activerecord.attributes.doorkeeper/application.website') | ||||
| = f.input :redirect_uri, hint: t('activerecord.attributes.doorkeeper/application.redirect_uri') | ||||
| = f.input :scopes, hint: t('activerecord.attributes.doorkeeper/application.scopes') | ||||
							
								
								
									
										20
									
								
								app/views/settings/applications/index.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/views/settings/applications/index.html.haml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| - content_for :page_title do | ||||
|   = t('doorkeeper.applications.index.title') | ||||
|  | ||||
| %table.table | ||||
|   %thead | ||||
|     %tr | ||||
|       %th= t('doorkeeper.applications.index.application') | ||||
|       %th= t('doorkeeper.applications.index.scopes') | ||||
|       %th= t('doorkeeper.applications.index.created_at') | ||||
|       %th | ||||
|   %tbody | ||||
|     - @applications.each do |application| | ||||
|       %tr | ||||
|         %td= link_to application.name, settings_application_path(application) | ||||
|         %th= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('<br />').html_safe | ||||
|         %td= l application.created_at | ||||
|         %td= table_link_to 'show', t('doorkeeper.applications.index.show'), settings_application_path(application) | ||||
|         %td= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } | ||||
| = paginate @applications | ||||
| = link_to t('add_new'), new_settings_application_path, class: 'button' | ||||
							
								
								
									
										9
									
								
								app/views/settings/applications/new.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/settings/applications/new.html.haml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| - content_for :page_title do | ||||
|   = t('doorkeeper.applications.new.title') | ||||
|    | ||||
| .form-container | ||||
|   = simple_form_for @application, url: settings_applications_path do |f| | ||||
|     = render 'fields', f:f | ||||
|      | ||||
|     .actions | ||||
|       = f.button :button, t('.create'), type: :submit | ||||
							
								
								
									
										28
									
								
								app/views/settings/applications/show.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/views/settings/applications/show.html.haml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| - content_for :page_title do | ||||
|   = t('doorkeeper.applications.show.title', name: @application.name) | ||||
|  | ||||
|  | ||||
| %p.hint= t('application.warning') | ||||
|    | ||||
| %div | ||||
|   %h3= t('application.uid') | ||||
|   %code= @application.uid | ||||
|  | ||||
| %div  | ||||
|   %h3= t('application.secret') | ||||
|   %code= @application.secret | ||||
|  | ||||
| %div | ||||
|   %h3= t('access_token.your_token') | ||||
|   %code= current_user.token_for_app(@application).token | ||||
|  | ||||
| = link_to t('access_token.regenerate'), settings_application_regenerate_path(@application), method: :put,  class: 'button' | ||||
|  | ||||
| %hr | ||||
|  | ||||
| = simple_form_for @application, url: settings_application_path(@application), method: :put do |f| | ||||
|   = render 'fields', f:f | ||||
|      | ||||
|   .actions | ||||
|     = f.button :button, t('generic.save_changes'), type: :submit | ||||
|  | ||||
| @@ -50,7 +50,7 @@ Doorkeeper.configure do | ||||
|   # Optional parameter :confirmation => true (default false) if you want to enforce ownership of | ||||
|   # a registered application | ||||
|   # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support | ||||
|   # enable_application_owner :confirmation => true | ||||
|   enable_application_owner | ||||
|  | ||||
|   # Define access token scopes for your provider | ||||
|   # For more information go to | ||||
|   | ||||
| @@ -3,8 +3,10 @@ en: | ||||
|   activerecord: | ||||
|     attributes: | ||||
|       doorkeeper/application: | ||||
|         name: Name | ||||
|         name: Application Name | ||||
|         website: Application Website | ||||
|         redirect_uri: Redirect URI | ||||
|         scopes: Scopes | ||||
|     errors: | ||||
|       models: | ||||
|         doorkeeper/application: | ||||
| @@ -37,9 +39,12 @@ en: | ||||
|         name: Name | ||||
|         new: New Application | ||||
|         title: Your applications | ||||
|         show: Show | ||||
|         delete: Delete | ||||
|       new: | ||||
|         title: New Application | ||||
|       show: | ||||
|         title: 'Application: %{name}' | ||||
|         actions: Actions | ||||
|         application_id: Application Id | ||||
|         callback_urls: Callback urls | ||||
|   | ||||
| @@ -33,6 +33,10 @@ en: | ||||
|     user_count_after: users | ||||
|     user_count_before: Home to | ||||
|     what_is_mastodon: What is Mastodon? | ||||
|   access_token: | ||||
|     your_token: Your Access Token | ||||
|     regenerate: Regenerate Access Token | ||||
|     regenerated: Access Token Regenerated | ||||
|   accounts: | ||||
|     follow: Follow | ||||
|     followers: Followers | ||||
| @@ -226,6 +230,12 @@ en: | ||||
|     settings: 'Change e-mail preferences: %{link}' | ||||
|     signature: Mastodon notifications from %{instance} | ||||
|     view: 'View:' | ||||
|   application: | ||||
|     created: Application Created | ||||
|     destroyed: Application Destroyed | ||||
|     uid: Client ID | ||||
|     secret: Client Secret | ||||
|     warning: Be very careful with this data. Never share it with anyone other than authorized applications!     | ||||
|   applications: | ||||
|     invalid_url: The provided URL is invalid | ||||
|   auth: | ||||
| @@ -423,6 +433,7 @@ en: | ||||
|     preferences: Preferences | ||||
|     settings: Settings | ||||
|     two_factor_authentication: Two-factor Authentication | ||||
|     your_apps: Your applications | ||||
|   statuses: | ||||
|     open_in_web: Open in web | ||||
|     over_character_limit: character limit of %{max} exceeded | ||||
|   | ||||
| @@ -12,6 +12,7 @@ SimpleNavigation::Configuration.run do |navigation| | ||||
|       settings.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url | ||||
|       settings.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url | ||||
|       settings.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url | ||||
|       settings.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url | ||||
|       settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url | ||||
|     end | ||||
|  | ||||
|   | ||||
| @@ -79,6 +79,11 @@ Rails.application.routes.draw do | ||||
|     end | ||||
|  | ||||
|     resource :follower_domains, only: [:show, :update] | ||||
|  | ||||
|     resources :applications do | ||||
|       put :regenerate | ||||
|     end | ||||
|  | ||||
|     resource :delete, only: [:show, :destroy] | ||||
|  | ||||
|     resources :sessions, only: [:destroy] | ||||
|   | ||||
							
								
								
									
										8
									
								
								db/migrate/20170427011934_re_add_owner_to_application.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								db/migrate/20170427011934_re_add_owner_to_application.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| class ReAddOwnerToApplication < ActiveRecord::Migration[5.0] | ||||
|   def change | ||||
|     add_column :oauth_applications, :owner_id, :integer, null: true | ||||
|     add_column :oauth_applications, :owner_type, :string, null: true | ||||
|     add_index :oauth_applications, [:owner_id, :owner_type] | ||||
|     add_foreign_key :oauth_applications, :users, column: :owner_id, on_delete: :cascade | ||||
|   end | ||||
| end | ||||
| @@ -216,8 +216,11 @@ ActiveRecord::Schema.define(version: 20170720000000) do | ||||
|     t.string "scopes", default: "", null: false | ||||
|     t.datetime "created_at" | ||||
|     t.datetime "updated_at" | ||||
|     t.boolean "superapp", default: false, null: false | ||||
|     t.string "website" | ||||
|     t.boolean  "superapp",     default: false, null: false | ||||
|     t.string   "website" | ||||
|     t.integer  "owner_id" | ||||
|     t.string   "owner_type" | ||||
|     t.index ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree | ||||
|     t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true | ||||
|   end | ||||
|  | ||||
|   | ||||
							
								
								
									
										166
									
								
								spec/controllers/settings/applications_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								spec/controllers/settings/applications_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| describe Settings::ApplicationsController do | ||||
|   render_views | ||||
|    | ||||
|   let!(:user) { Fabricate(:user) } | ||||
|   let!(:app) { Fabricate(:application, owner: user) } | ||||
|    | ||||
|   before do | ||||
|     sign_in user, scope: :user | ||||
|   end | ||||
|  | ||||
|   describe 'GET #index' do | ||||
|     let!(:other_app) { Fabricate(:application) } | ||||
|  | ||||
|     it 'shows apps' do | ||||
|       get :index | ||||
|       expect(response).to have_http_status(:success) | ||||
|       expect(assigns(:applications)).to include(app) | ||||
|       expect(assigns(:applications)).to_not include(other_app) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|    | ||||
|   describe 'GET #show' do | ||||
|     it 'returns http success' do | ||||
|       get :show, params: { id: app.id } | ||||
|       expect(response).to have_http_status(:success) | ||||
|       expect(assigns[:application]).to eql(app) | ||||
|     end | ||||
|  | ||||
|     it 'returns 404 if you dont own app' do | ||||
|       app.update!(owner: nil) | ||||
|  | ||||
|       get :show, params: { id: app.id } | ||||
|       expect(response.status).to eq 404 | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'GET #new' do | ||||
|     it 'works' do | ||||
|       get :new | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'POST #create' do | ||||
|     context 'success' do | ||||
|       def call_create | ||||
|         post :create, params: { | ||||
|                doorkeeper_application: { | ||||
|                  name: 'My New App', | ||||
|                  redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', | ||||
|                  website: 'http://google.com', | ||||
|                  scopes: 'read write follow' | ||||
|                } | ||||
|              } | ||||
|         response | ||||
|       end | ||||
|  | ||||
|       it 'creates an entry in the database' do | ||||
|         expect { call_create }.to change(Doorkeeper::Application, :count) | ||||
|       end | ||||
|        | ||||
|       it 'redirects back to applications page' do | ||||
|         expect(call_create).to redirect_to(settings_applications_path) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'failure' do | ||||
|       before do | ||||
|         post :create, params: { | ||||
|                doorkeeper_application: { | ||||
|                  name: '', | ||||
|                  redirect_uri: '', | ||||
|                  website: '', | ||||
|                  scopes: '' | ||||
|                } | ||||
|              } | ||||
|       end | ||||
|  | ||||
|       it 'returns http success' do | ||||
|         expect(response).to have_http_status(:success) | ||||
|       end | ||||
|  | ||||
|       it 'renders form again' do | ||||
|         expect(response).to render_template(:new) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|    | ||||
|   describe 'PATCH #update' do | ||||
|     context 'success' do | ||||
|       let(:opts) { | ||||
|         { | ||||
|           website: 'https://foo.bar/' | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       def call_update | ||||
|         patch :update, params: { | ||||
|                 id: app.id, | ||||
|                 doorkeeper_application: opts | ||||
|               } | ||||
|         response | ||||
|       end | ||||
|  | ||||
|       it 'updates existing application' do | ||||
|         call_update | ||||
|         expect(app.reload.website).to eql(opts[:website]) | ||||
|       end | ||||
|        | ||||
|       it 'redirects back to applications page' do | ||||
|         expect(call_update).to redirect_to(settings_applications_path) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'failure' do | ||||
|       before do | ||||
|         patch :update, params: { | ||||
|                 id: app.id, | ||||
|                 doorkeeper_application: { | ||||
|                   name: '', | ||||
|                   redirect_uri: '', | ||||
|                   website: '', | ||||
|                   scopes: '' | ||||
|                 } | ||||
|               } | ||||
|       end | ||||
|  | ||||
|       it 'returns http success' do | ||||
|         expect(response).to have_http_status(:success) | ||||
|       end | ||||
|  | ||||
|       it 'renders form again' do | ||||
|         expect(response).to render_template(:show) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'destroy' do | ||||
|     before do | ||||
|       post :destroy, params: { id: app.id } | ||||
|     end | ||||
|  | ||||
|     it 'redirects back to applications page' do | ||||
|       expect(response).to redirect_to(settings_applications_path) | ||||
|     end | ||||
|  | ||||
|     it 'removes the app' do | ||||
|       expect(Doorkeeper::Application.find_by(id: app.id)).to be_nil | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'regenerate' do | ||||
|     let(:token) { user.token_for_app(app) } | ||||
|     before do | ||||
|       expect(token).to_not be_nil | ||||
|       put :regenerate, params: { application_id: app.id } | ||||
|     end | ||||
|  | ||||
|     it 'should create new token' do | ||||
|       expect(user.token_for_app(app)).to_not eql(token) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -286,4 +286,24 @@ RSpec.describe User, type: :model do | ||||
|       Fabricate(:user) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'token_for_app' do | ||||
|     let(:user) { Fabricate(:user) } | ||||
|     let(:app) { Fabricate(:application, owner: user) } | ||||
|  | ||||
|     it 'returns a token' do | ||||
|       expect(user.token_for_app(app)).to be_a(Doorkeeper::AccessToken) | ||||
|     end | ||||
|  | ||||
|     it 'persists a token' do | ||||
|       t = user.token_for_app(app) | ||||
|       expect(user.token_for_app(app)).to eql(t) | ||||
|     end | ||||
|  | ||||
|     it 'is nil if user does not own app' do | ||||
|       app.update!(owner: nil) | ||||
|  | ||||
|       expect(user.token_for_app(app)).to be_nil | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user