yiffspace 0.0.25 → 0.1.1

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