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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/lib/yiffspace/concerns/active_record_extensions.rb +3 -3
  4. data/lib/yiffspace/configuration.rb +0 -25
  5. data/lib/yiffspace/{railtie.rb → engine.rb} +9 -7
  6. data/lib/yiffspace/extensions/active_record/where_chain.rb +30 -30
  7. data/lib/yiffspace/include/active_record_extensions.rb +5 -0
  8. data/lib/yiffspace/include/all.rb +12 -0
  9. data/lib/yiffspace/include/api_methods.rb +5 -0
  10. data/lib/yiffspace/include/attribute_matchers.rb +5 -0
  11. data/lib/yiffspace/include/attribute_methods.rb +5 -0
  12. data/lib/yiffspace/include/concurrency_methods.rb +5 -0
  13. data/lib/yiffspace/include/conditional_includes.rb +5 -0
  14. data/lib/yiffspace/include/current_methods.rb +5 -0
  15. data/lib/yiffspace/include/has_bit_flags.rb +5 -0
  16. data/lib/yiffspace/include/user_class_methods.rb +5 -0
  17. data/lib/yiffspace/include/user_methods.rb +5 -0
  18. data/lib/yiffspace/include/user_name_methods.rb +5 -0
  19. data/lib/yiffspace/include/user_resolvable_methods.rb +5 -0
  20. data/lib/yiffspace/version.rb +1 -1
  21. data/lib/yiffspace.rb +5 -6
  22. metadata +15 -50
  23. data/app/helpers/yiff_space/auth_helper.rb +0 -6
  24. data/engines/auth/app/controllers/yiff_space/auth/application_controller.rb +0 -11
  25. data/engines/auth/app/controllers/yiff_space/auth/root_controller.rb +0 -49
  26. data/engines/auth/app/controllers/yiff_space/auth/webhook_controller.rb +0 -73
  27. data/engines/auth/app/views/yiff_space/auth/root/permissions.html.erb +0 -14
  28. data/engines/auth/config/routes.rb +0 -12
  29. data/lib/tasks/yiffspace_tasks.rake +0 -41
  30. data/lib/yiffspace/auth/api_user.rb +0 -43
  31. data/lib/yiffspace/auth/auth_info/anonymous.rb +0 -60
  32. data/lib/yiffspace/auth/auth_info.rb +0 -78
  33. data/lib/yiffspace/auth/client.rb +0 -56
  34. data/lib/yiffspace/auth/discord_info.rb +0 -65
  35. data/lib/yiffspace/auth/engine.rb +0 -58
  36. data/lib/yiffspace/auth/helper.rb +0 -174
  37. data/lib/yiffspace/auth/permissions.rb +0 -87
  38. data/lib/yiffspace/auth/set_client_name.rb +0 -15
  39. data/lib/yiffspace/auth/user_info/anonymous.rb +0 -72
  40. data/lib/yiffspace/auth/user_info.rb +0 -87
  41. data/lib/yiffspace/auth.rb +0 -44
  42. data/lib/yiffspace/core_ext/action_dispatch/set_auth_client/scoped.rb +0 -13
  43. data/lib/yiffspace/core_ext/action_dispatch/set_auth_client.rb +0 -13
  44. data/lib/yiffspace/core_ext/logto/named_session_storage.rb +0 -7
  45. data/lib/yiffspace/extensions/action_dispatch/set_auth_client/scoped.rb +0 -15
  46. data/lib/yiffspace/extensions/action_dispatch/set_auth_client.rb +0 -13
  47. data/lib/yiffspace/extensions/logto/named_session_storage.rb +0 -22
  48. data/lib/yiffspace/logto_management_client.rb +0 -136
  49. data/lib/yiffspace/serializers/anonymous_auth_info_serializer.rb +0 -23
  50. data/lib/yiffspace/serializers/anonymous_user_info_serializer.rb +0 -23
  51. data/lib/yiffspace/serializers/auth_info_serializer.rb +0 -23
  52. data/lib/yiffspace/serializers/discord_info_serializer.rb +0 -23
  53. data/lib/yiffspace/serializers/permissions_serializer.rb +0 -23
  54. data/lib/yiffspace/serializers/user_info_serializer.rb +0 -23
  55. data/lib/yiffspace/utils/user_deduper.rb +0 -66
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YiffSpace
4
- module Auth
5
- class SetClientName < Utils::SetEnvConstraint
6
- def initialize(value)
7
- super(CLIENT_NAME_ENV, value.to_sym)
8
- end
9
-
10
- def self.default
11
- new(DEFAULT_CLIENT_NAME)
12
- end
13
- end
14
- end
15
- end
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("singleton")
4
-
5
- module YiffSpace
6
- module Auth
7
- class UserInfo
8
- class Anonymous
9
- include(::Singleton)
10
-
11
- %i[id user discord avatar].each do |attr|
12
- define_method(attr) { |*, **| raise(NotImplementedError, "#{attr} is not present on anonymous user") }
13
- end
14
-
15
- def username
16
- "anonymous"
17
- end
18
-
19
- def display_name
20
- "Anonymous"
21
- end
22
-
23
- def avatar
24
- nil
25
- end
26
-
27
- def avatar_url
28
- nil
29
- end
30
-
31
- def banner
32
- nil
33
- end
34
-
35
- def banner_url
36
- nil
37
- end
38
-
39
- def anonymous?
40
- true
41
- end
42
-
43
- # this feels wrong, but it hopefully shouldn't break anything
44
- def present?
45
- false
46
- end
47
-
48
- def blank?
49
- true
50
- end
51
-
52
- def serializable_hash(*)
53
- nil
54
- end
55
-
56
- def to_session
57
- serializable_hash
58
- end
59
-
60
- def self.from_json(*)
61
- Anonymous.instance
62
- end
63
-
64
- def self.from_session(data)
65
- return nil if data.blank?
66
-
67
- from_json(data)
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YiffSpace
4
- module Auth
5
- class UserInfo
6
- attr_reader(:id, :user, :discord)
7
-
8
- # @param id String
9
- # @param user LogtoCore::UserInfoResponse
10
- # @param discord Hash
11
- def initialize(id:, user:, discord:)
12
- raise(ArgumentError, "no id provided") if id.blank?
13
- raise(ArgumentError, "no user provided") if user.blank?
14
-
15
- @id = id
16
- @user = user
17
- @discord = DiscordInfo.from_json(discord)
18
- end
19
-
20
- def anonymous?
21
- false
22
- end
23
-
24
- # this feels wrong, but it hopefully shouldn't break anything
25
- def present?
26
- true
27
- end
28
-
29
- def blank?
30
- false
31
- end
32
-
33
- def avatar(type = nil)
34
- type.present? ? Images::Avatar.get_for(id, type) : Images::Avatar.default_for(id)
35
- end
36
-
37
- def avatar_url(type = nil)
38
- avatar(type).url
39
- end
40
-
41
- def banner(type = nil)
42
- type.present? ? Images::Banner.get_for(id, type) : Images::Banner.default_for(id)
43
- end
44
-
45
- def banner_url(type = nil)
46
- banner(type).url
47
- end
48
-
49
- delegate(:username, to: :discord)
50
-
51
- def display_name
52
- discord.global_name
53
- end
54
-
55
- def serializable_hash(_options = {})
56
- {
57
- "id" => id,
58
- "discord" => discord.serializable_hash,
59
- "user" => user.as_json,
60
- }
61
- end
62
-
63
- def to_session
64
- serializable_hash.without("discord").merge(discord: discord.to_session)
65
- end
66
-
67
- def self.from_json(data)
68
- raise(ArgumentError, "invalid data") if data.blank?
69
-
70
- data = JSON.parse(data) if data.is_a?(String)
71
- data = ::YiffSpace::Utils::OpenHash.from(data)
72
-
73
- new(
74
- id: data.id,
75
- discord: data.discord,
76
- user: data.user,
77
- )
78
- end
79
-
80
- def self.from_session(data)
81
- return nil if data.blank?
82
-
83
- from_json(data)
84
- end
85
- end
86
- end
87
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YiffSpace
4
- module Auth
5
- CLIENT_NAME_ENV = "yiffspace.auth.client_name"
6
- DEFAULT_CLIENT_NAME = :default
7
-
8
- @clients = {}
9
- @enable_debug_action = false
10
-
11
- module_function
12
-
13
- def register(name, &block)
14
- client = Client.new(name)
15
- block&.call(client)
16
- @clients[name.to_sym] = client
17
- client
18
- end
19
-
20
- def [](name)
21
- @clients[name.to_sym] || raise(KeyError, "unknown auth client: #{name.inspect}")
22
- end
23
-
24
- def default
25
- @clients[DEFAULT_CLIENT_NAME] || raise("no default client configured")
26
- end
27
-
28
- def get_by_id(id)
29
- @clients.values.find { |c| c.client_id == id } || raise(ArgumentError, "unable to find client with id: #{id}")
30
- end
31
-
32
- def enable_debug_action?
33
- @enable_debug_action
34
- end
35
-
36
- def enable_debug_action!
37
- @enable_debug_action = true
38
- end
39
-
40
- def disable_debug_action!
41
- @enable_debug_action = false
42
- end
43
- end
44
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative("../../../extensions/action_dispatch/set_auth_client/scoped")
4
-
5
- module ActionDispatch
6
- module Routing
7
- class Mapper
8
- module Scoping
9
- include(YiffSpace::Extensions::ActionDispatch::SetAuthClient::Scoped)
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative("../../extensions/action_dispatch/set_auth_client")
4
-
5
- module ActionDispatch
6
- module Routing
7
- class Mapper
8
- module Scoping
9
- include(YiffSpace::Extensions::ActionDispatch::SetAuthClient)
10
- end
11
- end
12
- end
13
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative("../../extensions/logto/named_session_storage")
4
-
5
- class LogtoClient
6
- NamedSessionStorage = YiffSpace::Extensions::Logto::NamedSessionStorage
7
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YiffSpace
4
- module Extensions
5
- module ActionDispatch
6
- module SetAuthClient
7
- module Scoped
8
- def yiffspace_set_auth_client(name, &)
9
- constraints(Auth::SetClientName.new(name), &)
10
- end
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YiffSpace
4
- module Extensions
5
- module ActionDispatch
6
- module SetAuthClient
7
- def set_auth_client(name, &)
8
- constraints(Auth::SetClientName.new(name), &)
9
- end
10
- end
11
- end
12
- end
13
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("logto/client")
4
-
5
- module YiffSpace
6
- module Extensions
7
- module Logto
8
- class NamedSessionStorage < LogtoClient::SessionStorage
9
- def initialize(name, session, app_id: nil)
10
- super(session, app_id: app_id)
11
- @name = name
12
- end
13
-
14
- protected
15
-
16
- def get_session_key(key)
17
- "#{@name}_#{@app_id || 'default'}_#{key}"
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,136 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_support/core_ext/integer/time")
4
- require("httparty")
5
-
6
- module YiffSpace
7
- class LogtoManagementClient
8
- attr_reader(:auth)
9
-
10
- def initialize(auth)
11
- @auth = auth
12
- end
13
-
14
- def get_token # rubocop:disable Naming/AccessorMethodName
15
- if @_cached_token
16
- return @_cached_token if @_cache_expire_time > Time.current
17
-
18
- @_cached_token = nil
19
- @_cache_expire_time = nil
20
- end
21
- response = HTTParty.post("#{auth.server_url}/oidc/token", {
22
- body: {
23
- grant_type: "client_credentials",
24
- client_id: YiffSpace.config.logto_api_client_id,
25
- client_secret: YiffSpace.config.logto_api_client_secret,
26
- resource: YiffSpace.config.logto_api_resource,
27
- scope: "all",
28
- },
29
- })
30
- raise("failed to get access token: #{response.to_json}") unless response.key?("access_token")
31
-
32
- @_cached_token = response["access_token"]
33
- @_cache_expire_time = response["expires_in"].to_i.seconds.from_now
34
- response["access_token"]
35
- end
36
-
37
- def get_user(id)
38
- response = HTTParty.get("#{auth.server_url}/api/users?discordId=#{id}", { headers: { "Authorization" => "Bearer #{get_token}" } })
39
- return nil if response.code == 404 || (response.parsed_response.is_a?(Array) && response.parsed_response.empty?)
40
- raise("failed to get user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200 || !response.parsed_response.is_a?(Array)
41
-
42
- Utils::TraceLogger.warn("LogtoManagementClient", "query discordId=#{id} returned more than one user:\n#{response.parsed_response.inspect}") if response.parsed_response.length > 1
43
-
44
- Auth::ApiUser.new(response.parsed_response.first)
45
- end
46
-
47
- def create_user(id)
48
- details = auth.fetch_discord_user(id)&.data
49
- raise("invalid discord user: #{id}") if details.blank?
50
-
51
- response = HTTParty.post("#{auth.server_url}/api/users", {
52
- headers: { "Authorization" => "Bearer #{get_token}", "Content-Type" => "application/json" },
53
- body: { avatar: "https://cdn.discordapp.com/avatars/#{details['id']}/#{details['avatar']}", name: details["username"] }.to_json,
54
- })
55
- raise("failed to create user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
56
-
57
- response2 = HTTParty.put("#{auth.server_url}/api/users/#{response.parsed_response['id']}/identities/discord", {
58
- headers: { "Authorization" => "Bearer #{get_token}", "Content-Type" => "application/json" },
59
- body: {
60
- userId: details["id"],
61
- details: {
62
- id: details["id"],
63
- name: details["username"],
64
- avatar: "https://cdn.discordapp.com/avatars/#{details['id']}/#{details['avatar']}",
65
- rawData: details,
66
- },
67
- }.to_json,
68
- })
69
- raise("failed to add discord identity: #{response2.code} #{response2.message}\n#{response2.parsed_response.inspect}") if response2.code != 201
70
-
71
- get_user(id) # force fresh fetch to ensure identity data is included properly
72
- end
73
-
74
- def get_or_create_user(id)
75
- get_user(id) || create_user(id)
76
- end
77
-
78
- # Pages through every user in the tenant. Used by maintenance tasks (e.g. the
79
- # dedupe_users rake task) - there's no discordId to filter by when scanning the
80
- # whole user base for duplicates/orphans.
81
- def list_users(page_size: 100)
82
- users = []
83
- page = 1
84
- loop do
85
- response = HTTParty.get("#{auth.server_url}/api/users", {
86
- headers: { "Authorization" => "Bearer #{get_token}" },
87
- query: { page: page, page_size: page_size },
88
- })
89
- raise("failed to list users: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200 || !response.parsed_response.is_a?(Array)
90
-
91
- batch = response.parsed_response
92
- users.concat(batch.map { |u| Auth::ApiUser.new(u) })
93
- break if batch.length < page_size
94
-
95
- page += 1
96
- end
97
- users
98
- end
99
-
100
- def delete_user(logto_id)
101
- response = HTTParty.delete("#{auth.server_url}/api/users/#{logto_id}", { headers: { "Authorization" => "Bearer #{get_token}" } })
102
- return true if [204, 404].include?(response.code)
103
-
104
- raise("failed to delete user #{logto_id}: #{response.code} #{response.message}\n#{response.parsed_response.inspect}")
105
- end
106
-
107
- def get_user_by_id(logto_id)
108
- response = HTTParty.get("#{auth.server_url}/api/users/#{logto_id}", { headers: { "Authorization" => "Bearer #{get_token}" } })
109
- return nil if response.code == 404
110
- raise("failed to get user: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
111
-
112
- Auth::ApiUser.new(response.parsed_response)
113
- end
114
-
115
- def get_user_roles(logto_id)
116
- response = HTTParty.get("#{auth.server_url}/api/users/#{logto_id}/roles", { headers: { "Authorization" => "Bearer #{get_token}" } })
117
- raise("failed to get user roles: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
118
-
119
- response.parsed_response
120
- end
121
-
122
- def get_role_scopes(role_id)
123
- response = HTTParty.get("#{auth.server_url}/api/roles/#{role_id}/scopes", { headers: { "Authorization" => "Bearer #{get_token}" } })
124
- raise("failed to get role scopes: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
125
-
126
- response.parsed_response
127
- end
128
-
129
- def get_users_with_role(role_id)
130
- response = HTTParty.get("#{auth.server_url}/api/roles/#{role_id}/users", { headers: { "Authorization" => "Bearer #{get_token}" } })
131
- raise("failed to get users with role: #{response.code} #{response.message}\n#{response.parsed_response.inspect}") if response.code != 200
132
-
133
- response.parsed_response
134
- end
135
- end
136
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_job")
4
-
5
- module YiffSpace
6
- module Serializers
7
- class AnonymousAuthInfoSerializer < ActiveJob::Serializers::ObjectSerializer
8
- def serialize(_)
9
- super({})
10
- end
11
-
12
- def deserialize(_)
13
- Auth::AuthInfo::Anonymous.instance
14
- end
15
-
16
- private
17
-
18
- def klass
19
- Auth::AuthInfo::Anonymous
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_job")
4
-
5
- module YiffSpace
6
- module Serializers
7
- class AnonymousUserInfoSerializer < ActiveJob::Serializers::ObjectSerializer
8
- def serialize(_)
9
- super({})
10
- end
11
-
12
- def deserialize(_)
13
- Auth::UserInfo::Anonymous.instance
14
- end
15
-
16
- private
17
-
18
- def klass
19
- Auth::UserInfo::Anonymous
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_job")
4
-
5
- module YiffSpace
6
- module Serializers
7
- class AuthInfoSerializer < ActiveJob::Serializers::ObjectSerializer
8
- def serialize(arg)
9
- super(**arg.serializable_hash)
10
- end
11
-
12
- def deserialize(arg)
13
- Auth::AuthInfo.from_json(arg)
14
- end
15
-
16
- private
17
-
18
- def klass
19
- Auth::AuthInfo
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_job")
4
-
5
- module YiffSpace
6
- module Serializers
7
- class DiscordInfoSerializer < ActiveJob::Serializers::ObjectSerializer
8
- def serialize(arg)
9
- super(**arg.serializable_hash)
10
- end
11
-
12
- def deserialize(arg)
13
- Auth::DiscordInfo.from_json(arg)
14
- end
15
-
16
- private
17
-
18
- def klass
19
- Auth::DiscordInfo
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_job")
4
-
5
- module YiffSpace
6
- module Serializers
7
- class PermissionsSerializer < ActiveJob::Serializers::ObjectSerializer
8
- def serialize(arg)
9
- super("value" => arg.value)
10
- end
11
-
12
- def deserialize(arg)
13
- Auth::Permissions.new(arg["value"])
14
- end
15
-
16
- private
17
-
18
- def klass
19
- Auth::Permissions
20
- end
21
- end
22
- end
23
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require("active_job")
4
-
5
- module YiffSpace
6
- module Serializers
7
- class UserInfoSerializer < ActiveJob::Serializers::ObjectSerializer
8
- def serialize(arg)
9
- super(**arg.serializable_hash)
10
- end
11
-
12
- def deserialize(arg)
13
- Auth::UserInfo.from_json(arg)
14
- end
15
-
16
- private
17
-
18
- def klass
19
- Auth::UserInfo
20
- end
21
- end
22
- end
23
- end
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module YiffSpace
4
- module Utils
5
- # get_or_create_user is check-then-act (query Logto, then create) with no locking
6
- # of its own - two concurrent calls for a discord id that doesn't exist yet will
7
- # both create a user, and the loser's discord-identity-attach step fails once the
8
- # winner's identity claims it first. That leaves an identity-less "orphan" user
9
- # behind in Logto. This scans the whole user base to find those orphans (and any
10
- # other duplicate/ambiguous state) so they can be cleaned up.
11
- module UserDeduper
12
- # create_user sets a new user's avatar to the discord CDN URL for the discord id
13
- # it was created for, before the identity-attach step runs - so even an orphan
14
- # that never got its identity attached still reveals which discord id it was
15
- # meant for.
16
- AVATAR_ID_PATTERN = %r{cdn\.discordapp\.com/avatars/(\d+)/}
17
-
18
- module_function
19
-
20
- def discord_id_for(user)
21
- user.data.identities&.discord&.userId || avatar_discord_id_for(user)
22
- end
23
-
24
- def avatar_discord_id_for(user)
25
- match = user.data.avatar.to_s.match(AVATAR_ID_PATTERN)
26
- match && match[1]
27
- end
28
-
29
- def linked?(user)
30
- user.data.identities&.discord&.userId.present?
31
- end
32
-
33
- # Returns a hash with:
34
- # - :conflicts - discord id => users, for ids where more than one user has the
35
- # identity actually attached. Ambiguous; always needs manual review, never
36
- # auto-deleted.
37
- # - :deletable_orphans - [orphan, keeper] pairs, where orphan has no identity
38
- # attached but its avatar points at a discord id claimed by exactly one other
39
- # (linked) user. This is the confirmed race-loser case - safe to delete.
40
- # - :unresolved_orphans - users with no identity attached and no corresponding
41
- # linked user (still mid-race, or the winner can't be identified). Reported
42
- # only, never auto-deleted.
43
- def scan(management)
44
- users = management.list_users
45
- linked, unlinked = users.partition { |u| linked?(u) }
46
-
47
- by_discord_id = linked.group_by { |u| u.data.identities.discord.userId }
48
- conflicts = by_discord_id.select { |_id, group| group.length > 1 }
49
-
50
- deletable_orphans = []
51
- unresolved_orphans = []
52
- unlinked.each do |user|
53
- discord_id = avatar_discord_id_for(user)
54
- keeper = discord_id && by_discord_id[discord_id]
55
- if keeper&.length == 1
56
- deletable_orphans << [user, keeper.first]
57
- else
58
- unresolved_orphans << user
59
- end
60
- end
61
-
62
- { conflicts: conflicts, deletable_orphans: deletable_orphans, unresolved_orphans: unresolved_orphans }
63
- end
64
- end
65
- end
66
- end