masks 0.5.0 → 0.6.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 +13 -0
- data/README.md +20 -0
- data/app/controllers/masks/rails/avatars_controller.rb +51 -0
- data/app/controllers/masks/rails/handshakes_controller.rb +5 -6
- data/app/controllers/masks/rails/sessions_controller.rb +5 -0
- data/config/routes.rb +1 -0
- data/lib/masks/client/claims.rb +41 -0
- data/lib/masks/client/errors.rb +8 -0
- data/lib/masks/client/http.rb +16 -1
- data/lib/masks/client/issuer.rb +16 -0
- data/lib/masks/client/registration.rb +27 -3
- data/lib/masks/rails/authentication.rb +45 -2
- data/lib/masks/rails/configuration.rb +15 -3
- data/lib/masks/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4455d2e731a770253802292bce1dd21a9b97c05489e65ba4a1f310c24873b79
|
|
4
|
+
data.tar.gz: df05e39f40a1f08fbb77897549c584653bf17676c3e6b13b64220e1751360e64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39e55aecad7e80a3cef669b9bcbe16f6f942c326bf98a9f9da954663365740d9e31a814a3da644ef68bd0090f040f853ff4b3bdda34f2f8fe6dd4b749190d138
|
|
7
|
+
data.tar.gz: 4cca8c0d431151c0a0865969dcc38767388ec4f93c1ab5e2b824e7dd875f8a38135969d7b8ed5a1966eb9b0f51b3fb60e8cff887c67d180561b3474e47653d84
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/masksrb/masks/compare/gem-v0.5.0...gem/v0.6.0) (2026-09-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **server:** a namespace is a grant, so an app can grow its own scopes ([4e816e4](https://github.com/masksrb/masks/commit/4e816e4456de9cc05601b8eaa03773788e3178bb))
|
|
9
|
+
* **server:** people, avatars, and one page instead of three ([d50c444](https://github.com/masksrb/masks/commit/d50c4440812fc1c3a92e926ff6d533b9a69dae2a))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
* **client:** an app whose issuer forgot it asks to be reconnected ([6956972](https://github.com/masksrb/masks/commit/695697233ca1ce35fa8b29ea74e58dd0d762b0a5))
|
|
15
|
+
|
|
3
16
|
## 0.5.0 — unreleased
|
|
4
17
|
|
|
5
18
|
First release carrying the consumer half in full. 0.4.0 predates the split described in
|
data/README.md
CHANGED
|
@@ -100,6 +100,26 @@ end
|
|
|
100
100
|
`resource_metadata`, so a client handed nothing but a URL can find its way to the issuer
|
|
101
101
|
and back.
|
|
102
102
|
|
|
103
|
+
### Avatars
|
|
104
|
+
|
|
105
|
+
Every actor has three faces at once — an uploaded `photo`, an `identicon`, and two-letter
|
|
106
|
+
`initials`. All three arrive on the id token, so drawing one costs no request.
|
|
107
|
+
|
|
108
|
+
```erb
|
|
109
|
+
<img src="<%= masks_claims.avatars.identicon %>?size=64" width="64" height="64" alt="">
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A photo is a likeness of a person and needs a token, which an `<img>` cannot carry, so the engine
|
|
113
|
+
proxies it with the token this app already holds:
|
|
114
|
+
|
|
115
|
+
```erb
|
|
116
|
+
<img src="/auth/avatar" width="64" height="64" alt="">
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`masks_claims.picture` resolves the standard OIDC claim — an offsite `picture_url` if the actor set
|
|
120
|
+
one, then the photo, then the identicon. `issuer.avatar_url(sub, style:, size:)` builds a URL for a
|
|
121
|
+
subject this app holds no token for. Sizes are 32, 64, 128, 256 or 512.
|
|
122
|
+
|
|
103
123
|
### Signing out
|
|
104
124
|
|
|
105
125
|
`DELETE /auth/logout` ends this app's session. Add `?everywhere=1` and the response
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
class AvatarsController < BaseController
|
|
4
|
+
SIZES = [ 32, 64, 128, 256, 512 ].freeze
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
return head :not_found unless masks_signed_in?
|
|
8
|
+
|
|
9
|
+
url = photo_url
|
|
10
|
+
|
|
11
|
+
return head :not_found if url.nil?
|
|
12
|
+
|
|
13
|
+
deliver(
|
|
14
|
+
Masks::Client::HTTP.fetch(
|
|
15
|
+
url, "Authorization" => masks_tokens.authorization, "Accept" => "image/*"
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
rescue Masks::Client::Error
|
|
19
|
+
head :bad_gateway
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def photo_url
|
|
25
|
+
held = masks_claims.avatars.photo
|
|
26
|
+
|
|
27
|
+
return nil if held.nil?
|
|
28
|
+
return nil unless held.start_with?("#{masks_config.issuer_for(request)}/")
|
|
29
|
+
|
|
30
|
+
asked = params[:size].presence
|
|
31
|
+
|
|
32
|
+
return held if asked.nil?
|
|
33
|
+
|
|
34
|
+
"#{held}?size=#{SIZES.find { |allowed| allowed >= asked.to_i } || SIZES.last}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def deliver(answer)
|
|
38
|
+
case answer
|
|
39
|
+
when Net::HTTPRedirection
|
|
40
|
+
redirect_to answer["Location"], allow_other_host: true
|
|
41
|
+
when Net::HTTPSuccess
|
|
42
|
+
expires_in 5.minutes, public: false
|
|
43
|
+
|
|
44
|
+
send_data answer.body, type: answer["Content-Type"], disposition: "inline"
|
|
45
|
+
else
|
|
46
|
+
head :not_found
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -39,7 +39,7 @@ module Masks
|
|
|
39
39
|
return redirect_to(masks_config.after_sign_out) unless masks_signed_in?
|
|
40
40
|
return redirect_to(masks_handshake_path) unless masks_config.can_forget?
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
forget_upstream
|
|
43
43
|
|
|
44
44
|
masks_config.forget!(request)
|
|
45
45
|
masks_forget
|
|
@@ -51,11 +51,10 @@ module Masks
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
54
|
+
def forget_upstream
|
|
55
|
+
masks_registration&.delete
|
|
56
|
+
rescue Masks::Client::Unregistered
|
|
57
|
+
false
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def returned
|
|
@@ -14,6 +14,7 @@ module Masks
|
|
|
14
14
|
|
|
15
15
|
def start
|
|
16
16
|
return redirect_to(masks_handshake_path) unless masks_configured?
|
|
17
|
+
return redirect_to(masks_handshake_path) if masks_reconnect!
|
|
17
18
|
|
|
18
19
|
pending = masks_requests.open(
|
|
19
20
|
return_to: requested_return_to || session.delete(:masks_return_to)
|
|
@@ -55,6 +56,10 @@ module Masks
|
|
|
55
56
|
masks_store(tokens, identity: identity)
|
|
56
57
|
|
|
57
58
|
redirect_to pending[:return_to] || masks_config.after_sign_in
|
|
59
|
+
rescue Masks::Client::Unregistered => e
|
|
60
|
+
return redirect_to(masks_handshake_path) if masks_disconnect!
|
|
61
|
+
|
|
62
|
+
refuse(e.code, e.description, pending)
|
|
58
63
|
rescue Masks::Client::Error => e
|
|
59
64
|
refuse(e.class.name.demodulize.underscore, e.message, pending)
|
|
60
65
|
end
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Masks::Rails::Engine.routes.draw do
|
|
2
2
|
get "/", to: "sessions#start", as: :start
|
|
3
3
|
get "/session", to: "sessions#show", as: :session
|
|
4
|
+
get "/avatar", to: "avatars#show", as: :avatar
|
|
4
5
|
get "/callback", to: "sessions#callback", as: :callback
|
|
5
6
|
match "/logout", to: "sessions#destroy", via: %i[get post delete], as: :logout
|
|
6
7
|
|
data/lib/masks/client/claims.rb
CHANGED
|
@@ -29,6 +29,39 @@ module Masks
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
class Avatars
|
|
33
|
+
STYLES = %w[photo identicon initials].freeze
|
|
34
|
+
FALLBACK = "identicon".freeze
|
|
35
|
+
|
|
36
|
+
attr_reader :to_h
|
|
37
|
+
|
|
38
|
+
def initialize(hash)
|
|
39
|
+
@to_h = hash.is_a?(Hash) ? hash : {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
STYLES.each do |style|
|
|
43
|
+
define_method(style) { to_h[style] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](style)
|
|
47
|
+
to_h[style.to_s]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def photo?
|
|
51
|
+
!photo.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def present?
|
|
55
|
+
to_h.any?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ==(other)
|
|
59
|
+
other.is_a?(Avatars) ? to_h == other.to_h : false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
AVATARS = "masks:avatars".freeze
|
|
64
|
+
|
|
32
65
|
attr_reader :to_h
|
|
33
66
|
|
|
34
67
|
def initialize(claims)
|
|
@@ -67,6 +100,14 @@ module Masks
|
|
|
67
100
|
@tenant ||= Tenant.new(self["tenant"])
|
|
68
101
|
end
|
|
69
102
|
|
|
103
|
+
def avatars
|
|
104
|
+
@avatars ||= Avatars.new(self[AVATARS])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def picture
|
|
108
|
+
self["picture"] || avatars.photo || avatars.identicon
|
|
109
|
+
end
|
|
110
|
+
|
|
70
111
|
def scopes
|
|
71
112
|
@scopes ||= self["scope"].to_s.split(/\s+/).reject(&:empty?).freeze
|
|
72
113
|
end
|
data/lib/masks/client/errors.rb
CHANGED
data/lib/masks/client/http.rb
CHANGED
|
@@ -31,6 +31,19 @@ module Masks
|
|
|
31
31
|
request(Net::HTTP::Delete.new(URI.parse(url.to_s), default_headers.merge(headers)))
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def fetch(url, headers = {})
|
|
35
|
+
uri = URI.parse(url.to_s)
|
|
36
|
+
|
|
37
|
+
Net::HTTP.start(
|
|
38
|
+
uri.hostname, uri.port,
|
|
39
|
+
use_ssl: uri.scheme == "https",
|
|
40
|
+
open_timeout: OPEN_TIMEOUT,
|
|
41
|
+
read_timeout: READ_TIMEOUT
|
|
42
|
+
) { |http| http.request(Net::HTTP::Get.new(uri, headers)) }
|
|
43
|
+
rescue SystemCallError, SocketError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError => e
|
|
44
|
+
raise Unreachable, "#{uri.host} is unreachable (#{e.class})"
|
|
45
|
+
end
|
|
46
|
+
|
|
34
47
|
def json(verb, url, body, headers)
|
|
35
48
|
uri = URI.parse(url.to_s)
|
|
36
49
|
request = verb.new(uri, default_headers.merge(headers))
|
|
@@ -67,7 +80,9 @@ module Masks
|
|
|
67
80
|
end
|
|
68
81
|
|
|
69
82
|
unless response.is_a?(Net::HTTPSuccess)
|
|
70
|
-
|
|
83
|
+
refusal = Unregistered.raised_by?(body) ? Unregistered : Rejected
|
|
84
|
+
|
|
85
|
+
raise refusal.new(
|
|
71
86
|
body["error"] || "http_#{response.code}",
|
|
72
87
|
body["error_description"] || response.message,
|
|
73
88
|
status: response.code.to_i
|
data/lib/masks/client/issuer.rb
CHANGED
|
@@ -51,6 +51,22 @@ module Masks
|
|
|
51
51
|
discovery.fetch(name) { raise Rejected.new("invalid_issuer", "#{url} publishes no #{name}") }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def avatar_styles
|
|
55
|
+
discovery["avatar_styles_supported"] || Claims::Avatars::STYLES
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def avatar_url(subject, style: nil, size: nil)
|
|
59
|
+
wanted = (style || Claims::Avatars::FALLBACK).to_s
|
|
60
|
+
|
|
61
|
+
unless avatar_styles.include?(wanted)
|
|
62
|
+
raise Rejected.new("invalid_style", "#{url} does not serve #{wanted} avatars")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
query = size ? "?size=#{size.to_i}" : ""
|
|
66
|
+
|
|
67
|
+
"#{endpoint('avatar_endpoint')}/#{subject}/#{wanted}#{query}"
|
|
68
|
+
end
|
|
69
|
+
|
|
54
70
|
def refresh!
|
|
55
71
|
@lock.synchronize { @cache = {} }
|
|
56
72
|
self
|
|
@@ -59,18 +59,30 @@ module Masks
|
|
|
59
59
|
metadata["registration_client_uri"]
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
REFUSED = [ 401, 403 ].freeze
|
|
63
|
+
GONE = (REFUSED + [ 404 ]).freeze
|
|
64
|
+
|
|
62
65
|
def read
|
|
63
|
-
HTTP.get(uri, authorization)
|
|
66
|
+
still_ours(REFUSED) { HTTP.get(uri, authorization) }
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def update(**attributes)
|
|
67
|
-
@metadata = metadata.merge(
|
|
70
|
+
@metadata = metadata.merge(
|
|
71
|
+
still_ours(REFUSED) { HTTP.put_json(uri, self.class.stringify(attributes), authorization) }
|
|
72
|
+
)
|
|
68
73
|
self
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
def delete
|
|
72
|
-
HTTP.delete(uri, authorization)
|
|
77
|
+
still_ours(GONE) { HTTP.delete(uri, authorization) }
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def known?
|
|
82
|
+
read
|
|
73
83
|
true
|
|
84
|
+
rescue Unregistered
|
|
85
|
+
false
|
|
74
86
|
end
|
|
75
87
|
|
|
76
88
|
def authorization
|
|
@@ -86,6 +98,18 @@ module Masks
|
|
|
86
98
|
scope: scope
|
|
87
99
|
)
|
|
88
100
|
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def still_ours(statuses)
|
|
105
|
+
yield
|
|
106
|
+
rescue Unregistered
|
|
107
|
+
raise
|
|
108
|
+
rescue Rejected => e
|
|
109
|
+
raise unless statuses.include?(e.status)
|
|
110
|
+
|
|
111
|
+
raise Unregistered.new(Unregistered::CODE, e.description, status: e.status)
|
|
112
|
+
end
|
|
89
113
|
end
|
|
90
114
|
end
|
|
91
115
|
end
|
|
@@ -6,7 +6,8 @@ module Masks
|
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
8
|
if respond_to?(:helper_method)
|
|
9
|
-
helper_method :masks_signed_in?, :masks_identity, :masks_tenant, :masks_scopes
|
|
9
|
+
helper_method :masks_signed_in?, :masks_identity, :masks_tenant, :masks_scopes,
|
|
10
|
+
:masks_claims
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -48,7 +49,10 @@ module Masks
|
|
|
48
49
|
session[masks_config.session_key] || {}
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
IDENTITY =
|
|
52
|
+
IDENTITY = [
|
|
53
|
+
"sub", "name", "preferred_username", "email", "email_verified", "tenant",
|
|
54
|
+
"picture", Masks::Client::Claims::AVATARS
|
|
55
|
+
].freeze
|
|
52
56
|
|
|
53
57
|
def masks_identity_from(tokens)
|
|
54
58
|
return nil if tokens.id_token.nil?
|
|
@@ -70,6 +74,36 @@ module Masks
|
|
|
70
74
|
masks_config.configured?(request)
|
|
71
75
|
end
|
|
72
76
|
|
|
77
|
+
def masks_registration
|
|
78
|
+
Masks::Client::Registration.held(
|
|
79
|
+
masks_config.issuer_for(request),
|
|
80
|
+
masks_config.credentials_for(request)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def masks_registered?
|
|
85
|
+
held = masks_registration
|
|
86
|
+
|
|
87
|
+
held.nil? || held.known?
|
|
88
|
+
rescue Masks::Client::Error
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def masks_disconnect!
|
|
93
|
+
return false unless masks_configured? && masks_config.can_forget?
|
|
94
|
+
|
|
95
|
+
masks_config.forget!(request)
|
|
96
|
+
masks_forget
|
|
97
|
+
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def masks_reconnect!
|
|
102
|
+
return false if masks_registered?
|
|
103
|
+
|
|
104
|
+
masks_disconnect!
|
|
105
|
+
end
|
|
106
|
+
|
|
73
107
|
def masks_handshake_path
|
|
74
108
|
Masks::Rails::Engine.routes.url_helpers.handshake_path
|
|
75
109
|
end
|
|
@@ -84,6 +118,10 @@ module Masks
|
|
|
84
118
|
masks_identity&.dig("tenant")
|
|
85
119
|
end
|
|
86
120
|
|
|
121
|
+
def masks_claims
|
|
122
|
+
@masks_claims ||= Masks::Client::Claims.new(masks_identity || {})
|
|
123
|
+
end
|
|
124
|
+
|
|
87
125
|
def masks_scopes
|
|
88
126
|
masks_tokens&.scopes || []
|
|
89
127
|
end
|
|
@@ -107,6 +145,9 @@ module Masks
|
|
|
107
145
|
)
|
|
108
146
|
)
|
|
109
147
|
true
|
|
148
|
+
rescue Masks::Client::Unregistered
|
|
149
|
+
masks_disconnect!
|
|
150
|
+
false
|
|
110
151
|
rescue Masks::Client::Rejected
|
|
111
152
|
masks_forget
|
|
112
153
|
false
|
|
@@ -144,6 +185,8 @@ module Masks
|
|
|
144
185
|
"nickname" => identity["preferred_username"],
|
|
145
186
|
"email" => identity["email"],
|
|
146
187
|
"email_verified" => identity["email_verified"],
|
|
188
|
+
"picture" => masks_claims.picture,
|
|
189
|
+
"avatars" => masks_claims.avatars.to_h.presence,
|
|
147
190
|
"tenant" => masks_tenant,
|
|
148
191
|
"scopes" => masks_scopes,
|
|
149
192
|
"expires_at" => masks_tokens&.expires_at
|
|
@@ -3,8 +3,8 @@ module Masks
|
|
|
3
3
|
class Configuration
|
|
4
4
|
class Unconfigured < Masks::Client::Error; end
|
|
5
5
|
|
|
6
|
-
attr_accessor :scope, :resource, :resource_scopes, :after_sign_in,
|
|
7
|
-
:session_key, :sign_out_of_issuer, :parent_controller,
|
|
6
|
+
attr_accessor :scope, :namespace, :resource, :resource_scopes, :after_sign_in,
|
|
7
|
+
:after_sign_out, :session_key, :sign_out_of_issuer, :parent_controller,
|
|
8
8
|
:credentials_path, :authenticate_everything
|
|
9
9
|
attr_writer :issuer, :redirect_uri, :name, :credentials, :store, :forget
|
|
10
10
|
|
|
@@ -134,10 +134,22 @@ module Masks
|
|
|
134
134
|
raise(Unconfigured, "Masks::Rails.config.resource is not set"),
|
|
135
135
|
redirect_uris: [ redirect_uri_for(request) ],
|
|
136
136
|
return_to: return_to_for(request),
|
|
137
|
-
scope:
|
|
137
|
+
scope: approved_scope
|
|
138
138
|
)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
# What the handshake asks to be approved for, which is not what a sign-in
|
|
142
|
+
# requests. An app that owns a namespace asks for the namespace once, so
|
|
143
|
+
# that adding a capability later is a deployment rather than an approval
|
|
144
|
+
# round; the authorize request still names the scopes it actually wants.
|
|
145
|
+
def approved_scope
|
|
146
|
+
return scope if namespace.blank?
|
|
147
|
+
|
|
148
|
+
outside = Array(scope).reject { |name| name.to_s.start_with?(namespace) }
|
|
149
|
+
|
|
150
|
+
outside + [ namespace ]
|
|
151
|
+
end
|
|
152
|
+
|
|
141
153
|
def return_to_for(request)
|
|
142
154
|
uri = URI.parse(redirect_uri_for(request))
|
|
143
155
|
port = ":#{uri.port}" unless uri.port == uri.default_port
|
data/lib/masks/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: masks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- geiger
|
|
@@ -33,6 +33,7 @@ files:
|
|
|
33
33
|
- CHANGELOG.md
|
|
34
34
|
- LICENSE
|
|
35
35
|
- README.md
|
|
36
|
+
- app/controllers/masks/rails/avatars_controller.rb
|
|
36
37
|
- app/controllers/masks/rails/base_controller.rb
|
|
37
38
|
- app/controllers/masks/rails/handshakes_controller.rb
|
|
38
39
|
- app/controllers/masks/rails/sessions_controller.rb
|