yiffspace 0.0.25 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/lib/yiffspace/configuration.rb +0 -25
  4. data/lib/yiffspace/{railtie.rb → engine.rb} +9 -7
  5. data/lib/yiffspace/extensions/active_record/where_chain.rb +5 -5
  6. data/lib/yiffspace/version.rb +1 -1
  7. data/lib/yiffspace.rb +5 -6
  8. metadata +2 -49
  9. data/app/helpers/yiff_space/auth_helper.rb +0 -6
  10. data/engines/auth/app/controllers/yiff_space/auth/application_controller.rb +0 -11
  11. data/engines/auth/app/controllers/yiff_space/auth/root_controller.rb +0 -49
  12. data/engines/auth/app/controllers/yiff_space/auth/webhook_controller.rb +0 -73
  13. data/engines/auth/app/views/yiff_space/auth/root/permissions.html.erb +0 -14
  14. data/engines/auth/config/routes.rb +0 -12
  15. data/lib/tasks/yiffspace_tasks.rake +0 -41
  16. data/lib/yiffspace/auth/api_user.rb +0 -43
  17. data/lib/yiffspace/auth/auth_info/anonymous.rb +0 -60
  18. data/lib/yiffspace/auth/auth_info.rb +0 -78
  19. data/lib/yiffspace/auth/client.rb +0 -56
  20. data/lib/yiffspace/auth/discord_info.rb +0 -65
  21. data/lib/yiffspace/auth/engine.rb +0 -58
  22. data/lib/yiffspace/auth/helper.rb +0 -174
  23. data/lib/yiffspace/auth/permissions.rb +0 -87
  24. data/lib/yiffspace/auth/set_client_name.rb +0 -15
  25. data/lib/yiffspace/auth/user_info/anonymous.rb +0 -72
  26. data/lib/yiffspace/auth/user_info.rb +0 -87
  27. data/lib/yiffspace/auth.rb +0 -44
  28. data/lib/yiffspace/core_ext/action_dispatch/set_auth_client/scoped.rb +0 -13
  29. data/lib/yiffspace/core_ext/action_dispatch/set_auth_client.rb +0 -13
  30. data/lib/yiffspace/core_ext/logto/named_session_storage.rb +0 -7
  31. data/lib/yiffspace/extensions/action_dispatch/set_auth_client/scoped.rb +0 -15
  32. data/lib/yiffspace/extensions/action_dispatch/set_auth_client.rb +0 -13
  33. data/lib/yiffspace/extensions/logto/named_session_storage.rb +0 -22
  34. data/lib/yiffspace/logto_management_client.rb +0 -136
  35. data/lib/yiffspace/serializers/anonymous_auth_info_serializer.rb +0 -23
  36. data/lib/yiffspace/serializers/anonymous_user_info_serializer.rb +0 -23
  37. data/lib/yiffspace/serializers/auth_info_serializer.rb +0 -23
  38. data/lib/yiffspace/serializers/discord_info_serializer.rb +0 -23
  39. data/lib/yiffspace/serializers/permissions_serializer.rb +0 -23
  40. data/lib/yiffspace/serializers/user_info_serializer.rb +0 -23
  41. data/lib/yiffspace/utils/user_deduper.rb +0 -66
@@ -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