sparkly-auth 1.0.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.
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/app/controllers/sparkly_accounts_controller.rb +59 -0
- data/app/controllers/sparkly_controller.rb +47 -0
- data/app/controllers/sparkly_sessions_controller.rb +52 -0
- data/app/models/password.rb +3 -0
- data/app/models/remembrance_token.rb +50 -0
- data/app/views/sparkly_accounts/edit.html.erb +24 -0
- data/app/views/sparkly_accounts/new.html.erb +24 -0
- data/app/views/sparkly_accounts/show.html.erb +0 -0
- data/app/views/sparkly_sessions/new.html.erb +22 -0
- data/dependencies.rb +1 -0
- data/generators/sparkly/USAGE +27 -0
- data/generators/sparkly/sparkly_generator.rb +76 -0
- data/generators/sparkly/templates/accounts_controller.rb +65 -0
- data/generators/sparkly/templates/accounts_helper.rb +2 -0
- data/generators/sparkly/templates/help_file.txt +56 -0
- data/generators/sparkly/templates/initializer.rb +30 -0
- data/generators/sparkly/templates/migrations/add_confirmed_to_sparkly_passwords.rb +9 -0
- data/generators/sparkly/templates/migrations/create_sparkly_passwords.rb +19 -0
- data/generators/sparkly/templates/migrations/create_sparkly_remembered_tokens.rb +15 -0
- data/generators/sparkly/templates/sessions_controller.rb +45 -0
- data/generators/sparkly/templates/sessions_helper.rb +2 -0
- data/generators/sparkly/templates/tasks/migrations.rb +1 -0
- data/generators/sparkly/templates/views/sparkly_accounts/edit.html.erb +24 -0
- data/generators/sparkly/templates/views/sparkly_accounts/new.html.erb +24 -0
- data/generators/sparkly/templates/views/sparkly_accounts/show.html.erb +0 -0
- data/generators/sparkly/templates/views/sparkly_sessions/new.html.erb +22 -0
- data/init.rb +44 -0
- data/lib/auth.rb +52 -0
- data/lib/auth/behavior/base.rb +64 -0
- data/lib/auth/behavior/core.rb +87 -0
- data/lib/auth/behavior/core/authenticated_model_methods.rb +52 -0
- data/lib/auth/behavior/core/controller_extensions.rb +52 -0
- data/lib/auth/behavior/core/controller_extensions/class_methods.rb +24 -0
- data/lib/auth/behavior/core/controller_extensions/current_user.rb +54 -0
- data/lib/auth/behavior/core/password_methods.rb +65 -0
- data/lib/auth/behavior/remember_me.rb +17 -0
- data/lib/auth/behavior/remember_me/configuration.rb +21 -0
- data/lib/auth/behavior/remember_me/controller_extensions.rb +66 -0
- data/lib/auth/behavior_lookup.rb +10 -0
- data/lib/auth/configuration.rb +328 -0
- data/lib/auth/encryptors/sha512.rb +20 -0
- data/lib/auth/generators/configuration_generator.rb +20 -0
- data/lib/auth/generators/controllers_generator.rb +34 -0
- data/lib/auth/generators/migration_generator.rb +32 -0
- data/lib/auth/generators/route_generator.rb +19 -0
- data/lib/auth/generators/views_generator.rb +26 -0
- data/lib/auth/model.rb +94 -0
- data/lib/auth/observer.rb +21 -0
- data/lib/auth/target_list.rb +5 -0
- data/lib/auth/tasks/migrations.rb +71 -0
- data/lib/auth/token.rb +10 -0
- data/lib/sparkly-auth.rb +1 -0
- data/rails/init.rb +17 -0
- data/rails/routes.rb +19 -0
- data/sparkly-auth.gemspec +143 -0
- data/spec/controllers/application_controller_spec.rb +13 -0
- data/spec/generators/sparkly_spec.rb +64 -0
- data/spec/lib/auth/behavior/core_spec.rb +184 -0
- data/spec/lib/auth/behavior/remember_me_spec.rb +127 -0
- data/spec/lib/auth/extensions/controller_spec.rb +32 -0
- data/spec/lib/auth/model_spec.rb +57 -0
- data/spec/lib/auth_spec.rb +32 -0
- data/spec/mocks/models/user.rb +3 -0
- data/spec/routes_spec.rb +24 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/views_spec.rb +18 -0
- metadata +210 -0
@@ -0,0 +1,328 @@
|
|
1
|
+
module Auth
|
2
|
+
class Configuration
|
3
|
+
include Auth::BehaviorLookup
|
4
|
+
|
5
|
+
class << self
|
6
|
+
include Auth::BehaviorLookup
|
7
|
+
|
8
|
+
def behavior_configs
|
9
|
+
@behavior_configs ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def register_behavior(name)
|
13
|
+
behavior_class = lookup_behavior(name)
|
14
|
+
# If the behavior has a configuration, add it to self.
|
15
|
+
accessor_name = name
|
16
|
+
name = "#{behavior_class.name}::Configuration"
|
17
|
+
behavior_configs << [ accessor_name, name.constantize ]
|
18
|
+
# eg Auth.remember_me.something = 5
|
19
|
+
Auth.class.delegate accessor_name, :to => :configuration
|
20
|
+
rescue NameError
|
21
|
+
# Presumably, the behavior does not have a configuration.
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# IS there a better way to do this?? I'm dying to find it...
|
26
|
+
Dir[File.join(File.dirname(__FILE__), "behavior/*.rb")].each do |fi|
|
27
|
+
unless fi[/\/base.rb$/]
|
28
|
+
const_name = fi.gsub(/^#{Regexp::escape File.dirname(__FILE__)}\/behavior\/(.*)\.rb$/, '\1')
|
29
|
+
register_behavior(const_name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The message to display when the user creates an account.
|
34
|
+
#
|
35
|
+
# Default:
|
36
|
+
# "Your account has been created."
|
37
|
+
attr_accessor :account_created_message
|
38
|
+
|
39
|
+
# The message to display when the user deletes his or her account.
|
40
|
+
#
|
41
|
+
# Default:
|
42
|
+
# "Your account has been deleted."
|
43
|
+
attr_accessor :account_deleted_message
|
44
|
+
|
45
|
+
# The message to display when user profile has been updated or the password has been changed.
|
46
|
+
#
|
47
|
+
# Default:
|
48
|
+
# "Your changes have been saved."
|
49
|
+
attr_accessor :account_updated_message
|
50
|
+
|
51
|
+
# The length of time an account is locked for, if it is locked.
|
52
|
+
#
|
53
|
+
# Default:
|
54
|
+
# 30.minutes
|
55
|
+
attr_accessor :account_lock_duration
|
56
|
+
|
57
|
+
# The message to display if an account has been locked.
|
58
|
+
#
|
59
|
+
# Default:
|
60
|
+
# "Account is locked due to too many invalid attempts."
|
61
|
+
attr_accessor :account_locked_message
|
62
|
+
|
63
|
+
# The array of Auth::Model instances which represent the models which will be authenticated.
|
64
|
+
# See also #authenticate
|
65
|
+
attr_accessor :authenticated_models
|
66
|
+
|
67
|
+
# The NAME of the controller to use as a base controller. All Sparkly controllers will subclass
|
68
|
+
# this, and methods such as current_user will be added to it. Defaults to 'application'.
|
69
|
+
#
|
70
|
+
# Default:
|
71
|
+
# 'application'
|
72
|
+
attr_accessor :base_controller_name
|
73
|
+
|
74
|
+
# The array of behaviors which will be applied by default to every authenticated model. If
|
75
|
+
# a behavior set is specified for a given model, it will be used instead of (not in addition to)
|
76
|
+
# this array.
|
77
|
+
#
|
78
|
+
# Default:
|
79
|
+
# [ :core ]
|
80
|
+
attr_accessor :behaviors
|
81
|
+
|
82
|
+
# The name of the controller to route to for creating users, editing them, etc.
|
83
|
+
#
|
84
|
+
# "sparkly_accounts"
|
85
|
+
attr_accessor :default_accounts_controller_name
|
86
|
+
|
87
|
+
# If an issue would prevent the user from viewing the current page, Auth will redirect the user
|
88
|
+
# to the value stored in session[:destination]. If this value is not set, then Auth will default
|
89
|
+
# to this path.
|
90
|
+
#
|
91
|
+
# Default:
|
92
|
+
# "/"
|
93
|
+
attr_accessor :default_destination
|
94
|
+
|
95
|
+
# The method to call in order to determine which resource to use when implicitly logging in.
|
96
|
+
#
|
97
|
+
# If set to nil, the #default_destination will be used instead.
|
98
|
+
#
|
99
|
+
# Default:
|
100
|
+
# :new_user_session_path
|
101
|
+
attr_accessor :default_login_path
|
102
|
+
|
103
|
+
# The name of the controller to route to for logging in, logging out, etc.
|
104
|
+
#
|
105
|
+
# Default:
|
106
|
+
# "sparkly_sessions"
|
107
|
+
attr_accessor :default_sessions_controller_name
|
108
|
+
|
109
|
+
# The class to use for encryption of passwords. This can be any class, as long as it responds
|
110
|
+
# to #encrypt and #matches?
|
111
|
+
#
|
112
|
+
# Default:
|
113
|
+
# Auth::Encryptors::Sha512
|
114
|
+
attr_accessor :encryptor
|
115
|
+
|
116
|
+
# Message to display if username and/or password were incorrect.
|
117
|
+
#
|
118
|
+
# Default:
|
119
|
+
# "Credentials were not valid."
|
120
|
+
attr_accessor :invalid_credentials_message
|
121
|
+
|
122
|
+
# If true, the user will be automatically logged in after registering a new account.
|
123
|
+
# Note that this can be modified by some behaviors.
|
124
|
+
#
|
125
|
+
# Default:
|
126
|
+
# true
|
127
|
+
attr_accessor :login_after_signup
|
128
|
+
|
129
|
+
# The message to display when the user is not allowed to view a page because s/he must log in.
|
130
|
+
#
|
131
|
+
# Default:
|
132
|
+
# "You must be signed in to view this page."
|
133
|
+
attr_accessor :login_required_message
|
134
|
+
|
135
|
+
# Message to display if login was successful.
|
136
|
+
#
|
137
|
+
# Default:
|
138
|
+
# "Signed in successfully."
|
139
|
+
attr_accessor :login_successful_message
|
140
|
+
|
141
|
+
# Message to display when user logs out.
|
142
|
+
#
|
143
|
+
# Default:
|
144
|
+
# "You have been signed out."
|
145
|
+
attr_accessor :logout_message
|
146
|
+
|
147
|
+
# The message to display when the user is not allowed to view a page because s/he must log out.
|
148
|
+
#
|
149
|
+
# "You must be signed out to view this page."
|
150
|
+
attr_accessor :logout_required_message
|
151
|
+
|
152
|
+
# The maximum login attempts permitted before an account is locked. Set to nil to disable locking.
|
153
|
+
#
|
154
|
+
# Default:
|
155
|
+
# 5
|
156
|
+
attr_accessor :max_login_failures
|
157
|
+
|
158
|
+
# Minimum length for passwords.
|
159
|
+
#
|
160
|
+
# Default:
|
161
|
+
# 7
|
162
|
+
attr_accessor :minimum_password_length
|
163
|
+
|
164
|
+
# Regular expression which passwords must match. The default forces at least 1
|
165
|
+
# uppercase, lowercase and numeric character.
|
166
|
+
#
|
167
|
+
# Default:
|
168
|
+
# /(^(?=.*\d)(?=.*[a-zA-Z]).{7,}$)/
|
169
|
+
attr_accessor :password_format
|
170
|
+
|
171
|
+
# When the password to be created does not conform to the above format, this error
|
172
|
+
# message will be shown.
|
173
|
+
#
|
174
|
+
# Default:
|
175
|
+
# "must contain at least 1 uppercase, 1 lowercase and 1 number"
|
176
|
+
attr_accessor :password_format_message
|
177
|
+
|
178
|
+
# The number of passwords to keep in the password change history for each user. Any given
|
179
|
+
# user may not use the same password twice for at least this duration. For instance, if
|
180
|
+
# set to 4, then a user must change his password 4 times before s/he can reuse one of
|
181
|
+
# his/her previous passwords.
|
182
|
+
#
|
183
|
+
# Default:
|
184
|
+
# 4
|
185
|
+
attr_accessor :password_history_length
|
186
|
+
|
187
|
+
# The message to display when password change matches one of the previous passwords
|
188
|
+
#
|
189
|
+
# Default:
|
190
|
+
# "must not be the same as any of your recent passwords"
|
191
|
+
attr_accessor :password_uniqueness_message
|
192
|
+
|
193
|
+
# How frequently should passwords be forced to change? Nil for never.
|
194
|
+
#
|
195
|
+
# Default:
|
196
|
+
# 30.days
|
197
|
+
attr_accessor :password_update_frequency
|
198
|
+
|
199
|
+
# The path to the Sparkly Auth libraries.
|
200
|
+
attr_reader :path
|
201
|
+
|
202
|
+
# The maximum session duration. Users will be logged out automatically after this period expires.
|
203
|
+
#
|
204
|
+
# Default:
|
205
|
+
# 30.minutes
|
206
|
+
attr_accessor :session_duration
|
207
|
+
|
208
|
+
# Message to display when the user's session times out due to inactivity.
|
209
|
+
#
|
210
|
+
# Default:
|
211
|
+
# "You have been signed out due to inactivity. Please sign in again."
|
212
|
+
attr_accessor :session_timeout_message
|
213
|
+
|
214
|
+
# Finds the controller with the same name as #base_controller_name and returns it.
|
215
|
+
def base_controller
|
216
|
+
"#{base_controller_name.to_s.camelize}Controller".constantize
|
217
|
+
rescue NameError => err
|
218
|
+
begin
|
219
|
+
base_controller_name.to_s.camelize.constantize
|
220
|
+
rescue NameError
|
221
|
+
raise err
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns the classes which represent each behavior listed in #behaviors
|
226
|
+
def behavior_classes
|
227
|
+
behaviors.collect { |behavior| lookup_behavior(behavior) }
|
228
|
+
end
|
229
|
+
|
230
|
+
# Causes Sparkly Auth to *not* generate routes by default. You'll have to map them yourself if you disable
|
231
|
+
# route generation.
|
232
|
+
def disable_route_generation!
|
233
|
+
@generate_routes = false
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns true if Sparkly Auth is expected to generate routes for this application. This is true by
|
237
|
+
# default, and can be disabled with #disable_route_generation!
|
238
|
+
def generate_routes?
|
239
|
+
@generate_routes
|
240
|
+
end
|
241
|
+
|
242
|
+
def initialize
|
243
|
+
@password_format = /(^(?=.*\d)(?=.*[a-zA-Z]).{7,}$)/
|
244
|
+
@password_format_message = "must contain at least 1 uppercase, 1 lowercase and 1 number"
|
245
|
+
@minimum_password_length = 7
|
246
|
+
@path = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
247
|
+
@authenticated_models = Auth::TargetList.new
|
248
|
+
@behaviors = [ :core ]
|
249
|
+
@password_update_frequency = 30.days
|
250
|
+
@encryptor = Auth::Encryptors::Sha512
|
251
|
+
@password_uniqueness_message = "must not be the same as any of your recent passwords"
|
252
|
+
@password_history_length = 4
|
253
|
+
@default_accounts_controller_name = "sparkly_accounts"
|
254
|
+
@default_sessions_controller_name = "sparkly_sessions"
|
255
|
+
@login_required_message = "You must be signed in to view this page."
|
256
|
+
@logout_required_message = "You must be signed out to view this page."
|
257
|
+
@invalid_credentials_message = "Credentials were not valid."
|
258
|
+
@login_successful_message = "Signed in successfully."
|
259
|
+
@default_destination = "/"
|
260
|
+
@base_controller_name = 'application'
|
261
|
+
@session_duration = 30.minutes
|
262
|
+
@logout_message = "You have been signed out."
|
263
|
+
@session_timeout_message = "You have been signed out due to inactivity. Please sign in again."
|
264
|
+
@default_login_path = :new_user_session_path
|
265
|
+
@account_deleted_message = "Your account has been deleted."
|
266
|
+
@account_created_message = "Your account has been created."
|
267
|
+
@account_updated_message = "Your changes have been saved."
|
268
|
+
@account_locked_message = "Account is locked due to too many invalid attempts."
|
269
|
+
@account_lock_duration = 30.minutes
|
270
|
+
@max_login_failures = 5
|
271
|
+
@generate_routes = true
|
272
|
+
@login_after_signup = false
|
273
|
+
|
274
|
+
self.class.behavior_configs.each do |accessor_name, config_klass|
|
275
|
+
instance_variable_set("@#{accessor_name}", config_klass.new(self))
|
276
|
+
singleton = (class << self; self; end)
|
277
|
+
singleton.send(:define_method, accessor_name) { instance_variable_get("@#{accessor_name}") }
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def apply!
|
282
|
+
# Apply behaviors to controllers
|
283
|
+
behaviors.each do |behavior_name|
|
284
|
+
behavior = lookup_behavior(behavior_name)
|
285
|
+
behavior.apply_to_controllers
|
286
|
+
end
|
287
|
+
|
288
|
+
# Apply options to authenticated models
|
289
|
+
authenticated_models.each do |model|
|
290
|
+
model.apply_options!
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Accepts a list of model names (or the models themselves) and an optional set of options which
|
295
|
+
# govern how the models will be authenticated.
|
296
|
+
#
|
297
|
+
# Examples:
|
298
|
+
# Auth.configure do |config|
|
299
|
+
# config.authenticate :user
|
300
|
+
# config.authenticate :admin, :key => :login
|
301
|
+
# config.authenticate :user, :admin, :with => /a password validating regexp/
|
302
|
+
# end
|
303
|
+
#
|
304
|
+
# Note that if an item is specified more than once, the options will be merged together for the
|
305
|
+
# entry. For instance, in the above example, the :user model will be authenticated with :password,
|
306
|
+
# while the :admin model will be authenticated with :password on key :login.
|
307
|
+
#
|
308
|
+
def authenticate(*model_names)
|
309
|
+
options = model_names.extract_options!
|
310
|
+
model_names.flatten.each do |name|
|
311
|
+
if model = authenticated_models.find(name)
|
312
|
+
model.merge_options! options
|
313
|
+
else
|
314
|
+
authenticated_models << Auth::Model.new(name, options)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns the configuration for the given authenticated model.
|
320
|
+
def for_model(name_or_class_or_instance)
|
321
|
+
name_or_class = name_or_class_or_instance
|
322
|
+
name_or_class = name_or_class.class if name_or_class.kind_of?(ActiveRecord::Base)
|
323
|
+
authenticated_models.find(name_or_class)
|
324
|
+
end
|
325
|
+
|
326
|
+
private
|
327
|
+
end
|
328
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Auth::Encryptors::Sha512
|
2
|
+
class << self
|
3
|
+
attr_accessor :token_delimeter
|
4
|
+
attr_writer :stretches
|
5
|
+
|
6
|
+
def stretches
|
7
|
+
@stretches ||= 20
|
8
|
+
end
|
9
|
+
|
10
|
+
def encrypt(*what)
|
11
|
+
digest = what.flatten.join(token_delimeter)
|
12
|
+
stretches.times { digest = Digest::SHA512.hexdigest(digest) }
|
13
|
+
digest
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(encrypted_copy, *what)
|
17
|
+
encrypt(*what) == encrypted_copy
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Auth::Generators::ConfigurationGenerator < Rails::Generator::Base
|
2
|
+
attr_reader :model
|
3
|
+
|
4
|
+
def initialize(args, options = {})
|
5
|
+
super(args, options)
|
6
|
+
end
|
7
|
+
|
8
|
+
def manifest
|
9
|
+
record do |m|
|
10
|
+
m.directory "lib/tasks"
|
11
|
+
m.directory "config/initializers"
|
12
|
+
m.file "tasks/migrations.rb", "lib/tasks/sparkly_migration.rb"
|
13
|
+
m.file 'initializer.rb', 'config/initializers/sparkly_authentication.rb'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def spec
|
18
|
+
@spec ||= Rails::Generator::Spec.new("configuration", File.join(Auth.path, "auth/generators"), nil)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Auth::Generators::ControllersGenerator < Rails::Generator::NamedBase
|
2
|
+
attr_reader :model
|
3
|
+
|
4
|
+
def initialize(model, options = {})
|
5
|
+
@model = model
|
6
|
+
args = [ model.name ]
|
7
|
+
super(args, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def manifest
|
11
|
+
record do |m|
|
12
|
+
m.directory "app/controllers"
|
13
|
+
m.directory "app/helpers"
|
14
|
+
|
15
|
+
m.template "accounts_controller.rb", File.join("app/controllers", "#{model.accounts_controller.underscore}_controller.rb"),
|
16
|
+
:assigns => { :model => model }
|
17
|
+
m.template "sessions_controller.rb", File.join("app/controllers", "#{model.sessions_controller.underscore}_controller.rb"),
|
18
|
+
:assigns => { :model => model }
|
19
|
+
|
20
|
+
m.template "accounts_helper.rb", File.join("app/helpers", "#{model.accounts_controller.underscore}_helper.rb"),
|
21
|
+
:assigns => { :model => model }
|
22
|
+
m.template "sessions_helper.rb", File.join("app/helpers", "#{model.sessions_controller.underscore}_helper.rb"),
|
23
|
+
:assigns => { :model => model }
|
24
|
+
|
25
|
+
# Controller generator should also kick off the corresponding view generation.
|
26
|
+
views = Auth::Generators::ViewsGenerator.new(model, :source => File.join(source_root), :destination => destination_root)
|
27
|
+
views.manifest.replay(m)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def spec
|
32
|
+
@spec ||= Rails::Generator::Spec.new("controllers", File.join(Auth.path, "auth/generators"), nil)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Auth::Generators::MigrationGenerator < Rails::Generator::NamedBase
|
2
|
+
attr_reader :model
|
3
|
+
|
4
|
+
def initialize(model, options = {})
|
5
|
+
@model = model
|
6
|
+
args = [ model.name ]
|
7
|
+
super(args, options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def manifest
|
11
|
+
record do |m|
|
12
|
+
m.directory "db/migrate"
|
13
|
+
mg_version = 0
|
14
|
+
Auth.behavior_classes.each do |behavior|
|
15
|
+
behavior.migrations.each do |file_name|
|
16
|
+
fn_with_ext = file_name[/\.([^\.]+)$/] ? file_name : "#{file_name}.rb"
|
17
|
+
mg_version += 1
|
18
|
+
mg_version_s = mg_version.to_s.rjust(3, '0')
|
19
|
+
m.template File.join("migrations", fn_with_ext), File.join("db/migrate/#{mg_version_s}_#{fn_with_ext}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def table_name
|
26
|
+
model && model.target ? model.target.table_name : super
|
27
|
+
end
|
28
|
+
|
29
|
+
def spec
|
30
|
+
@spec ||= Rails::Generator::Spec.new("sparkly_migration", File.join(Auth.path, "auth/generators"), nil)
|
31
|
+
end
|
32
|
+
end
|