pinfirmable 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 (159) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +36 -0
  4. data/Rakefile +30 -0
  5. data/app/assets/javascripts/pinfirmable.js +80 -0
  6. data/app/controllers/devise/pinfirmable_controller.rb +56 -0
  7. data/app/mailers/pinfirmable_mailer.rb +13 -0
  8. data/app/views/devise/pinfirmable/new.html.erb +25 -0
  9. data/app/views/pinfirmable_mailer/pin_email.html.erb +5 -0
  10. data/config/locales/en.yml +12 -0
  11. data/db/migrate/20160901131628_add_pinfirmable_columns_to_resource.rb +8 -0
  12. data/lib/pinfirmable/engine.rb +11 -0
  13. data/lib/pinfirmable/hooks/pinfirmable.rb +7 -0
  14. data/lib/pinfirmable/models/pinfirmable.rb +23 -0
  15. data/lib/pinfirmable/pin.rb +13 -0
  16. data/lib/pinfirmable/routes.rb +18 -0
  17. data/lib/pinfirmable/version.rb +3 -0
  18. data/lib/pinfirmable.rb +14 -0
  19. data/lib/tasks/pinfirmable_tasks.rake +4 -0
  20. data/spec/controllers/pinfirmable_controller/create_spec.rb +60 -0
  21. data/spec/controllers/pinfirmable_controller/new_spec.rb +14 -0
  22. data/spec/controllers/pinfirmable_controller/resend_email_spec.rb +21 -0
  23. data/spec/dummy/Rakefile +6 -0
  24. data/spec/dummy/app/assets/config/manifest.js +5 -0
  25. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  26. data/spec/dummy/app/assets/javascripts/cable.js +13 -0
  27. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  28. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  29. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  30. data/spec/dummy/app/controllers/application_controller.rb +7 -0
  31. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  32. data/spec/dummy/app/jobs/application_job.rb +2 -0
  33. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  34. data/spec/dummy/app/models/application_record.rb +3 -0
  35. data/spec/dummy/app/models/user.rb +6 -0
  36. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  37. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  38. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  39. data/spec/dummy/bin/bundle +3 -0
  40. data/spec/dummy/bin/rails +4 -0
  41. data/spec/dummy/bin/rake +4 -0
  42. data/spec/dummy/bin/setup +34 -0
  43. data/spec/dummy/bin/update +29 -0
  44. data/spec/dummy/config/application.rb +14 -0
  45. data/spec/dummy/config/boot.rb +5 -0
  46. data/spec/dummy/config/cable.yml +9 -0
  47. data/spec/dummy/config/database.yml +25 -0
  48. data/spec/dummy/config/environment.rb +5 -0
  49. data/spec/dummy/config/environments/development.rb +55 -0
  50. data/spec/dummy/config/environments/production.rb +86 -0
  51. data/spec/dummy/config/environments/test.rb +42 -0
  52. data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
  53. data/spec/dummy/config/initializers/assets.rb +11 -0
  54. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  55. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  56. data/spec/dummy/config/initializers/devise.rb +274 -0
  57. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  58. data/spec/dummy/config/initializers/inflections.rb +16 -0
  59. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  60. data/spec/dummy/config/initializers/new_framework_defaults.rb +24 -0
  61. data/spec/dummy/config/initializers/session_store.rb +3 -0
  62. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  63. data/spec/dummy/config/locales/devise.en.yml +62 -0
  64. data/spec/dummy/config/locales/en.yml +23 -0
  65. data/spec/dummy/config/puma.rb +47 -0
  66. data/spec/dummy/config/routes.rb +5 -0
  67. data/spec/dummy/config/secrets.yml +22 -0
  68. data/spec/dummy/config/spring.rb +6 -0
  69. data/spec/dummy/config.ru +5 -0
  70. data/spec/dummy/db/development.sqlite3 +0 -0
  71. data/spec/dummy/db/migrate/20160901092452_devise_create_users.rb +41 -0
  72. data/spec/dummy/db/schema.rb +33 -0
  73. data/spec/dummy/db/test.sqlite3 +0 -0
  74. data/spec/dummy/log/development.log +4 -0
  75. data/spec/dummy/log/test.log +72068 -0
  76. data/spec/dummy/public/404.html +67 -0
  77. data/spec/dummy/public/422.html +67 -0
  78. data/spec/dummy/public/500.html +66 -0
  79. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  80. data/spec/dummy/public/apple-touch-icon.png +0 -0
  81. data/spec/dummy/public/favicon.ico +0 -0
  82. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/0I/0Ib_rTFdgnfYvsNReWAqAUKRVHoP48RXWpZzH31zbsA.cache +0 -0
  83. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/0_/0_BEcKcOVfBOnDXLNQYgBo66sz57rUWfK3CFy37xHAs.cache +1 -0
  84. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/0d/0dthYfJkw1ySY3H3yGN-X3YlabpIgzRctIar_VjJs-o.cache +0 -0
  85. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/0p/0pi1IJaTb-uJV_j5Apw90X2yVzONgxwSk4YGRQH_7tw.cache +0 -0
  86. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/3C/3CXox-ywyGX-ImZxHmbU1PT6T7YqLaQJgtJiXe-Gspw.cache +1 -0
  87. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/4j/4jK76DbDIsiKy6e_G5ssrieGsBWgQILORWkgoPoHGiA.cache +0 -0
  88. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/52/52mUG1lT2sxnvYn_J1cCmkSNnFBLJvyroFgPJt4XNiY.cache +1 -0
  89. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/59/590cxj6DeFThrcsMnUtKVC5E9cONxauOL9EdNRByYas.cache +1 -0
  90. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/6z/6zpMuUiqZWJ15l4nB9YTExEFA7YQFwRs2DnIu7kJNK8.cache +0 -0
  91. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/7u/7ui7Y-rju7m-pR065VlaNtEn3rOCS434sF43WeBoADA.cache +1 -0
  92. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/87/87c6IwXEO0_GPKWLX-inn2yDraMkjDHE7Gd58IfQKUM.cache +0 -0
  93. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/9W/9WGnclv7yQ7AU1vxOdZyZNQErQxvkv5TFLPsNqIHgDM.cache +1 -0
  94. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/A0/A03xbRgK3Ix-tjuc7SRE6m00FMsdS8Qxzo9Hy63qWI0.cache +1 -0
  95. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/AD/ADncNYPGmQMedyN-TDLuBvHUgK6QnKa7prDCGaPBt8Q.cache +1 -0
  96. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/BL/BL9KvH210DakvbZpa0y7uHeDCIxbVSZeVzX9ORPHklA.cache +0 -0
  97. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/CU/CUUpvLwWXnqbPsnCoZYlJRQbCE9EnVaunFNss-gK0_Q.cache +1 -0
  98. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/E7/E7BFg5Bv1-bfKP7DWLfYzAsN6zua4WSSmgFbE2LcNqI.cache +1 -0
  99. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/FV/FVK45g3SK8uvE1S5sMm2QwQDjxBG6_ivZsbGiVw_PCc.cache +2 -0
  100. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/FV/fVZX1E54KolnE_T-rx6tajbpB60V3lZJjlU3VAnU6mQ.cache +0 -0
  101. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/FW/FWJmoL0Wb5jd6OZ8nsoS5plcoVaP9UuRoDopKMk9mzI.cache +1 -0
  102. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Ff/Ff2kYMWnsRbC_7lkEB2fklWVPGGu3-mwavjevR0Dosg.cache +1 -0
  103. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Fx/Fx6vJr-0KDZ1RkfAgtgOSBu28QxN2I90ScvQq2_M7Dk.cache +1 -0
  104. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/G8/G8LtyZik__wwpmptzRrSPhOp1_2LSteMMvQ9oGD4EaE.cache +0 -0
  105. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/H1/H1NqkZMp5bVfN_gVtD5DI4IDfmEw3vLsfUl6oRnt4Tk.cache +1 -0
  106. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Hu/HuAv_RW-N_0y86EoCMOnfxRTT7CcahYAci_bjtZWK2c.cache +2 -0
  107. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Ji/JiPW4Kt5eOMvoyMvBGYS4ZTZPGkMUBmNIZ-RnfHNHfU.cache +0 -0
  108. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Kl/KlW4RRk60Viqs5yetur9CA6cPokW4yPR6zR3M9ziJ98.cache +1 -0
  109. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Lr/LrhnzGqLYA3-9bW8scwWo_Sfc_mlJEHs_FCqH4t-fuE.cache +1 -0
  110. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/OY/OY1Oc7QCzPN7zGREO3ZnVu5r4IskjAS1SqHAQ53B6tc.cache +0 -0
  111. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/PX/PXdqXI13N_erEUdt85TgWIlvOH4RTIXKMAm-wZr_JQI.cache +0 -0
  112. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Rz/RzG0b39PTQxcTGjL-Euwb8kG6fYyw0TISKfl9HnGZwI.cache +1 -0
  113. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Sm/Sm53Us6lv7P-FA8xvKLsUL_Gk1leKI1p2uuIwE4L7F0.cache +0 -0
  114. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/Up/UpwPFEUiXqWZKXzP2lqsMok5X5awuh18Oxe8sHVHPgk.cache +1 -0
  115. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/WX/WX2p7x3BZk2FlICmGW8K7ViNsPC9tAnjw78j3RgS7I4.cache +0 -0
  116. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/XZ/XZlTMeQI1tgBxD4GyAEccoJzoWl-o3VnQgiOLkLMYCU.cache +1 -0
  117. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/_J/_JqPvyIHGRAA3W5yKSWIVxPeZv39yEYAea9s_qIkTns.cache +0 -0
  118. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/ae/aeqnlkDgYUJkn3MGhS9H15kWLorV4KlpqM86mS4QKf4.cache +0 -0
  119. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/aw/awevoChN6gsPZ4sAAli2bpCg58LCjgrMO_mf6QZrEpQ.cache +1 -0
  120. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/b8/b8qWdGp0oLZ932cOSfCWz8GGZidFAwWcdPI3FGfv0B0.cache +0 -0
  121. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/b_/b_Itlk9QZZd7Rvf8kcA4yLP1R5Acu7jB-m1xQiSU0qE.cache +0 -0
  122. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/c2/c270k9jWPU8yElVyFFdlsvApVjIzN47zjATRxZrMWys.cache +0 -0
  123. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/cQ/cQz3lzvG0R6MMztIUVR97n-0miKhe2Si4LeS2xugRw0.cache +1 -0
  124. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/cc/ccZlTdoD75wgZdjc9wQXzmlu_0SXOn7WhKgsCZBoBrw.cache +1 -0
  125. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/er/erS28TTvZ3iXT8lWy2hfzZKyX3zc9WcEjuW7oCpSlpU.cache +0 -0
  126. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/fs/fsIJsHTd_I3ghT_JBxhC3LVvW6J7RQ_AMJduAVMEd-0.cache +1 -0
  127. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/g3/g39vmOVZj2M63gH281qIU23pqqpq_2Y37u-QmgGB6ek.cache +0 -0
  128. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/g9/g9coOrsutLlDERJWOPFZh6_V-WYFlzGkaHMIzofKqfc.cache +0 -0
  129. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/gR/gRsrCSxJvxqtDEsYpMWREbMrrZOhQX5FhuqdKQ0ouws.cache +2 -0
  130. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/iN/iN7mM-M4a8Rw7vcInE44X9KTxQWx-v429j0pR5bP3hE.cache +1 -0
  131. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/ic/icdeYVWN9XD-9OojrXmUYX2li_Tq3lE-HID2s9ojH9Q.cache +2 -0
  132. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/jA/jAw6wecL4SRb-h5_8dEL-ssdzoINI1SL6rA6dy9c4UQ.cache +1 -0
  133. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/k6/k66kn0XGYputzIg4YmGlQ4HqXbJZLbOWAQyzLlBCqRY.cache +2 -0
  134. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/kE/KEKhbAfFG76ispjqj05RbM9r5RbzX0k9oLw5f8e3B7o.cache +1 -0
  135. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/kE/KeyFd5cIvfUx121T3mPGzSAheDvtidWKC-91dbobm3w.cache +1 -0
  136. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/kE/kEOIAdeqXzsRmWKAkG14WUfhCwG4lyugAwvPi3yDNno.cache +1 -0
  137. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/kM/kMvNXvXf4RfdAsPUQb0mrVncC7DyH_r8AwJW4lqAvtc.cache +1 -0
  138. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/mX/mX1nlsL_SWOB4y22W5FheRX0YEONKyOY7xUeIvRiHco.cache +0 -0
  139. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/n_/N_bVbIOFifb5lMTgn3hmc4qd998Fdanj7kxicNQ7ioM.cache +0 -0
  140. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/n_/n_xYqQYhwEMQknb3jFQnjlxxBE9TzMNHCdJ-bEyZFIw.cache +0 -0
  141. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/nf/nfkiYL3GpQDOru_4361htDx6xDKWZA8kRgKWPalINJY.cache +1 -0
  142. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/nx/nxTv3sKVUQZADJyM3dPaVmUA78MIsMLD_K279yN_GsI.cache +0 -0
  143. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/r0/r0U6EPXTIVVLx4zSzDqwT0KCz3TBRzxbdXjI4d04vyM.cache +1 -0
  144. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/rD/rDsjtCmvl7oTNulriGIUbB5zA5UVnzEYJDVxpvCFu_Q.cache +1 -0
  145. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/sf/sf3QxMRblVIGu94NOkWKbkqGobdMHDIi4xOkc9UHWWo.cache +0 -0
  146. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/tU/tU8_GR1SFXrgVKIN6mFB0Rd3rP2dVJg9e0l5QlGdwbs.cache +1 -0
  147. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/ta/tanXh3aWxGrofJOeYGTreVfZb_0c3EfUq-tBmHpEHwU.cache +0 -0
  148. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/w1/w1cTNnTXc6KzCcpu2zd2ydRdPe2gr_dZGoWNRCQGXsI.cache +0 -0
  149. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/wC/wC0vwK3MtR_dBamKroNo_l5roOk3jF3xmQAPSE7NLww.cache +1 -0
  150. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/wU/wUdlJV2o6ddUH4FTwl050VtaN59iZu9b7QNI39Bbtb4.cache +1 -0
  151. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/x7/x7PYh8DJvPykcEqpVab2vcY9-GFz-3cqtoMlRAu94Uc.cache +0 -0
  152. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/xH/xHv2g42ffiUxgku0zobVrbVWM6VJAV4_ZV9b7dghdmU.cache +1 -0
  153. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/yl/yl7Cnv9zE-DNvCK1DQab8tCyKkq4V__3gSEu6VrNQZo.cache +0 -0
  154. data/spec/dummy/tmp/cache/assets/sprockets/v3.0/zR/zRKxGdH7UE371p9Qrn9c9BNa0OB0Mn9NdrEkYzUiTbM.cache +2 -0
  155. data/spec/features/resend_email_spec.rb +21 -0
  156. data/spec/features/user_signup_spec.rb +56 -0
  157. data/spec/routing/pinfirmable_routing_spec.rb +15 -0
  158. data/spec/spec_helper.rb +24 -0
  159. metadata +431 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f55d06fcbd986c88e69b9c9df912939affadb399
4
+ data.tar.gz: bc519c3cd1082adc7c732937a0cd1f4b474a7589
5
+ SHA512:
6
+ metadata.gz: 8825940ef2a09fe180a816085e036ef2676b86eab19c34ec75e0f9b019c53e04886d71b158b14e3dda6f6938582a77f1fe565a71e3aeab52d7b4d81401c7d6af
7
+ data.tar.gz: 05da6db8b180edab5297581588aec469826bf04c1b9afdc311ede93a42b4beb625b8d65d788719ffdb901f11409272de5a4efe731c996799a986d5f12e9bb015
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Yoomee Digital Ltd
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Pinfirmable
2
+ A replacement for Devise `confirmable` to use a pin rather than an emailed link to confirm a users email. (Inspired by Slack)
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'pinfirmable'
9
+ ```
10
+ And then execute:
11
+ ```bash
12
+ $ bundle
13
+ ```
14
+ Add the `pinfirmable` module to your devise model (e.g User)
15
+ ```ruby
16
+ class User < ApplicationRecord
17
+ devise :database_authenticatable,
18
+ :registerable,
19
+ ...
20
+ :pinfirmable
21
+ ```
22
+ ```bash
23
+ rake db:migrate
24
+ # If your devise model isn't called user
25
+ rake db:migrate MODEL=admin
26
+ ```
27
+ Add the javascript include to the asset pipeline.
28
+ ```
29
+ //= require pinfirmable
30
+ ```
31
+
32
+ ## Todo
33
+ - Encrypt the pin in the DB
34
+
35
+ ## License
36
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "rdoc/task"
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "Pinfirmable"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.md")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load "rails/tasks/engine.rake"
19
+
20
+ load "rails/tasks/statistics.rake"
21
+
22
+ require "bundler/gem_tasks"
23
+
24
+ require "rspec/core"
25
+ require "rspec/core/rake_task"
26
+
27
+ desc "Run all specs in spec directory (excluding plugin specs)"
28
+ RSpec::Core::RakeTask.new(spec: "app:db:test:prepare")
29
+
30
+ task default: :spec
@@ -0,0 +1,80 @@
1
+ var pinfirmable = {
2
+ init: function() {
3
+ var elem = document.getElementById('pinfirmable-noscript');
4
+ if(elem) {
5
+ elem.style.display = 'none';
6
+ }
7
+ },
8
+
9
+ autoTab: function(event) {
10
+ switch (this.detectKeyPress(event)) {
11
+ case "digit":
12
+ this.handleDigitPress(event);
13
+ event.preventDefault();
14
+ break;
15
+ case "backspace":
16
+ this.handleBackspacePress(event);
17
+ break;
18
+ case "character":
19
+ return false;
20
+ default:
21
+ return true;
22
+ }
23
+ },
24
+
25
+ handleDigitPress: function(event) {
26
+ var elem = event.srcElement;
27
+ elem.value = event.key;
28
+
29
+ var nextElem = this.nextElement(elem);
30
+ if(nextElem.type == "text" || nextElem.type == "number") {
31
+ nextElem.focus();
32
+ }
33
+ else {
34
+ elem.form.submit();
35
+ }
36
+ return true;
37
+ },
38
+
39
+ handleBackspacePress: function(event) {
40
+ var elem = event.srcElement;
41
+ if(elem.value.length > 0) {
42
+ elem.value = "";
43
+ } else {
44
+ var previousElement = this.previousElement(elem);
45
+ previousElement.value = "";
46
+ previousElement.focus();
47
+ }
48
+ },
49
+
50
+ indexOfElement: function(elem) {
51
+ var i;
52
+ var elements = elem.form.elements;
53
+ for (i=0, numElements=elements.length; i<numElements; i++) {
54
+ if (elements[i]==elem) {
55
+ break;
56
+ }
57
+ }
58
+ return i;
59
+ },
60
+
61
+ nextElement: function(elem) {
62
+ var elements = elem.form.elements;
63
+ var i = this.indexOfElement(elem);
64
+ return elements[i + 1];
65
+ },
66
+
67
+ previousElement: function(elem) {
68
+ var elements = elem.form.elements;
69
+ var i = this.indexOfElement(elem);
70
+ return elements[i - 1];
71
+ },
72
+
73
+ detectKeyPress: function(event) {
74
+ if(event.keyCode === 8) { return "backspace"; }
75
+ if(!isNaN(event.key - parseFloat(event.key))) { return "digit"; }
76
+ if(/\S/.test(String.fromCharCode(event.keyCode))) { return "character"; }
77
+ }
78
+ };
79
+
80
+ pinfirmable.init();
@@ -0,0 +1,56 @@
1
+ module Devise
2
+ class PinfirmableController < DeviseController
3
+ def new
4
+ @locked_out = locked_out?
5
+ end
6
+
7
+ def create
8
+ if locked_out?
9
+ @locked_out = true
10
+ render(:new, status: 429) && return
11
+ end
12
+
13
+ if Pinfirmable::Pin.new(params[:digits]).matches_user_pin(pinfirmable_user)
14
+ pinfirmable_user.update_attribute(:pinfirmable_pin, nil)
15
+ redirect_to after_confirmation_path_for(resource_name, pinfirmable_user)
16
+ else
17
+ tries = pinfirmable_user.pinfirmable_tries += 1
18
+ lockout = (tries % 3).zero? ? (tries / 3).minute.from_now : nil
19
+ pinfirmable_user.update_attributes(
20
+ pinfirmable_tries: tries,
21
+ pinfirmable_lockout: lockout
22
+ )
23
+ redirect_to user_confirmemail_path
24
+ end
25
+ end
26
+
27
+ def resend_email
28
+ flash[:notice] = "We have just resent your code"
29
+ PinfirmableMailer.pin_email(pinfirmable_user).deliver
30
+ redirect_to user_confirmemail_path
31
+ end
32
+
33
+ protected
34
+
35
+ def pinfirmable_user
36
+ request.env["CHECKING_PINFIRMABLE_PIN"] = true
37
+ pu = current_user
38
+ request.env["CHECKING_PINFIRMABLE_PIN"] = false
39
+ pu
40
+ end
41
+
42
+ def locked_out?
43
+ return false unless pinfirmable_user
44
+ return false if pinfirmable_user.pinfirmable_lockout.nil?
45
+ pinfirmable_user.pinfirmable_lockout > Time.now
46
+ end
47
+
48
+ def after_confirmation_path_for(resource_name, resource)
49
+ if signed_in?(resource_name)
50
+ signed_in_root_path(resource)
51
+ else
52
+ new_session_path(resource_name)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ class PinfirmableMailer < ApplicationMailer
2
+ def pin_email(user)
3
+ @code_part1 = user.pinfirmable_pin[0..2]
4
+ @code_part2 = user.pinfirmable_pin[3..6]
5
+ mail(
6
+ to: user.email,
7
+ subject: default_i18n_subject(
8
+ code_part1: @code_part1,
9
+ code_part2: @code_part2
10
+ )
11
+ )
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ <div class="pinfirmable-panel">
2
+ <h1><%= t("confirm.title") %></h1>
3
+ <p><%= simple_format t("confirm.body") %></p>
4
+ <%= form_tag("/users/pinfirmable", method: "post", id: "pinfirmable-form") do %>
5
+ <div id="pinfirmable-pin-inputs">
6
+ <%= text_field_tag "digits[]", "", id: "digits_1", class: "pin-input", maxlength: "1", onKeyDown: "return pinfirmable.autoTab(event)", onFocus: "return this.select()", autocomplete: "off", autofocus: true %>
7
+ <%= text_field_tag "digits[]", "", id: "digits_2", class: "pin-input", maxlength: "1", onKeyDown: "return pinfirmable.autoTab(event)", onFocus: "return this.select()", autocomplete: "off" %>
8
+ <%= text_field_tag "digits[]", "", id: "digits_3", class: "pin-input", maxlength: "1", onKeyDown: "return pinfirmable.autoTab(event)", onFocus: "return this.select()", autocomplete: "off" %>
9
+ <span class="pin-separator">&nbsp;</span>
10
+ <%= text_field_tag "digits[]", "", id: "digits_4", class: "pin-input", maxlength: "1", onKeyDown: "return pinfirmable.autoTab(event)", onFocus: "return this.select()", autocomplete: "off" %>
11
+ <%= text_field_tag "digits[]", "", id: "digits_5", class: "pin-input", maxlength: "1", onKeyDown: "return pinfirmable.autoTab(event)", onFocus: "return this.select()", autocomplete: "off" %>
12
+ <%= text_field_tag "digits[]", "", id: "digits_6", class: "pin-input", maxlength: "1", onKeyDown: "return pinfirmable.autoTab(event)", onFocus: "return this.select()", autocomplete: "off" %>
13
+ </div>
14
+ <%= submit_tag("GO!", id: "pinfirmable-noscript") %>
15
+ <% end %>
16
+ <% if @locked_out %>
17
+ <div class="pinfirmable-alert">
18
+ <%= t("confirm.rate_limit") %>
19
+ </div>
20
+ <% end %>
21
+
22
+ <%= form_tag("/users/pinfirmable/resend_email", method: "post", id: "pinfirmable-resend-form") do %>
23
+ <%= submit_tag(t("confirm.no_email")) %>
24
+ <% end %>
25
+ </div>
@@ -0,0 +1,5 @@
1
+ <h1>Confirm your email with the following code</h1>
2
+
3
+ <h2><%= t(".code", code_part1: @code_part1, code_part2: @code_part2) %></h2>
4
+
5
+ Thanks
@@ -0,0 +1,12 @@
1
+ en:
2
+ pinfirmable_mailer:
3
+ pin_email:
4
+ subject: "Confirmation code: %{code_part1}-%{code_part2}"
5
+ code: "%{code_part1}-%{code_part2}"
6
+ confirm:
7
+ title: "Thanks. We've sent you an email"
8
+ body: |
9
+ The email we have sent you contains a 6 digit code. Please enter your code below.
10
+ We do this to make sure that we are creating your account with the correct email address and to stop any account being created in error.
11
+ no_email: "Not received an email?"
12
+ rate_limit: "You're trying too many times, please try again in a minute."
@@ -0,0 +1,8 @@
1
+ class AddPinfirmableColumnsToResource < ActiveRecord::Migration[5.0]
2
+ def change
3
+ model = (ENV["MODEL"] || "user").downcase.pluralize.to_sym
4
+ add_column model, :pinfirmable_pin, :string
5
+ add_column model, :pinfirmable_tries, :integer, default: 0
6
+ add_column model, :pinfirmable_lockout, :datetime, default: nil
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module Pinfirmable
2
+ class Engine < ::Rails::Engine
3
+ initializer :append_migrations do |app|
4
+ unless app.root.to_s.match root.to_s
5
+ config.paths["db/migrate"].expanded.each do |expanded_path|
6
+ app.config.paths["db/migrate"] << expanded_path
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ Warden::Manager.after_set_user do |user, warden, _options|
2
+ unless warden.env["CHECKING_PINFIRMABLE_PIN"] || !user.pinfirmable_pin.present?
3
+ response = Rack::Response.new
4
+ response.redirect "/users/confirmemail"
5
+ throw :warden, response.finish
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ module Devise
2
+ module Models
3
+ module Pinfirmable
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ before_create :generate_confirmation_token
7
+ after_commit :send_confirmation_instructions, on: :create
8
+ end
9
+
10
+ protected
11
+
12
+ def generate_confirmation_token
13
+ pin = ""
14
+ 6.times { pin << SecureRandom.random_number(9).to_s }
15
+ self.pinfirmable_pin = pin
16
+ end
17
+
18
+ def send_confirmation_instructions
19
+ PinfirmableMailer.pin_email(self).deliver
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module Pinfirmable
2
+ class Pin
3
+ attr_accessor :digits
4
+
5
+ def initialize(digits)
6
+ @digits = digits.flatten.join
7
+ end
8
+
9
+ def matches_user_pin(user)
10
+ user.pinfirmable_pin == @digits
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ class Mapper
4
+ protected
5
+
6
+ def devise_pinfirmable(mapping, controllers)
7
+ resource :pinfirmable,
8
+ only: [:create],
9
+ path: mapping.path_names[:pinfirmable],
10
+ controller: controllers[:pinfirmable] do
11
+ post :resend_email
12
+ end
13
+
14
+ get :confirmemail, to: :new, controller: controllers[:pinfirmable], action: :new
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Pinfirmable
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,14 @@
1
+ require "pinfirmable/engine"
2
+ require "devise"
3
+
4
+ module Pinfirmable
5
+ end
6
+
7
+ Devise.add_module :pinfirmable,
8
+ controller: :pinfirmable,
9
+ route: :pinfirmable,
10
+ model: "pinfirmable/models/pinfirmable"
11
+
12
+ require "pinfirmable/routes"
13
+ require "pinfirmable/pin"
14
+ require "pinfirmable/hooks/pinfirmable"
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :pinfirmable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+ require "timecop"
3
+
4
+ RSpec.describe Devise::PinfirmableController do
5
+ describe "POST create" do
6
+ before do
7
+ @request.env["devise.mapping"] = Devise.mappings[:user]
8
+ @user = User.create(email: "test@example.com", password: "password")
9
+ sign_in @user
10
+ end
11
+
12
+ context "correct pin" do
13
+ it "returns success" do
14
+ post :create, params: { digits: @user.pinfirmable_pin.split("") }
15
+ expect(response).to redirect_to("/")
16
+ end
17
+ end
18
+
19
+ context "incorrect pin" do
20
+ let(:pin) { %w(1 2 3 4 5 6) }
21
+
22
+ it "returns success" do
23
+ post :create, params: { digits: pin }
24
+ expect(response).to redirect_to("/users/confirmemail")
25
+ end
26
+
27
+ it "increments the tries" do
28
+ post :create, params: { digits: pin }
29
+ expect(@user.reload.pinfirmable_tries).to eq(1)
30
+ end
31
+
32
+ it "after 3 tries the user is stopped from trying anymore pins for 1 minute" do
33
+ Timecop.freeze(Time.now)
34
+ 3.times { post :create, params: { digits: pin } }
35
+ expect(@user.reload.pinfirmable_lockout).to eq(1.minute.from_now)
36
+ end
37
+
38
+ it "after 6 tries the user is stopped from trying anymore pins for 1 minute" do
39
+ 3.times { post :create, params: { digits: pin } }
40
+ Timecop.freeze(2.minutes.from_now)
41
+ 3.times { post :create, params: { digits: pin } }
42
+ expect(@user.reload.pinfirmable_lockout).to eq(2.minutes.from_now)
43
+ end
44
+
45
+ it "while locked out no tries are allowed" do
46
+ Timecop.freeze(Time.now)
47
+ 4.times { post :create, params: { digits: pin } }
48
+ expect(response.status).to eq(429)
49
+ end
50
+
51
+ it "after the lockout you can try again" do
52
+ Timecop.freeze(Time.now)
53
+ 4.times { post :create, params: { digits: pin } }
54
+ Timecop.freeze(2.minutes.from_now)
55
+ post :create, params: { digits: pin }
56
+ expect(@user.reload.pinfirmable_tries).to eq(4)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Devise::PinfirmableController do
4
+ describe "GET new" do
5
+ before do
6
+ @request.env["devise.mapping"] = Devise.mappings[:user]
7
+ end
8
+
9
+ it "returns success" do
10
+ get :new
11
+ expect(response).to be_success
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Devise::PinfirmableController do
4
+ describe "POST resend_email" do
5
+ before do
6
+ @request.env["devise.mapping"] = Devise.mappings[:user]
7
+ @user = User.create(email: "test@example.com", password: "password")
8
+ sign_in @user
9
+ end
10
+
11
+ it "redirects back to the confirm page" do
12
+ post :resend_email
13
+ expect(response).to be_redirect
14
+ end
15
+
16
+ it "sets the flash" do
17
+ post :resend_email
18
+ expect(flash[:notice]).to eq("We have just resent your code")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,5 @@
1
+
2
+ //= link_tree ../images
3
+ //= link_directory ../javascripts .js
4
+ //= link_directory ../stylesheets .css
5
+ //= link pinfirmable_manifest.js
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,13 @@
1
+ // Action Cable provides the framework to deal with WebSockets in Rails.
2
+ // You can generate new channels where WebSocket features live using the rails generate channel command.
3
+ //
4
+ //= require action_cable
5
+ //= require_self
6
+ //= require_tree ./channels
7
+
8
+ (function() {
9
+ this.App || (this.App = {});
10
+
11
+ App.cable = ActionCable.createConsumer();
12
+
13
+ }).call(this);
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Channel < ActionCable::Channel::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ApplicationCable
2
+ class Connection < ActionCable::Connection::Base
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+
4
+ def index
5
+ render plain: "Hello world!"
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationJob < ActiveJob::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: "from@example.com"
3
+ layout "mailer"
4
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,6 @@
1
+ class User < ApplicationRecord
2
+ # Include default devise modules. Others available are:
3
+ # :confirmable, :lockable, :timeoutable and :omniauthable
4
+ devise :database_authenticatable, :registerable,
5
+ :recoverable, :rememberable, :trackable, :validatable, :pinfirmable
6
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= csrf_meta_tags %>
6
+
7
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
8
+ <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
9
+ </head>
10
+
11
+ <body>
12
+ <%= yield %>
13
+ </body>
14
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
3
+ load Gem.bin_path("bundler", "bundle")