mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-29 14:35:06 +01:00
First tiny steps towards FASP integration
This is lacking tests (and proper icons), but can talk to a debug provider.
This commit is contained in:
parent
0ac43700d8
commit
f136a9cf0f
1
Gemfile
1
Gemfile
@ -61,6 +61,7 @@ gem 'inline_svg'
|
|||||||
gem 'irb', '~> 1.8'
|
gem 'irb', '~> 1.8'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
|
gem 'linzer', '~> 0.6.1'
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
|
||||||
gem 'mutex_m'
|
gem 'mutex_m'
|
||||||
|
10
Gemfile.lock
10
Gemfile.lock
@ -203,6 +203,7 @@ GEM
|
|||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (3.1.7)
|
dotenv (3.1.7)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
|
ed25519 (1.3.0)
|
||||||
elasticsearch (7.17.11)
|
elasticsearch (7.17.11)
|
||||||
elasticsearch-api (= 7.17.11)
|
elasticsearch-api (= 7.17.11)
|
||||||
elasticsearch-transport (= 7.17.11)
|
elasticsearch-transport (= 7.17.11)
|
||||||
@ -381,6 +382,12 @@ GEM
|
|||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rexml
|
rexml
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
|
linzer (0.6.1)
|
||||||
|
ed25519 (~> 1.3, >= 1.3.0)
|
||||||
|
openssl (~> 3.0, >= 3.0.0)
|
||||||
|
rack (>= 2.2, < 4.0)
|
||||||
|
starry (~> 0.2)
|
||||||
|
uri (~> 1.0, >= 1.0.2)
|
||||||
llhttp-ffi (0.5.0)
|
llhttp-ffi (0.5.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
@ -793,6 +800,8 @@ GEM
|
|||||||
simplecov-lcov (0.8.0)
|
simplecov-lcov (0.8.0)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
stackprof (0.2.26)
|
stackprof (0.2.26)
|
||||||
|
starry (0.2.0)
|
||||||
|
base64
|
||||||
stoplight (4.1.0)
|
stoplight (4.1.0)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.2)
|
stringio (3.1.2)
|
||||||
@ -941,6 +950,7 @@ DEPENDENCIES
|
|||||||
letter_opener (~> 1.8)
|
letter_opener (~> 1.8)
|
||||||
letter_opener_web (~> 3.0)
|
letter_opener_web (~> 3.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
|
linzer (~> 0.6.1)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
mail (~> 2.8)
|
mail (~> 2.8)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
|
20
app/controllers/admin/fasp/debug/callbacks_controller.rb
Normal file
20
app/controllers/admin/fasp/debug/callbacks_controller.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Fasp::Debug::CallbacksController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
authorize [:admin, :fasp, :provider], :update?
|
||||||
|
|
||||||
|
@callbacks = Fasp::DebugCallback
|
||||||
|
.includes(:fasp_provider)
|
||||||
|
.order(created_at: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize [:admin, :fasp, :provider], :update?
|
||||||
|
|
||||||
|
callback = Fasp::DebugCallback.find(params[:id])
|
||||||
|
callback.destroy
|
||||||
|
|
||||||
|
redirect_to admin_fasp_debug_callbacks_path
|
||||||
|
end
|
||||||
|
end
|
19
app/controllers/admin/fasp/debug_calls_controller.rb
Normal file
19
app/controllers/admin/fasp/debug_calls_controller.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Fasp::DebugCallsController < Admin::BaseController
|
||||||
|
before_action :set_provider
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize [:admin, @provider], :update?
|
||||||
|
|
||||||
|
@provider.perform_debug_call
|
||||||
|
|
||||||
|
redirect_to admin_fasp_providers_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_provider
|
||||||
|
@provider = Fasp::Provider.find(params[:provider_id])
|
||||||
|
end
|
||||||
|
end
|
47
app/controllers/admin/fasp/providers_controller.rb
Normal file
47
app/controllers/admin/fasp/providers_controller.rb
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Fasp::ProvidersController < Admin::BaseController
|
||||||
|
before_action :set_provider, only: [:show, :edit, :update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize [:admin, :fasp, :provider], :index?
|
||||||
|
|
||||||
|
@providers = Fasp::Provider.order(confirmed: :asc, created_at: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize [:admin, @provider], :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
authorize [:admin, @provider], :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize [:admin, @provider], :update?
|
||||||
|
|
||||||
|
if @provider.update(provider_params)
|
||||||
|
redirect_to admin_fasp_providers_path
|
||||||
|
else
|
||||||
|
render :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize [:admin, @provider], :destroy?
|
||||||
|
|
||||||
|
@provider.destroy
|
||||||
|
|
||||||
|
redirect_to admin_fasp_providers_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def provider_params
|
||||||
|
params.require(:provider).permit(enabled_capabilities: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_provider
|
||||||
|
@provider = Fasp::Provider.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
23
app/controllers/admin/fasp/registrations_controller.rb
Normal file
23
app/controllers/admin/fasp/registrations_controller.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Fasp::RegistrationsController < Admin::BaseController
|
||||||
|
before_action :set_provider
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize [:admin, @provider], :create?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize [:admin, @provider], :create?
|
||||||
|
|
||||||
|
@provider.update_info!(confirm: true)
|
||||||
|
|
||||||
|
redirect_to edit_admin_fasp_provider_path(@provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_provider
|
||||||
|
@provider = Fasp::Provider.find(params[:provider_id])
|
||||||
|
end
|
||||||
|
end
|
76
app/controllers/api/fasp/base_controller.rb
Normal file
76
app/controllers/api/fasp/base_controller.rb
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::Fasp::BaseController < ApplicationController
|
||||||
|
class Error < ::StandardError; end
|
||||||
|
|
||||||
|
DIGEST_PATTERN = /sha-256=:(.*?):/
|
||||||
|
KEYID_PATTERN = /keyid="(.*?)"/
|
||||||
|
|
||||||
|
attr_reader :current_provider
|
||||||
|
|
||||||
|
skip_forgery_protection
|
||||||
|
|
||||||
|
before_action :require_authentication
|
||||||
|
after_action :sign_response
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_authentication
|
||||||
|
validate_content_digest!
|
||||||
|
validate_signature!
|
||||||
|
rescue Error, Linzer::Error, ActiveRecord::RecordNotFound => e
|
||||||
|
logger.debug("FASP Authentication error: #{e}")
|
||||||
|
authentication_error
|
||||||
|
end
|
||||||
|
|
||||||
|
def authentication_error
|
||||||
|
respond_to do |format|
|
||||||
|
format.json { head 401 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_content_digest!
|
||||||
|
content_digest_header = request.headers['content-digest']
|
||||||
|
raise Error, 'content-digest missing' if content_digest_header.blank?
|
||||||
|
|
||||||
|
digest_received = content_digest_header.match(DIGEST_PATTERN)[1]
|
||||||
|
|
||||||
|
digest_computed = OpenSSL::Digest.base64digest('sha256', request.body&.string || '')
|
||||||
|
|
||||||
|
raise Error, 'content-digest does not match' if digest_received != digest_computed
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_signature!
|
||||||
|
signature_input = request.headers['signature-input']&.encode('UTF-8')
|
||||||
|
raise Error, 'signature-input is missing' if signature_input.blank?
|
||||||
|
|
||||||
|
keyid = signature_input.match(KEYID_PATTERN)[1]
|
||||||
|
provider = Fasp::Provider.find(keyid)
|
||||||
|
linzer_request = Linzer.new_request(
|
||||||
|
request.method,
|
||||||
|
request.original_url,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'content-digest' => request.headers['content-digest'],
|
||||||
|
'signature-input' => signature_input,
|
||||||
|
'signature' => request.headers['signature'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
message = Linzer::Message.new(linzer_request)
|
||||||
|
key = Linzer.new_ed25519_public_key(provider.provider_public_key_raw, keyid)
|
||||||
|
signature = Linzer::Signature.build(message.headers)
|
||||||
|
Linzer.verify(key, message, signature)
|
||||||
|
@current_provider = provider
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_response
|
||||||
|
response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:"
|
||||||
|
|
||||||
|
linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] })
|
||||||
|
message = Linzer::Message.new(linzer_response)
|
||||||
|
key = Linzer.new_ed25519_key(current_provider.server_private_key.raw_private_key)
|
||||||
|
signature = Linzer.sign(key, message, %w(@status content-digest))
|
||||||
|
|
||||||
|
response.headers.merge!(signature.to_h)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::Fasp::Debug::V0::Callback::ResponsesController < Api::Fasp::BaseController
|
||||||
|
def create
|
||||||
|
Fasp::DebugCallback.create(
|
||||||
|
fasp_provider: current_provider,
|
||||||
|
ip: request.remote_ip,
|
||||||
|
request_body: request.raw_post
|
||||||
|
)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.json { head 201 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
26
app/controllers/api/fasp/registrations_controller.rb
Normal file
26
app/controllers/api/fasp/registrations_controller.rb
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::Fasp::RegistrationsController < Api::Fasp::BaseController
|
||||||
|
skip_before_action :require_authentication
|
||||||
|
|
||||||
|
def create
|
||||||
|
@current_provider = Fasp::Provider.create!(
|
||||||
|
name: params[:name],
|
||||||
|
base_url: params[:baseUrl],
|
||||||
|
remote_identifier: params[:serverId],
|
||||||
|
provider_public_key_base64: params[:publicKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: registration_confirmation
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def registration_confirmation
|
||||||
|
{
|
||||||
|
faspId: current_provider.id.to_s,
|
||||||
|
publicKey: current_provider.server_public_key_base64,
|
||||||
|
registrationCompletionUri: admin_fasp_provider_url(current_provider),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
70
app/lib/fasp/request.rb
Normal file
70
app/lib/fasp/request.rb
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Fasp::Request
|
||||||
|
def initialize(provider)
|
||||||
|
@provider = provider
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(path)
|
||||||
|
url = @provider.url(path)
|
||||||
|
response = HTTP.headers(headers('GET', url)).get(url)
|
||||||
|
validate!(response)
|
||||||
|
|
||||||
|
response.parse
|
||||||
|
end
|
||||||
|
|
||||||
|
def post(path, body: nil)
|
||||||
|
url = @provider.url(path)
|
||||||
|
body = body.to_json
|
||||||
|
response = HTTP.headers(headers('POST', url, body)).post(url, body:)
|
||||||
|
|
||||||
|
response.parse if response.body.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def headers(verb, url, body = '')
|
||||||
|
result = {
|
||||||
|
'accept' => 'application/json',
|
||||||
|
'content-digest' => content_digest(body),
|
||||||
|
}
|
||||||
|
result.merge(signature_headers(verb, url, result))
|
||||||
|
end
|
||||||
|
|
||||||
|
def content_digest(body)
|
||||||
|
"sha-256=:#{OpenSSL::Digest.base64digest('sha256', body || '')}:"
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature_headers(verb, url, headers)
|
||||||
|
linzer_request = Linzer.new_request(verb, url, {}, headers)
|
||||||
|
message = Linzer::Message.new(linzer_request)
|
||||||
|
key = Linzer.new_ed25519_key(@provider.server_private_key.raw_private_key, @provider.remote_identifier)
|
||||||
|
signature = Linzer.sign(key, message, %w(@method @target-uri content-digest))
|
||||||
|
Linzer::Signer.send(:populate_parameters, key, {})
|
||||||
|
|
||||||
|
signature.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate!(response)
|
||||||
|
content_digest_header = response.headers['content-digest']
|
||||||
|
raise 'content-digest missing' if content_digest_header.blank?
|
||||||
|
raise 'content-digest does not match' if content_digest_header != content_digest(response.body)
|
||||||
|
|
||||||
|
signature_input = response.headers['signature-input'].encode('UTF-8')
|
||||||
|
raise 'signature-input is missing' if signature_input.blank?
|
||||||
|
|
||||||
|
linzer_response = Linzer.new_response(
|
||||||
|
response.body,
|
||||||
|
response.status,
|
||||||
|
{
|
||||||
|
'content-digest' => content_digest_header,
|
||||||
|
'signature-input' => signature_input,
|
||||||
|
'signature' => response.headers['signature'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
message = Linzer::Message.new(linzer_response)
|
||||||
|
key = Linzer.new_ed25519_public_key(@provider.provider_public_key_raw)
|
||||||
|
signature = Linzer::Signature.build(message.headers)
|
||||||
|
Linzer.verify(key, message, signature)
|
||||||
|
end
|
||||||
|
end
|
10
app/models/concerns/fasp/provider/debug_concern.rb
Normal file
10
app/models/concerns/fasp/provider/debug_concern.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Fasp::Provider::DebugConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def perform_debug_call
|
||||||
|
Fasp::Request.new(self)
|
||||||
|
.post('/debug/v0/callback/logs', body: { hello: 'world' })
|
||||||
|
end
|
||||||
|
end
|
7
app/models/fasp.rb
Normal file
7
app/models/fasp.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Fasp
|
||||||
|
def self.table_name_prefix
|
||||||
|
'fasp_'
|
||||||
|
end
|
||||||
|
end
|
16
app/models/fasp/debug_callback.rb
Normal file
16
app/models/fasp/debug_callback.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: fasp_debug_callbacks
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# ip :string
|
||||||
|
# request_body :text
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# fasp_provider_id :bigint(8) not null
|
||||||
|
#
|
||||||
|
class Fasp::DebugCallback < ApplicationRecord
|
||||||
|
belongs_to :fasp_provider, class_name: 'Fasp::Provider'
|
||||||
|
end
|
98
app/models/fasp/provider.rb
Normal file
98
app/models/fasp/provider.rb
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: fasp_providers
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# base_url :string not null
|
||||||
|
# capabilities :jsonb
|
||||||
|
# confirmed :boolean default(FALSE), not null
|
||||||
|
# contact_email :string
|
||||||
|
# fediverse_account :string
|
||||||
|
# name :string not null
|
||||||
|
# privacy_policy :jsonb
|
||||||
|
# provider_public_key_pem :string not null
|
||||||
|
# remote_identifier :string not null
|
||||||
|
# server_private_key_pem :string not null
|
||||||
|
# sign_in_url :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class Fasp::Provider < ApplicationRecord
|
||||||
|
include DebugConcern
|
||||||
|
|
||||||
|
has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all
|
||||||
|
|
||||||
|
before_create :create_keypair
|
||||||
|
|
||||||
|
def enabled_capabilities=(hash)
|
||||||
|
capabilities.each do |capability|
|
||||||
|
capability['enabled'] = hash[capability['id']] == '1'
|
||||||
|
end
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def capability?(capability_name, only_enabled: true)
|
||||||
|
return false unless confirmed?
|
||||||
|
|
||||||
|
capabilities.present? && capabilities.any? do |capability|
|
||||||
|
capability['id'] == capability_name &&
|
||||||
|
(only_enabled ? capability['enabled'] : true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def server_private_key
|
||||||
|
@server_private_key ||= OpenSSL::PKey.read(server_private_key_pem)
|
||||||
|
end
|
||||||
|
|
||||||
|
def server_public_key_base64
|
||||||
|
Base64.strict_encode64(server_private_key.raw_public_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_public_key_base64=(string)
|
||||||
|
self.provider_public_key_pem =
|
||||||
|
OpenSSL::PKey.new_raw_public_key(
|
||||||
|
'ed25519',
|
||||||
|
Base64.strict_decode64(string)
|
||||||
|
).public_to_pem
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_public_key
|
||||||
|
@provider_public_key ||= OpenSSL::PKey.read(provider_public_key_pem)
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_public_key_raw
|
||||||
|
provider_public_key.raw_public_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_public_key_fingerprint
|
||||||
|
OpenSSL::Digest.base64digest('sha256', provider_public_key_raw)
|
||||||
|
end
|
||||||
|
|
||||||
|
def url(path)
|
||||||
|
base = base_url
|
||||||
|
base = base.chomp('/') if path.start_with?('/')
|
||||||
|
"#{base}#{path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_info!(confirm: false)
|
||||||
|
self.confirmed = true if confirm
|
||||||
|
provider_info = Fasp::Request.new(self).get('/provider_info')
|
||||||
|
assign_attributes(
|
||||||
|
privacy_policy: provider_info['privacyPolicy'],
|
||||||
|
capabilities: provider_info['capabilities'],
|
||||||
|
sign_in_url: provider_info['signInUrl'],
|
||||||
|
contact_email: provider_info['contactEmail'],
|
||||||
|
fediverse_account: provider_info['fediverseAccount']
|
||||||
|
)
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_keypair
|
||||||
|
self.server_private_key_pem =
|
||||||
|
OpenSSL::PKey.generate_key('ed25519').private_to_pem
|
||||||
|
end
|
||||||
|
end
|
23
app/policies/admin/fasp/provider_policy.rb
Normal file
23
app/policies/admin/fasp/provider_policy.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Fasp::ProviderPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
role.can?(:manage_federation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
role.can?(:manage_federation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
role.can?(:manage_federation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
role.can?(:manage_federation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
role.can?(:manage_federation)
|
||||||
|
end
|
||||||
|
end
|
10
app/views/admin/fasp/debug/callbacks/_callback.html.haml
Normal file
10
app/views/admin/fasp/debug/callbacks/_callback.html.haml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
%tr
|
||||||
|
%td= callback.fasp_provider.name
|
||||||
|
%td= callback.fasp_provider.base_url
|
||||||
|
%td= callback.ip
|
||||||
|
%td
|
||||||
|
%time.relative-formatted{ datetime: callback.created_at.iso8601 }
|
||||||
|
%td
|
||||||
|
%code= callback.request_body
|
||||||
|
%td
|
||||||
|
= table_link_to 'close', t('admin.fasp.debug.callbacks.delete'), admin_fasp_debug_callback_path(callback), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
22
app/views/admin/fasp/debug/callbacks/index.html.haml
Normal file
22
app/views/admin/fasp/debug/callbacks/index.html.haml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.fasp.debug.callbacks.title')
|
||||||
|
|
||||||
|
- content_for :heading do
|
||||||
|
%h2= t('admin.fasp.providers.title')
|
||||||
|
= render 'admin/fasp/shared/links'
|
||||||
|
|
||||||
|
- unless @callbacks.empty?
|
||||||
|
%hr.spacer
|
||||||
|
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('admin.fasp.providers.name')
|
||||||
|
%th= t('admin.fasp.providers.base_url')
|
||||||
|
%th= t('admin.fasp.debug.callbacks.ip')
|
||||||
|
%th= t('admin.fasp.debug.callbacks.created_at')
|
||||||
|
%th= t('admin.fasp.debug.callbacks.request_body')
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
= render partial: 'callback', collection: @callbacks
|
17
app/views/admin/fasp/providers/_provider.html.haml
Normal file
17
app/views/admin/fasp/providers/_provider.html.haml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
%tr
|
||||||
|
%td= provider.name
|
||||||
|
%td= provider.base_url
|
||||||
|
%td
|
||||||
|
- if provider.confirmed?
|
||||||
|
= t('admin.fasp.providers.active')
|
||||||
|
- else
|
||||||
|
= t('admin.fasp.providers.registration_requested')
|
||||||
|
%td
|
||||||
|
- unless provider.confirmed?
|
||||||
|
= table_link_to 'link', t('admin.fasp.providers.finish_registration'), new_admin_fasp_provider_registration_path(provider)
|
||||||
|
- if provider.sign_in_url.present?
|
||||||
|
= table_link_to 'link', t('admin.fasp.providers.sign_in'), provider.sign_in_url, target: '_blank'
|
||||||
|
- if provider.capability?('callback')
|
||||||
|
= table_link_to 'link', t('admin.fasp.providers.callback'), admin_fasp_provider_debug_calls_path(provider), data: { method: :post }
|
||||||
|
|
||||||
|
= table_link_to 'close', t('admin.providers.delete'), admin_fasp_provider_path(provider), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
21
app/views/admin/fasp/providers/edit.html.haml
Normal file
21
app/views/admin/fasp/providers/edit.html.haml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.fasp.providers.edit')
|
||||||
|
|
||||||
|
= simple_form_for [:admin, @provider] do |f|
|
||||||
|
= render 'shared/error_messages', object: @provider
|
||||||
|
|
||||||
|
%h4= t('admin.fasp.providers.select_capabilities')
|
||||||
|
|
||||||
|
- f.object.capabilities.each do |capability|
|
||||||
|
.fields-group
|
||||||
|
.input.with_label.boolean.optional.field_with_hint
|
||||||
|
.label_input
|
||||||
|
%label.boolean.optional{ for: "provider_enabled_capabilities_#{capability['id']}" }
|
||||||
|
= capability['id']
|
||||||
|
.label_input__wrapper
|
||||||
|
= hidden_field_tag "provider[enabled_capabilities][#{capability['id']}]", '0', id: false
|
||||||
|
%label.checkbox
|
||||||
|
= check_box_tag "provider[enabled_capabilities][#{capability['id']}]", class: 'optional boolean'
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('admin.fasp.providers.save'), type: :submit
|
20
app/views/admin/fasp/providers/index.html.haml
Normal file
20
app/views/admin/fasp/providers/index.html.haml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.fasp.providers.title')
|
||||||
|
|
||||||
|
- content_for :heading do
|
||||||
|
%h2= t('admin.fasp.providers.title')
|
||||||
|
= render 'admin/fasp/shared/links'
|
||||||
|
|
||||||
|
- unless @providers.empty?
|
||||||
|
%hr.spacer
|
||||||
|
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('admin.fasp.providers.name')
|
||||||
|
%th= t('admin.fasp.providers.base_url')
|
||||||
|
%th= t('admin.fasp.providers.status')
|
||||||
|
%th
|
||||||
|
%tbody
|
||||||
|
= render partial: 'provider', collection: @providers
|
20
app/views/admin/fasp/registrations/new.html.haml
Normal file
20
app/views/admin/fasp/registrations/new.html.haml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.fasp.providers.registrations.title')
|
||||||
|
|
||||||
|
%p= t('admin.fasp.providers.registrations.description')
|
||||||
|
|
||||||
|
%table.table.inline-table
|
||||||
|
%tbody
|
||||||
|
%tr
|
||||||
|
%th= t('admin.fasp.providers.name')
|
||||||
|
%td= @provider.name
|
||||||
|
%tr
|
||||||
|
%th= t('admin.fasp.providers.public_key_fingerprint')
|
||||||
|
%td
|
||||||
|
%code= @provider.provider_public_key_fingerprint
|
||||||
|
|
||||||
|
= form_with url: admin_fasp_provider_registration_path(@provider), class: :simple_form do |_form|
|
||||||
|
.fields-group
|
||||||
|
.actions
|
||||||
|
= link_to t('admin.fasp.providers.registrations.reject'), admin_fasp_provider_path(@provider), data: { method: :delete }, class: 'button negative'
|
||||||
|
%button.button= t('admin.fasp.providers.registrations.confirm')
|
5
app/views/admin/fasp/shared/_links.html.haml
Normal file
5
app/views/admin/fasp/shared/_links.html.haml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.content__heading__tabs
|
||||||
|
= render_navigation renderer: :links do |primary|
|
||||||
|
:ruby
|
||||||
|
primary.item :providers, safe_join([material_symbol('description'), t('admin.fasp.providers.providers')]), admin_fasp_providers_path
|
||||||
|
primary.item :debug_callbacks, safe_join([material_symbol('group'), t('admin.fasp.debug.callbacks.title')]), admin_fasp_debug_callbacks_path
|
@ -555,6 +555,36 @@ en:
|
|||||||
total_storage: Media attachments
|
total_storage: Media attachments
|
||||||
totals_time_period_hint_html: The totals displayed below include data for all time.
|
totals_time_period_hint_html: The totals displayed below include data for all time.
|
||||||
unknown_instance: There is currently no record of this domain on this server.
|
unknown_instance: There is currently no record of this domain on this server.
|
||||||
|
fasp:
|
||||||
|
debug:
|
||||||
|
callbacks:
|
||||||
|
created_at: Created at
|
||||||
|
delete: Delete
|
||||||
|
ip: IP address
|
||||||
|
request_body: Request body
|
||||||
|
title: Debug Callbacks
|
||||||
|
providers:
|
||||||
|
active: Active
|
||||||
|
base_url: Base URL
|
||||||
|
debug: Debug call
|
||||||
|
delete: Delete
|
||||||
|
edit: Edit Provider
|
||||||
|
finish_registration: Finish registration
|
||||||
|
name: Name
|
||||||
|
public_key_fingerprint: Public key fingerprint
|
||||||
|
providers: Providers
|
||||||
|
registration_requested: Registration requested
|
||||||
|
registrations:
|
||||||
|
confirm: Confirm
|
||||||
|
description: You received a registration from a FASP. Reject it if you did not initiate this. If you initiated this, carefully compare name and key fingerprint before confirming the registration.
|
||||||
|
reject: Reject
|
||||||
|
title: Confirm FASP Registration
|
||||||
|
save: Save
|
||||||
|
select_capabilities: Select Capabilities
|
||||||
|
sign_in: Sign In
|
||||||
|
status: Status
|
||||||
|
title: Fediverse Auxiliary Service Providers
|
||||||
|
title: FASP
|
||||||
invites:
|
invites:
|
||||||
deactivate_all: Deactivate all
|
deactivate_all: Deactivate all
|
||||||
filter:
|
filter:
|
||||||
|
@ -73,6 +73,7 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||||||
s.item :announcements, safe_join([material_symbol('campaign'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }
|
s.item :announcements, safe_join([material_symbol('campaign'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }
|
||||||
s.item :custom_emojis, safe_join([material_symbol('mood'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) }
|
s.item :custom_emojis, safe_join([material_symbol('mood'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) }
|
||||||
s.item :webhooks, safe_join([material_symbol('inbox'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) }
|
s.item :webhooks, safe_join([material_symbol('inbox'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) }
|
||||||
|
s.item :fasp, safe_join([material_symbol('captive_portal'), t('admin.fasp.title')]), admin_fasp_providers_path, highlights_on: %r{/admin/fasp}, if: -> { current_user.can?(:manage_federation) }
|
||||||
s.item :relays, safe_join([material_symbol('captive_portal'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !limited_federation_mode? && current_user.can?(:manage_federation) }
|
s.item :relays, safe_join([material_symbol('captive_portal'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !limited_federation_mode? && current_user.can?(:manage_federation) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -195,6 +195,8 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
draw(:api)
|
draw(:api)
|
||||||
|
|
||||||
|
draw(:fasp)
|
||||||
|
|
||||||
draw(:web_app)
|
draw(:web_app)
|
||||||
|
|
||||||
get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web, defaults: { any: '' }, format: false
|
get '/web/(*any)', to: redirect('/%{any}', status: 302), as: :web, defaults: { any: '' }, format: false
|
||||||
|
29
config/routes/fasp.rb
Normal file
29
config/routes/fasp.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
namespace :api, format: false do
|
||||||
|
namespace :fasp do
|
||||||
|
namespace :debug do
|
||||||
|
namespace :v0 do
|
||||||
|
namespace :callback do
|
||||||
|
resources :responses, only: [:create]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
resource :registration, only: [:create]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
namespace :admin do
|
||||||
|
namespace :fasp do
|
||||||
|
namespace :debug do
|
||||||
|
resources :callbacks, only: [:index, :destroy]
|
||||||
|
end
|
||||||
|
|
||||||
|
resources :providers, only: [:index, :show, :edit, :update, :destroy] do
|
||||||
|
resources :debug_calls, only: [:create]
|
||||||
|
|
||||||
|
resource :registration, only: [:new, :create]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
db/migrate/20241205103523_create_fasp_providers.rb
Normal file
21
db/migrate/20241205103523_create_fasp_providers.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateFaspProviders < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :fasp_providers do |t|
|
||||||
|
t.boolean :confirmed, null: false, default: false
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :base_url, null: false, index: { unique: true }
|
||||||
|
t.string :sign_in_url
|
||||||
|
t.string :remote_identifier, null: false
|
||||||
|
t.string :provider_public_key_pem, null: false
|
||||||
|
t.string :server_private_key_pem, null: false
|
||||||
|
t.jsonb :capabilities
|
||||||
|
t.jsonb :privacy_policy
|
||||||
|
t.string :contact_email
|
||||||
|
t.string :fediverse_account
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
db/migrate/20241206131513_create_fasp_debug_callbacks.rb
Normal file
13
db/migrate/20241206131513_create_fasp_debug_callbacks.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateFaspDebugCallbacks < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
create_table :fasp_debug_callbacks do |t|
|
||||||
|
t.references :fasp_provider, null: false, foreign_key: true
|
||||||
|
t.string :ip
|
||||||
|
t.text :request_body
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
27
db/schema.rb
27
db/schema.rb
@ -444,6 +444,32 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_16_224825) do
|
|||||||
t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true
|
t.index ["domain"], name: "index_email_domain_blocks_on_domain", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "fasp_debug_callbacks", force: :cascade do |t|
|
||||||
|
t.bigint "fasp_provider_id", null: false
|
||||||
|
t.string "ip"
|
||||||
|
t.text "request_body"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["fasp_provider_id"], name: "index_fasp_debug_callbacks_on_fasp_provider_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "fasp_providers", force: :cascade do |t|
|
||||||
|
t.boolean "confirmed", default: false, null: false
|
||||||
|
t.string "name", null: false
|
||||||
|
t.string "base_url", null: false
|
||||||
|
t.string "sign_in_url"
|
||||||
|
t.string "remote_identifier", null: false
|
||||||
|
t.string "provider_public_key_pem", null: false
|
||||||
|
t.string "server_private_key_pem", null: false
|
||||||
|
t.jsonb "capabilities"
|
||||||
|
t.jsonb "privacy_policy"
|
||||||
|
t.string "contact_email"
|
||||||
|
t.string "fediverse_account"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "favourites", force: :cascade do |t|
|
create_table "favourites", force: :cascade do |t|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
t.datetime "updated_at", precision: nil, null: false
|
||||||
@ -1283,6 +1309,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_16_224825) do
|
|||||||
add_foreign_key "custom_filter_statuses", "statuses", on_delete: :cascade
|
add_foreign_key "custom_filter_statuses", "statuses", on_delete: :cascade
|
||||||
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade
|
add_foreign_key "email_domain_blocks", "email_domain_blocks", column: "parent_id", on_delete: :cascade
|
||||||
|
add_foreign_key "fasp_debug_callbacks", "fasp_providers"
|
||||||
add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
|
add_foreign_key "favourites", "accounts", name: "fk_5eb6c2b873", on_delete: :cascade
|
||||||
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
add_foreign_key "favourites", "statuses", name: "fk_b0e856845e", on_delete: :cascade
|
||||||
add_foreign_key "featured_tags", "accounts", on_delete: :cascade
|
add_foreign_key "featured_tags", "accounts", on_delete: :cascade
|
||||||
|
7
spec/fabricators/fasp/debug_callback_fabricator.rb
Normal file
7
spec/fabricators/fasp/debug_callback_fabricator.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator('Fasp::DebugCallback') do
|
||||||
|
fasp_provider nil
|
||||||
|
ip 'MyString'
|
||||||
|
request_body 'MyText'
|
||||||
|
end
|
14
spec/fabricators/fasp/provider_fabricator.rb
Normal file
14
spec/fabricators/fasp/provider_fabricator.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator('Fasp::Provider') do
|
||||||
|
name 'MyString'
|
||||||
|
base_url 'MyString'
|
||||||
|
sign_in_url 'MyString'
|
||||||
|
remote_identifier 'MyString'
|
||||||
|
provider_public_key_pem 'MyString'
|
||||||
|
server_private_key_pem 'MyString'
|
||||||
|
capabilities ''
|
||||||
|
privacy_policy ''
|
||||||
|
contact_email 'MyString'
|
||||||
|
fediverse_account 'MyString'
|
||||||
|
end
|
7
spec/models/fasp/debug_callback_spec.rb
Normal file
7
spec/models/fasp/debug_callback_spec.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Fasp::DebugCallback do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
7
spec/models/fasp/provider_spec.rb
Normal file
7
spec/models/fasp/provider_spec.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Fasp::Provider do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
29
spec/policies/admin/fasp/provider_policy_spec.rb
Normal file
29
spec/policies/admin/fasp/provider_policy_spec.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Admin::Fasp::ProviderPolicy, type: :policy do
|
||||||
|
subject { described_class }
|
||||||
|
|
||||||
|
let(:user) { User.new }
|
||||||
|
|
||||||
|
permissions '.scope' do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions :show? do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions :create? do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions :update? do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions :destroy? do
|
||||||
|
pending "add some examples to (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
9
spec/requests/admin/fasp/debug/callbacks_spec.rb
Normal file
9
spec/requests/admin/fasp/debug/callbacks_spec.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin::Fasp::Debug::Callbacks' do
|
||||||
|
describe 'GET /index' do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
9
spec/requests/admin/fasp/debug_calls_spec.rb
Normal file
9
spec/requests/admin/fasp/debug_calls_spec.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin::Fasp::DebugCalls' do
|
||||||
|
describe 'GET /index' do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
9
spec/requests/admin/fasp/providers_spec.rb
Normal file
9
spec/requests/admin/fasp/providers_spec.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin::Fasp::Providers' do
|
||||||
|
describe 'GET /index' do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
9
spec/requests/admin/fasp/registrations_spec.rb
Normal file
9
spec/requests/admin/fasp/registrations_spec.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin::Fasp::Registrations' do
|
||||||
|
describe 'GET /index' do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Api::Fasp::Debug::V0::Callback::Responses' do
|
||||||
|
describe 'GET /index' do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
9
spec/requests/api/fasp/registrations_spec.rb
Normal file
9
spec/requests/api/fasp/registrations_spec.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Api::Fasp::Registrations' do
|
||||||
|
describe 'GET /index' do
|
||||||
|
pending "add some examples (or delete) #{__FILE__}"
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user