yiffspace 0.0.24 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/yiffspace/concerns/active_record_extensions.rb +3 -3
- data/lib/yiffspace/configuration.rb +0 -25
- data/lib/yiffspace/{railtie.rb → engine.rb} +9 -7
- data/lib/yiffspace/extensions/active_record/where_chain.rb +30 -30
- data/lib/yiffspace/include/active_record_extensions.rb +5 -0
- data/lib/yiffspace/include/all.rb +12 -0
- data/lib/yiffspace/include/api_methods.rb +5 -0
- data/lib/yiffspace/include/attribute_matchers.rb +5 -0
- data/lib/yiffspace/include/attribute_methods.rb +5 -0
- data/lib/yiffspace/include/concurrency_methods.rb +5 -0
- data/lib/yiffspace/include/conditional_includes.rb +5 -0
- data/lib/yiffspace/include/current_methods.rb +5 -0
- data/lib/yiffspace/include/has_bit_flags.rb +5 -0
- data/lib/yiffspace/include/user_class_methods.rb +5 -0
- data/lib/yiffspace/include/user_methods.rb +5 -0
- data/lib/yiffspace/include/user_name_methods.rb +5 -0
- data/lib/yiffspace/include/user_resolvable_methods.rb +5 -0
- data/lib/yiffspace/version.rb +1 -1
- data/lib/yiffspace.rb +5 -6
- metadata +15 -50
- data/app/helpers/yiff_space/auth_helper.rb +0 -6
- data/engines/auth/app/controllers/yiff_space/auth/application_controller.rb +0 -11
- data/engines/auth/app/controllers/yiff_space/auth/root_controller.rb +0 -49
- data/engines/auth/app/controllers/yiff_space/auth/webhook_controller.rb +0 -73
- data/engines/auth/app/views/yiff_space/auth/root/permissions.html.erb +0 -14
- data/engines/auth/config/routes.rb +0 -12
- data/lib/tasks/yiffspace_tasks.rake +0 -41
- data/lib/yiffspace/auth/api_user.rb +0 -43
- data/lib/yiffspace/auth/auth_info/anonymous.rb +0 -60
- data/lib/yiffspace/auth/auth_info.rb +0 -78
- data/lib/yiffspace/auth/client.rb +0 -56
- data/lib/yiffspace/auth/discord_info.rb +0 -65
- data/lib/yiffspace/auth/engine.rb +0 -58
- data/lib/yiffspace/auth/helper.rb +0 -174
- data/lib/yiffspace/auth/permissions.rb +0 -87
- data/lib/yiffspace/auth/set_client_name.rb +0 -15
- data/lib/yiffspace/auth/user_info/anonymous.rb +0 -72
- data/lib/yiffspace/auth/user_info.rb +0 -87
- data/lib/yiffspace/auth.rb +0 -44
- data/lib/yiffspace/core_ext/action_dispatch/set_auth_client/scoped.rb +0 -13
- data/lib/yiffspace/core_ext/action_dispatch/set_auth_client.rb +0 -13
- data/lib/yiffspace/core_ext/logto/named_session_storage.rb +0 -7
- data/lib/yiffspace/extensions/action_dispatch/set_auth_client/scoped.rb +0 -15
- data/lib/yiffspace/extensions/action_dispatch/set_auth_client.rb +0 -13
- data/lib/yiffspace/extensions/logto/named_session_storage.rb +0 -22
- data/lib/yiffspace/logto_management_client.rb +0 -136
- data/lib/yiffspace/serializers/anonymous_auth_info_serializer.rb +0 -23
- data/lib/yiffspace/serializers/anonymous_user_info_serializer.rb +0 -23
- data/lib/yiffspace/serializers/auth_info_serializer.rb +0 -23
- data/lib/yiffspace/serializers/discord_info_serializer.rb +0 -23
- data/lib/yiffspace/serializers/permissions_serializer.rb +0 -23
- data/lib/yiffspace/serializers/user_info_serializer.rb +0 -23
- data/lib/yiffspace/utils/user_deduper.rb +0 -66
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
<h1 class="title">Permissions</h1>
|
|
2
|
-
<% if logged_in? %>
|
|
3
|
-
<% if auth.permissions.any? %>
|
|
4
|
-
<ul class="permissions-list">
|
|
5
|
-
<% auth.permissions.each do |perm| %>
|
|
6
|
-
<li class="permission"><%= perm %></li>
|
|
7
|
-
<% end %>
|
|
8
|
-
</ul>
|
|
9
|
-
<% else %>
|
|
10
|
-
<p class="message">You have no permissions.</p>
|
|
11
|
-
<% end %>
|
|
12
|
-
<% else %>
|
|
13
|
-
<p class="message">You are not logged in.</p>
|
|
14
|
-
<% end %>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
YiffSpace::Auth::Engine.routes.draw do
|
|
4
|
-
constraints(YiffSpace::Auth::SetClientName.default) do
|
|
5
|
-
post(:webhook, controller: :webhook)
|
|
6
|
-
get(:cb, controller: :root)
|
|
7
|
-
get(:logout, controller: :root)
|
|
8
|
-
get(:permissions, controller: :root)
|
|
9
|
-
get(:debug, controller: :root) if YiffSpace::Auth.enable_debug_action?
|
|
10
|
-
root(action: :show, controller: :root, as: :auth)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
namespace(:yiffspace) do
|
|
4
|
-
desc("Report and interactively remove duplicate/orphaned Logto users left behind by " \
|
|
5
|
-
"get_or_create_user races. Prompts for confirmation before deleting each one. " \
|
|
6
|
-
"Pass AUTH_CLIENT=name to use a registered auth client other than :default " \
|
|
7
|
-
"(e.g. AUTH_CLIENT=yiffyapi_manage).")
|
|
8
|
-
task(dedupe_users: :environment) do
|
|
9
|
-
client_name = (ENV["AUTH_CLIENT"] || YiffSpace::Auth::DEFAULT_CLIENT_NAME).to_sym
|
|
10
|
-
management = YiffSpace::Auth[client_name].logto_management
|
|
11
|
-
|
|
12
|
-
puts("Scanning users via auth client #{client_name.inspect}...")
|
|
13
|
-
result = YiffSpace::Utils::UserDeduper.scan(management)
|
|
14
|
-
|
|
15
|
-
puts("\n#{result[:conflicts].size} discord id(s) with more than one linked user (ambiguous - not handled by this task, review manually):")
|
|
16
|
-
result[:conflicts].each do |discord_id, group|
|
|
17
|
-
puts(" discordId=#{discord_id}: #{group.map { |u| "#{u.id} (#{u.name})" }.join(', ')}")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
puts("\n#{result[:unresolved_orphans].size} orphaned user(s) with no matching linked account (not handled by this task, review manually):")
|
|
21
|
-
result[:unresolved_orphans].each do |user|
|
|
22
|
-
puts(" #{user.id} (name=#{user.name}, avatar=#{user.data.avatar})")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
puts("\n#{result[:deletable_orphans].size} confirmed duplicate(s) - orphan left by a losing get_or_create_user race:")
|
|
26
|
-
if result[:deletable_orphans].empty?
|
|
27
|
-
puts(" none found")
|
|
28
|
-
else
|
|
29
|
-
result[:deletable_orphans].each do |orphan, keeper|
|
|
30
|
-
print(" delete #{orphan.id} (name=#{orphan.name}, avatar=#{orphan.data.avatar}), keeping #{keeper.id} (name=#{keeper.name}) for discordId=#{YiffSpace::Utils::UserDeduper.discord_id_for(keeper)}? [y/N] ")
|
|
31
|
-
answer = $stdin.gets&.strip&.downcase
|
|
32
|
-
if answer == "y"
|
|
33
|
-
management.delete_user(orphan.id)
|
|
34
|
-
puts(" deleted #{orphan.id}")
|
|
35
|
-
else
|
|
36
|
-
puts(" skipped")
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module YiffSpace
|
|
4
|
-
module Auth
|
|
5
|
-
class ApiUser
|
|
6
|
-
attr_reader(:data)
|
|
7
|
-
|
|
8
|
-
def initialize(data)
|
|
9
|
-
@data = Utils::OpenHash.from(data)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
delegate(:id, :name, to: :data)
|
|
13
|
-
|
|
14
|
-
def discord_id
|
|
15
|
-
data.identities.discord.userId
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def created_at
|
|
19
|
-
Time.zone.at(data.createdAt)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def updated_at
|
|
23
|
-
Time.zone.at(data.updatedAt)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def last_sign_in_at
|
|
27
|
-
data.lastSignInAt.nil? ? nil : Time.zone.at(data.lastSignInAt)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def discord
|
|
31
|
-
DiscordInfo.new(data.identities.discord.details.rawData)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def avatar
|
|
35
|
-
Images::Avatar.get_for(discord_id, :discord)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def banner
|
|
39
|
-
Images::Banner.get_for(discord_id, :discord)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require("singleton")
|
|
4
|
-
|
|
5
|
-
module YiffSpace
|
|
6
|
-
module Auth
|
|
7
|
-
class AuthInfo
|
|
8
|
-
class Anonymous
|
|
9
|
-
include(::Singleton)
|
|
10
|
-
|
|
11
|
-
%i[id token].each do |attr|
|
|
12
|
-
define_method(attr) { |*, **| raise(NotImplementedError, "#{attr} is not present on anonymous auth") }
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def roles
|
|
16
|
-
[]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def permissions
|
|
20
|
-
Permissions.new([])
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def has_permission?(*)
|
|
24
|
-
false
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def anonymous?
|
|
28
|
-
true
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# this feels wrong, but it hopefully shouldn't break anything
|
|
32
|
-
def present?
|
|
33
|
-
false
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def blank?
|
|
37
|
-
true
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def serializable_hash(*)
|
|
41
|
-
nil
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def to_session
|
|
45
|
-
serializable_hash
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def self.from_json(*)
|
|
49
|
-
Anonymous.instance
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def self.from_session(data)
|
|
53
|
-
return nil if data.blank?
|
|
54
|
-
|
|
55
|
-
from_json(data)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module YiffSpace
|
|
4
|
-
module Auth
|
|
5
|
-
class AuthInfo
|
|
6
|
-
attr_reader(:id, :token, :roles, :permissions, :client_id)
|
|
7
|
-
|
|
8
|
-
# @param id String
|
|
9
|
-
# @param roles Array(String)
|
|
10
|
-
# @param permissions Array(String)
|
|
11
|
-
# @param token LogtoClient::AccessTokenClaims
|
|
12
|
-
# @param client_id String
|
|
13
|
-
def initialize(id:, roles:, permissions:, token:, client_id:)
|
|
14
|
-
raise(ArgumentError, "no id provided") if id.blank?
|
|
15
|
-
raise(ArgumentError, "no token provided") if token.blank?
|
|
16
|
-
raise(ArgumentError, "no client id provided") if client_id.blank?
|
|
17
|
-
|
|
18
|
-
@id = id
|
|
19
|
-
@token = token
|
|
20
|
-
@roles = Array(roles)
|
|
21
|
-
@permissions = Permissions.new(permissions, separator: YiffSpace::Auth.get_by_id(client_id).permissions_separator)
|
|
22
|
-
@client_id = client_id
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def anonymous?
|
|
26
|
-
false
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# this feels wrong, but it hopefully shouldn't break anything
|
|
30
|
-
def present?
|
|
31
|
-
true
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def blank?
|
|
35
|
-
false
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def has_permission?(name)
|
|
39
|
-
permissions.include?(name.to_s)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def serializable_hash(*)
|
|
43
|
-
{
|
|
44
|
-
"id" => id,
|
|
45
|
-
"token" => token.as_json,
|
|
46
|
-
"roles" => roles,
|
|
47
|
-
"permissions" => permissions.values,
|
|
48
|
-
"client_id" => client_id,
|
|
49
|
-
}
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def to_session
|
|
53
|
-
serializable_hash
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def self.from_json(data)
|
|
57
|
-
raise(ArgumentError, "invalid data") if data.blank?
|
|
58
|
-
|
|
59
|
-
data = JSON.parse(data) if data.is_a?(String)
|
|
60
|
-
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
61
|
-
|
|
62
|
-
new(
|
|
63
|
-
id: data.id,
|
|
64
|
-
token: data.token,
|
|
65
|
-
roles: data.roles,
|
|
66
|
-
permissions: data.permissions,
|
|
67
|
-
client_id: data.client_id,
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.from_session(data)
|
|
72
|
-
return nil if data.blank?
|
|
73
|
-
|
|
74
|
-
from_json(data)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative("../core_ext/logto/named_session_storage")
|
|
4
|
-
require("httparty")
|
|
5
|
-
|
|
6
|
-
module YiffSpace
|
|
7
|
-
module Auth
|
|
8
|
-
class Client
|
|
9
|
-
attr_accessor(:client_id, :client_secret, :scopes, :resource,
|
|
10
|
-
:redirect_uri, :server_url, :auth_session_key, :user_session_key,
|
|
11
|
-
:update_discord_images, :permissions_separator, :logto_webhook_secret)
|
|
12
|
-
|
|
13
|
-
attr_reader(:name)
|
|
14
|
-
|
|
15
|
-
def initialize(name)
|
|
16
|
-
@name = name.to_sym
|
|
17
|
-
@scopes = %i[openid offline_access profile roles identities custom_data]
|
|
18
|
-
@redirect_uri = "http://127.0.0.1:3000/auth/cb"
|
|
19
|
-
@server_url = "https://auth.yiff.space"
|
|
20
|
-
@auth_session_key = :"yiffspace_auth_#{name}"
|
|
21
|
-
@user_session_key = :"yiffspace_user_#{name}"
|
|
22
|
-
@update_discord_images = true
|
|
23
|
-
@permissions_separator = ":"
|
|
24
|
-
@logto_webhook_secret = nil
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def logto(controller)
|
|
28
|
-
LogtoClient.new(
|
|
29
|
-
config: LogtoClient::Config.new(
|
|
30
|
-
endpoint: server_url,
|
|
31
|
-
app_id: client_id,
|
|
32
|
-
app_secret: client_secret,
|
|
33
|
-
scopes: scopes,
|
|
34
|
-
resources: [resource].compact,
|
|
35
|
-
),
|
|
36
|
-
# Allow the client to redirect to other hosts (i.e. your Logto tenant)
|
|
37
|
-
navigate: ->(uri) { controller.redirect_to(uri, allow_other_host: true) },
|
|
38
|
-
# Controller has access to the session object
|
|
39
|
-
storage: LogtoClient::NamedSessionStorage.new("yiffspace", controller.session, app_id: @name),
|
|
40
|
-
)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def logto_management
|
|
44
|
-
@logto_management ||= LogtoManagementClient.new(self)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def fetch_discord_user(id)
|
|
48
|
-
response = HTTParty.get("https://discord.com/api/v10/users/#{id}", { headers: { "Authorization" => "Bot #{YiffSpace.config.discord_bot_token}" } })
|
|
49
|
-
return nil if response.code == 404
|
|
50
|
-
raise("failed to fetch discord user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
|
|
51
|
-
|
|
52
|
-
DiscordInfo.new(response.parsed_response)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module YiffSpace
|
|
4
|
-
module Auth
|
|
5
|
-
class DiscordInfo
|
|
6
|
-
# locale, mfa_enabled, and premium_type are not present for legacy users backfilled from api data,
|
|
7
|
-
# we don't need that data either way
|
|
8
|
-
ATTRIBUTES = %i[id flags avatar banner username global_name public_flags discriminator].freeze
|
|
9
|
-
attr_reader(:data, *ATTRIBUTES)
|
|
10
|
-
|
|
11
|
-
def initialize(options = {}, **kwargs)
|
|
12
|
-
@data = options.merge(kwargs).with_indifferent_access
|
|
13
|
-
ATTRIBUTES.each do |attr|
|
|
14
|
-
instance_variable_set("@#{attr}", @data[attr])
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
return unless YiffSpace.config.auth.update_discord_images
|
|
18
|
-
|
|
19
|
-
last_avatar = Rails.cache.fetch("yiffspace:auth:discord_avatar_update:#{id}")
|
|
20
|
-
last_banner = Rails.cache.fetch("yiffspace:auth:discord_banner_update:#{id}")
|
|
21
|
-
|
|
22
|
-
if last_avatar != avatar
|
|
23
|
-
Rails.cache.write("yiffspace:auth:discord_avatar_update:#{id}", avatar, expires_in: 30.days)
|
|
24
|
-
update_avatar
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
return unless last_banner != banner
|
|
28
|
-
|
|
29
|
-
Rails.cache.write("yiffspace:auth:discord_banner_update:#{id}", banner, expires_in: 30.days)
|
|
30
|
-
update_banner
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def update_avatar
|
|
34
|
-
Images::Avatar.get_for(id, :discord).update(avatar)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def update_banner
|
|
38
|
-
Images::Banner.get_for(id, :discord).update(banner)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def serializable_hash(*)
|
|
42
|
-
ATTRIBUTES.to_h { |key| [key.to_s, instance_variable_get("@#{key}")] }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def to_session
|
|
46
|
-
serializable_hash
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.from_json(data)
|
|
50
|
-
raise(ArgumentError, "invalid data") if data.blank?
|
|
51
|
-
|
|
52
|
-
data = JSON.parse(data) if data.is_a?(String)
|
|
53
|
-
data = ::YiffSpace::Utils::OpenHash.from(data)
|
|
54
|
-
|
|
55
|
-
new(data)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.from_session(data)
|
|
59
|
-
return nil if data.blank?
|
|
60
|
-
|
|
61
|
-
from_json(data)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require("rails")
|
|
4
|
-
|
|
5
|
-
module YiffSpace
|
|
6
|
-
module Auth
|
|
7
|
-
class Engine < ::Rails::Engine
|
|
8
|
-
isolate_namespace(YiffSpace::Auth)
|
|
9
|
-
config.root = File.expand_path("../../../engines/auth", __dir__)
|
|
10
|
-
shared_root = File.expand_path("../../../app", __dir__)
|
|
11
|
-
shared_controllers = "#{shared_root}/controllers"
|
|
12
|
-
shared_helpers = "#{shared_root}/helpers"
|
|
13
|
-
shared_models = "#{shared_root}/models"
|
|
14
|
-
shared_views = "#{shared_root}/views"
|
|
15
|
-
shared_assets = "#{shared_root}/assets"
|
|
16
|
-
|
|
17
|
-
paths["app/controllers"] << shared_controllers
|
|
18
|
-
paths["app/helpers"] << shared_helpers
|
|
19
|
-
paths["app/models"] << shared_models
|
|
20
|
-
paths["app/views"] << shared_views
|
|
21
|
-
paths["app/assets"] << shared_assets
|
|
22
|
-
|
|
23
|
-
config.autoload_paths += [shared_controllers, shared_helpers, shared_models]
|
|
24
|
-
config.eager_load_paths += [shared_controllers, shared_helpers, shared_models]
|
|
25
|
-
|
|
26
|
-
initializer("yiffspace.auth.asset_paths") do |app|
|
|
27
|
-
if app.config.respond_to?(:assets)
|
|
28
|
-
app.config.assets.paths << "#{shared_assets}/config"
|
|
29
|
-
app.config.assets.precompile += %w[yiffspace/application.css]
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
class << self
|
|
34
|
-
def for(name)
|
|
35
|
-
@instances ||= {}
|
|
36
|
-
@instances[name.to_sym] ||= begin
|
|
37
|
-
subclass = Class.new(self)
|
|
38
|
-
subclass.engine_name("yiffspace_auth_#{name}")
|
|
39
|
-
# Inherit isolation settings that aren't copied from the parent class
|
|
40
|
-
subclass.instance_variable_set(:@isolated, true)
|
|
41
|
-
subclass.routes.default_scope = { module: "yiff_space/auth" }
|
|
42
|
-
subclass.routes.draw do
|
|
43
|
-
constraints(SetClientName.new(name)) do
|
|
44
|
-
post(:webhook, controller: :webhook, action: :create)
|
|
45
|
-
get(:cb, controller: :root)
|
|
46
|
-
get(:logout, controller: :root)
|
|
47
|
-
get(:permissions, controller: :root)
|
|
48
|
-
get(:debug, controller: :root) if ::YiffSpace::Auth.enable_debug_action?
|
|
49
|
-
root(action: :show, controller: :root, as: :auth)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
subclass
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require("active_support/concern")
|
|
4
|
-
|
|
5
|
-
module YiffSpace
|
|
6
|
-
module Auth
|
|
7
|
-
module Helper
|
|
8
|
-
extend(ActiveSupport::Concern)
|
|
9
|
-
|
|
10
|
-
module ClassMethods
|
|
11
|
-
def set_client_name(name) # rubocop:disable Naming/AccessorMethodName
|
|
12
|
-
before_action do |controller|
|
|
13
|
-
controller.request.env["yiffspace.auth.client_name"] = name
|
|
14
|
-
if controller.respond_to?(:yiffspace_client_name=)
|
|
15
|
-
controller.yiffspace_client_name = name
|
|
16
|
-
elsif controller.respond_to?(:client_name=)
|
|
17
|
-
controller.client_name = name
|
|
18
|
-
elsif controller.respond_to?(:helpers)
|
|
19
|
-
if controller.helpers.respond_to?(:yiffspace_client_name=)
|
|
20
|
-
controller.helpers.yiffspace_client_name = name
|
|
21
|
-
elsif controller.helpers.respond_to?(:client_name=)
|
|
22
|
-
controller.helpers.client_name = name
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def auth_raw
|
|
30
|
-
session[auth_client_config.auth_session_key]
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def auth
|
|
34
|
-
return AuthInfo::Anonymous.instance if auth_raw.blank?
|
|
35
|
-
|
|
36
|
-
AuthInfo.from_session(auth_raw)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def auth?
|
|
40
|
-
auth_raw.present? && !auth.anonymous?
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def auth=(value)
|
|
44
|
-
value = nil if value.is_a?(AuthInfo::Anonymous)
|
|
45
|
-
session[auth_client_config.auth_session_key] = value&.to_session
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def reset_auth!
|
|
49
|
-
session.delete(auth_client_config.auth_session_key)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def user_raw
|
|
53
|
-
session[auth_client_config.user_session_key]
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def user
|
|
57
|
-
return UserInfo::Anonymous.instance if user_raw.blank?
|
|
58
|
-
|
|
59
|
-
UserInfo.from_session(user_raw)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def user?
|
|
63
|
-
user_raw.present? && !user.anonymous?
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def user=(value)
|
|
67
|
-
value = nil if value.is_a?(UserInfo::Anonymous)
|
|
68
|
-
session[auth_client_config.user_session_key] = value&.to_session
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def reset_user!
|
|
72
|
-
session.delete(auth_client_config.user_session_key)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def full_reset!
|
|
76
|
-
reset_auth!
|
|
77
|
-
reset_user!
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def require_auth(path)
|
|
81
|
-
redirect_to(path) unless logged_in?
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def logged_in?
|
|
85
|
-
auth? && user?
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def has_permission?(name)
|
|
89
|
-
return false unless logged_in?
|
|
90
|
-
|
|
91
|
-
auth.permissions.has?(name)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
DIRTY_FLAG_KEY = "yiffspace:auth:dirty:%s"
|
|
95
|
-
|
|
96
|
-
# Checks the dirty flag written by the Logto webhook handler. If set, re-fetches
|
|
97
|
-
# the user's current roles and permissions from the Logto Management API and
|
|
98
|
-
# rewrites the session — without waiting for the access token to expire.
|
|
99
|
-
# Call this as a before_action in any controller that needs instant revocation.
|
|
100
|
-
def sync_auth_if_dirty!
|
|
101
|
-
return unless auth?
|
|
102
|
-
|
|
103
|
-
flag_key = format(DIRTY_FLAG_KEY, auth.id)
|
|
104
|
-
return unless Rails.cache.exist?(flag_key)
|
|
105
|
-
|
|
106
|
-
Rails.cache.delete(flag_key)
|
|
107
|
-
|
|
108
|
-
management = auth_client_config.logto_management
|
|
109
|
-
api_user = management.get_user_by_id(auth.id)
|
|
110
|
-
|
|
111
|
-
if api_user.nil? || api_user.data["isSuspended"]
|
|
112
|
-
full_reset!
|
|
113
|
-
return
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
roles = management.get_user_roles(auth.id)
|
|
117
|
-
permissions = roles.flat_map { |role| management.get_role_scopes(role["id"]) }
|
|
118
|
-
.pluck("name")
|
|
119
|
-
.uniq
|
|
120
|
-
|
|
121
|
-
self.auth = AuthInfo.new(
|
|
122
|
-
id: auth.id,
|
|
123
|
-
token: auth.token,
|
|
124
|
-
roles: roles.pluck("name"),
|
|
125
|
-
permissions: permissions,
|
|
126
|
-
client_id: auth.client_id,
|
|
127
|
-
)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def url_helpers
|
|
131
|
-
YiffSpace::Auth::Engine.for(client_name).routes.url_helpers
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Returns the Auth::Client for the current request. In auth engine controllers this is
|
|
135
|
-
# resolved from the routing default set by Engine.for; in host app controllers it falls
|
|
136
|
-
# back to the default registered client. Override in your controller to choose a specific
|
|
137
|
-
# client when multiple are registered.
|
|
138
|
-
def auth_client_config
|
|
139
|
-
client_name = self.client_name
|
|
140
|
-
client_name.present? ? YiffSpace::Auth[client_name.to_sym] : YiffSpace::Auth.default
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def client_name
|
|
144
|
-
respond_to?(:request, true) && request.env[CLIENT_NAME_ENV]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def client_name=(value)
|
|
148
|
-
request.env[CLIENT_NAME_ENV] = value.to_sym
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
module Scoped
|
|
152
|
-
extend(ActiveSupport::Concern)
|
|
153
|
-
include(Helper)
|
|
154
|
-
|
|
155
|
-
included do
|
|
156
|
-
private(*Helper.instance_methods(false))
|
|
157
|
-
private_class_method(*Helper::ClassMethods.instance_methods(false))
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
Helper.instance_methods(false).each do |name|
|
|
161
|
-
define_method("yiffspace_#{name}") { |*args, **kwargs, &block| send(name, *args, **kwargs, &block) }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
module ClassMethods
|
|
165
|
-
include(Helper::ClassMethods)
|
|
166
|
-
|
|
167
|
-
Helper::ClassMethods.instance_methods(false).each do |name|
|
|
168
|
-
define_method("yiffspace_#{name}") { |*args, **kwargs, &block| send(name, *args, **kwargs, &block) }
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module YiffSpace
|
|
4
|
-
module Auth
|
|
5
|
-
class Permissions
|
|
6
|
-
attr_accessor(:values)
|
|
7
|
-
|
|
8
|
-
delegate(:each, to: :values)
|
|
9
|
-
|
|
10
|
-
include(Enumerable)
|
|
11
|
-
|
|
12
|
-
def initialize(perms, separator: ":")
|
|
13
|
-
@values = perms
|
|
14
|
-
@tree = {}
|
|
15
|
-
@separator = separator
|
|
16
|
-
|
|
17
|
-
perms.each do |perm|
|
|
18
|
-
current = @tree
|
|
19
|
-
parts = perm.split(separator)
|
|
20
|
-
|
|
21
|
-
parts.each do |part|
|
|
22
|
-
current[part] ||= {}
|
|
23
|
-
current = current[part]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# mark leaf
|
|
27
|
-
current[:__leaf__] = true
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def has?(perm)
|
|
32
|
-
current = @tree
|
|
33
|
-
parts = perm.split(@separator)
|
|
34
|
-
|
|
35
|
-
parts.each do |part|
|
|
36
|
-
return false unless current[part]
|
|
37
|
-
|
|
38
|
-
current = current[part]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
current[:__leaf__] == true
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
alias include? has?
|
|
45
|
-
|
|
46
|
-
def method_missing(name, *_args)
|
|
47
|
-
str = name.to_s
|
|
48
|
-
|
|
49
|
-
if str.end_with?("?")
|
|
50
|
-
key = str[0..-2]
|
|
51
|
-
return @tree[key]&.dig(:__leaf__) == true
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
if @tree.key?(str)
|
|
55
|
-
self.class.new_from_subtree(@tree[str], @separator)
|
|
56
|
-
else
|
|
57
|
-
# return empty node instead of raising
|
|
58
|
-
self.class.new([], separator: @separator)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def respond_to_missing?(_name, _include_private = false)
|
|
63
|
-
true
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.new_from_subtree(tree, separator)
|
|
67
|
-
obj = allocate
|
|
68
|
-
obj.instance_variable_set(:@tree, tree)
|
|
69
|
-
obj.instance_variable_set(:@values, leaves_from_tree(tree))
|
|
70
|
-
obj.instance_variable_set(:@separator, separator)
|
|
71
|
-
obj
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def self.leaves_from_tree(node, prefix = "")
|
|
75
|
-
result = []
|
|
76
|
-
node.each do |key, subtree|
|
|
77
|
-
next if key == :__leaf__
|
|
78
|
-
|
|
79
|
-
path = prefix.empty? ? key : "#{prefix}.#{key}"
|
|
80
|
-
result << path if subtree[:__leaf__]
|
|
81
|
-
result.concat(leaves_from_tree(subtree, path))
|
|
82
|
-
end
|
|
83
|
-
result
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|