wobauth 3.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +165 -0
- data/Rakefile +30 -0
- data/app/assets/javascripts/wobauth/admin.js +13 -0
- data/app/assets/javascripts/wobauth/authorities.js.coffee +31 -0
- data/app/assets/javascripts/wobauth/groups.js +2 -0
- data/app/assets/javascripts/wobauth/memberships.js +2 -0
- data/app/assets/javascripts/wobauth/roles.js +2 -0
- data/app/assets/stylesheets/wobauth/application.css +15 -0
- data/app/assets/stylesheets/wobauth/authorities.css +4 -0
- data/app/assets/stylesheets/wobauth/groups.css +4 -0
- data/app/assets/stylesheets/wobauth/memberships.css +4 -0
- data/app/assets/stylesheets/wobauth/roles.css +4 -0
- data/app/controllers/wobauth/ad_users_controller.rb +24 -0
- data/app/controllers/wobauth/application_controller.rb +25 -0
- data/app/controllers/wobauth/authorities_controller.rb +88 -0
- data/app/controllers/wobauth/groups/authorities_controller.rb +11 -0
- data/app/controllers/wobauth/groups/memberships_controller.rb +11 -0
- data/app/controllers/wobauth/groups_controller.rb +60 -0
- data/app/controllers/wobauth/login_controller.rb +10 -0
- data/app/controllers/wobauth/memberships_controller.rb +79 -0
- data/app/controllers/wobauth/registrations_controller.rb +26 -0
- data/app/controllers/wobauth/roles_controller.rb +30 -0
- data/app/controllers/wobauth/users/authorities_controller.rb +11 -0
- data/app/controllers/wobauth/users/memberships_controller.rb +11 -0
- data/app/controllers/wobauth/users_controller.rb +76 -0
- data/app/helpers/wobauth/ad_users_helper.rb +60 -0
- data/app/helpers/wobauth/application_helper.rb +53 -0
- data/app/helpers/wobauth/authorities_helper.rb +9 -0
- data/app/models/wobauth/ad_user.rb +4 -0
- data/app/models/wobauth/admin_ability.rb +67 -0
- data/app/models/wobauth/authority.rb +17 -0
- data/app/models/wobauth/group.rb +18 -0
- data/app/models/wobauth/membership.rb +11 -0
- data/app/models/wobauth/role.rb +19 -0
- data/app/models/wobauth/user.rb +11 -0
- data/app/services/wobauth/search_ad_user_service.rb +53 -0
- data/app/views/devise/registrations/edit.html.erb +25 -0
- data/app/views/devise/sessions/new.html.erb +17 -0
- data/app/views/devise/shared/_links.html.erb +0 -0
- data/app/views/wobauth/ad_users/index.html.erb +69 -0
- data/app/views/wobauth/authorities/_form.html.erb +32 -0
- data/app/views/wobauth/authorities/edit.html.erb +1 -0
- data/app/views/wobauth/authorities/index.html.erb +41 -0
- data/app/views/wobauth/authorities/new.html.erb +1 -0
- data/app/views/wobauth/authorities/show.html.erb +45 -0
- data/app/views/wobauth/groups/_form.html.erb +20 -0
- data/app/views/wobauth/groups/_group_memberships.html.erb +21 -0
- data/app/views/wobauth/groups/_group_roles.html.erb +23 -0
- data/app/views/wobauth/groups/edit.html.erb +1 -0
- data/app/views/wobauth/groups/index.html.erb +31 -0
- data/app/views/wobauth/groups/new.html.erb +1 -0
- data/app/views/wobauth/groups/show.html.erb +45 -0
- data/app/views/wobauth/memberships/_form.html.erb +20 -0
- data/app/views/wobauth/memberships/edit.html.erb +1 -0
- data/app/views/wobauth/memberships/index.html.erb +33 -0
- data/app/views/wobauth/memberships/new.html.erb +1 -0
- data/app/views/wobauth/memberships/show.html.erb +28 -0
- data/app/views/wobauth/roles/_role_authorities.html.erb +21 -0
- data/app/views/wobauth/roles/index.html.erb +26 -0
- data/app/views/wobauth/roles/show.html.erb +18 -0
- data/app/views/wobauth/shared/_accounting.html.erb +25 -0
- data/app/views/wobauth/shared/_admin.html.erb +13 -0
- data/app/views/wobauth/users/_form.html.erb +28 -0
- data/app/views/wobauth/users/_user_groups.html.erb +22 -0
- data/app/views/wobauth/users/_user_roles.html.erb +23 -0
- data/app/views/wobauth/users/edit.html.erb +1 -0
- data/app/views/wobauth/users/index.html.erb +52 -0
- data/app/views/wobauth/users/new.html.erb +1 -0
- data/app/views/wobauth/users/show.html.erb +130 -0
- data/config/initializers/assets.rb +1 -0
- data/config/initializers/devise.rb +260 -0
- data/config/initializers/devise_failure_app.rb +21 -0
- data/config/initializers/simple_form.rb +169 -0
- data/config/initializers/simple_form_bootstrap.rb +154 -0
- data/config/initializers/wobapphelpers.rb +18 -0
- data/config/locales/de.yml +80 -0
- data/config/locales/devise.de.yml +60 -0
- data/config/locales/devise.en.yml +59 -0
- data/config/locales/en.yml +27 -0
- data/config/locales/simple_form.en.yml +31 -0
- data/config/locales/wobapphelpers.de.yml +10 -0
- data/config/locales/wobapphelpers.en.yml +8 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20140501113226_create_wobauth_roles.rb +9 -0
- data/db/migrate/20140501150743_create_wobauth_groups.rb +10 -0
- data/db/migrate/20140504124045_create_wobauth_memberships.rb +11 -0
- data/db/migrate/20140504143328_create_wobauth_authorities.rb +15 -0
- data/db/migrate/20140508120810_devise_create_wobauth_users.rb +55 -0
- data/db/migrate/20171231084355_additional_fields_to_wobauth_user.rb +8 -0
- data/lib/concerns/models/user.rb +44 -0
- data/lib/generators/templates/initializers/wobauth.rb +20 -0
- data/lib/generators/wobauth/install_generator.rb +20 -0
- data/lib/tasks/wobauth_tasks.rake +4 -0
- data/lib/templates/erb/scaffold/_form.html.erb +26 -0
- data/lib/templates/erb/scaffold/edit.html.erb +1 -0
- data/lib/templates/erb/scaffold/index.html.erb +32 -0
- data/lib/templates/erb/scaffold/new.html.erb +1 -0
- data/lib/templates/erb/scaffold/show.html.erb +15 -0
- data/lib/templates/rails/scaffold_controller/controller.rb +66 -0
- data/lib/wobauth/engine.rb +33 -0
- data/lib/wobauth/version.rb +4 -0
- data/lib/wobauth.rb +57 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/db/test1.sqlite3 +0 -0
- data/test/dummy/log/test.log +1954 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/-6/-6x3utBDawbjV5d477TJ9PeEoQTk9Yh292Yg_8Ox16U.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/-A/-A9u9WxFQ9YT2TSLvmYJYJf2Kg9JfDoIt6YZQRcwUHE.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/-C/-CJCPQLf_lB4gJr983MP5sZdH3uZuYPIe-xSw4QBln4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/-K/-K6Cz6iJ4bgvnwN-rMFxBtyYEU6EGdLH9b4N38_GwIs.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/-x/-xwsVQHMHEQFqvVNjKKkEiujw9_CUgnJ2Hmzampfy60.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/2J/2JNdqFTZ9i8DAT1rZCc-Xj70u9zcQtlJoqEK68sRPVM.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/4N/4NSPDtUmuTVU5uLI-eBtnORjzrwVgeLgrgIJh4tjWeQ.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/4u/4uuqiFX5bLo0J5QqMPSZc642NRkdX4l-Vguvyq6Oe3U.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/5O/5Oqd8RObzOeHuHy3NTiGE5p_ZkpftjQQgJgS_3KKdH0.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/6U/6UZEqCkIx3XO_NjOjjWyLET2-dS4Gs_RPAB9NtA99-A.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/76/767sF803BgrHtDgUPUUn-HN41e6fklZIQkVKyKD59P4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/77/77ZyE9pECDHt-19ik7nBaJXJQT-7V2UDr49kFOIO1DM.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/7n/7nJvPEaj-WV6RCKHc3hpHsAbWek8cidSKvDKLedQpH4.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/85/856_llbCcD4ntIG6h4uR2WQY9SrutzA-9K6xT53ZPTY.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/9A/9A6WX6bvqMDoyDrYrGQy2l6nf_JtHoQZsa-pEJ0FgDU.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/BA/BABtX6k2NZISYrxZSviAXC8UIxZfRhEDDbx7dMf_ShQ.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/D4/D4_Vl_wB6Vq8O0LvQ9iHt-YqG_c0fUR4af06OZlk10k.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/DX/DXGc2xprS_TuG5PzNCaBHKBMWTlyrZZYA9jlvlcBn3M.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Ex/ExDgDLUsesHZC1FAxhQLmTyBpg6IW4gVCUB8U8Ov-X4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/F8/F8M6mPdg93dPiXZzHGo3Y0I42et4yc5Hr-eCfuOUeac.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Hk/Hkx4xbJvgHfFgfNDKyHxertTGvARWOgVuNuGU-OWPEU.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Jo/Jom6Wnb-Sd-d3W24hNHS58BHtuw5Dsm7vvWVPtLB0PA.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/K5/K5UnItDV1g9ixIoFloUxCNirxL5UFrOT0jz1vKy5shc.cache +31 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/K6/K6geC8zADOljHu2R2-DHUgVrgGyij9e4dnxgIdEypMU.cache +2 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/KW/KWpcGMHEfc0mpysCx46leXLgyrzubUIXWouB91Ia5Ik.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/LD/LD1fywEt5YfgacZhvpug0VXFakWRb1jidort_y8Fl00.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/LT/LTqeGcgCnDytohL1bGvcUZdnrp6gImr9Gzgfj6sYtW4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/LX/LXirLCvjN2zWwKoda0ZdfOoFufIGRC9bZFKo95Kxs2w.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/MH/MH3Sbn3D_A-k2L9Qd_MSjC2d6zv52DnzX3REO1P8n-E.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Nm/NmUDbY8fbYAMYUH3HOmHzTLkeJaf-49yXUpF5TXGnYM.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/O1/O1xVVUGePb-AZZi4nfbefQqZJEuQKvVAnHbbksdys0g.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Og/Og28UaPh4fHs8CazZFMRRClJKMdtcZ6TLkcrwXO9888.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Py/Py0iDud1Fnb6eLZHFV1BlJ1PKkeJS5bSVb8DL5Lyoyo.cache +2 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Qs/QscUYhnZ_Tw9H1UICqO4jqvLwSznyhEMlkTJRFR63Qk.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Rx/RxBkonRxRoJzfP-2eiuZPIg4Wy1eaNd6MQ6N0QTzLbw.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/S_/S_VAM9XIEPYa1eIrH2MGCvwgC6UuKNU3IsOsXYCbRfE.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/TK/TK13HHucosr5_ne6uScM6nK6naCAYu11S4GxXyvGEA4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/TP/TPYDffricRYUN-8EoF99EbRAm5m9QVJ4NsV4JQHUaTU.cache +2 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/TQ/TQZclZQajgRAt0UPyDLa5SDHOXXd7nrs_Aw1guZ1EDE.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/U-/U-j-KeDrdeyrmflpuJSSH3aH_tGth_0zwnjl9eaecCU.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Ui/Ui_ImrdWE848w92udaqCheUJypT-7QIicGTDDcF94ZY.cache +2 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Vy/VyeoEaFiHk-BFfUz0QQyO1ywWUjhhvXaWjOhrueWuz4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/WC/WCZBAZL9AYx33c-WzmPLL8ZaSkyPBwQEpJSzDMruSTA.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/XG/XG-1Cxql9XcU0QzVsKfaImD2F-_fbQakmCOO1PZeiO8.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Y-/Y-mfMOkq6o7o6NIVKY5m1HzoQPj6deRrSVoSnePwWDg.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/Z5/Z56duv_o-wQlnyich8DqrY8VfI5mRampXLuyzcCQSes.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/_X/_XFCt_G6l_CeYOky6Ky04YbAJMGmZ1S55wnsXuzjbFQ.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/_c/_cAbQgu5_t6QaopeTV6bHpVH6nABdZSHUD3dtw3TFvI.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/aU/aUB5QePK5Y_57d0ISu6VcHykCHzl47ej-nXJSzHo7mM.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/ao/ao7sMvRzih2MlCNwwQnYbm-FUNM2rl1wlruiVgH8VbE.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/b8/b8sc4SHWqZyiR9LoszM7AwU7Z9RStRdUzs5Cm4b1xi4.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/dG/dGu-1JGz62Geuns1TpCaYbAwvwLKjSjDp_VhKV3nWOw.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/dH/dHU4yPNGwU72V1rymqid8RLS4ole8B-9UGSJG-QyDsQ.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/ei/eiJS8jxPa_CT4ECt01iVkhPzc9QQShE__hFgFUVMqhc.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/hq/hqAVf5Mc94swL5QKxbwrtNQ1F6-4shrOpiZMk60yjMM.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/hz/hzzNh1KGk98zydo_p20ZW15iDYFoCsUZuYdK99A54Mo.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/ip/ip5FbXNQpdaOzVbU6_r90bYcY8R47qbE2_C79IUdlvQ.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/je/je9xQVOnasRNd1k49Q5KJdmxQscPKO-8qeVDMmmsVrI.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/jt/jtOzRwEskymBEhztMwmAwvXzROImYepCy7Zb25fxexQ.cache +2 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/m3/m3BDc8KCgIUXE0VeZ142byDvlbYuOEBpNW_2ucKyb3M.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/mf/mfkdFcsUrRviUKGZkElnxw6dCyhyArAFbAVNuqQKcsE.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/mx/mxQkXyjT__7etUOHa4WpbGL4YguqInaF8rQXNK4c6JU.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/o4/o4yHfopv-ZxcXaMdi1_2O0CkBEWWQEd0wR0tEwKJ8s8.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/oF/oFjp7US8e9Cu4GOLqlO_vPIaX9jYwqrYbCFq6mgaYWE.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/op/opKuUcXieH4Q4hch708zXOG2Rud2bhIilb38wKiOdjA.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/p4/p4U2tbFHGXaOchAVtbEfIHRvWKO2i-0nsq_4vtGNABU.cache +2 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/s4/s4jckxw_fz9fwDYqkrZPCsD7cCATrXbD5R7ducBhtoU.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/uP/uPXk7u0shmDjGGOhtU06VSo7E81DYc2t9OGhQUvxEA0.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/vS/vSQW3iohJ14LLnwWgoHFeHB-I3yIzLi9tLoZkUaE81M.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/vu/vuRt78a_J3dg7DUMHX1Nx9DYLME15Z1NjQ-4V8BoNCU.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/w6/w6Gz48zxIouRBGOu5VmTHWsL9r0M4LKp124UOJ3mZN0.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/wj/wjsRbpDa20LzSzj66UKcgy3Wif53rLBEoeuyKIKz4pk.cache +3 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/wo/wo1g4ZKtmQmol-3MoVPFApwd5fnGEA23tCRCLuEw3D0.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/xG/xGQ1RojQn3N9C51pkj4Flt274TjVuszRi1Puir0p6dE.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/xN/xNqJDUb7i6VSD0HU-EV3dcNYRoz2d48cGwn1CBqLvVU.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/y1/y1pwuOg2qdJFc_DgDEw6OIyr6GbmGpwV-p7X2sS64sU.cache +0 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/zO/zOWzdA_n0s0vjbJ6IGB8gj1-mbqzbCywIQCMLr71qDE.cache +1 -0
- data/test/dummy/tmp/cache/assets/sprockets/v3.0/zV/zVdKofyDUgEZzwP0GQ8q4RxOoxo-qoGazLMre0zxw9c.cache +0 -0
- data/test/tmp/config/initializers/wobauth.rb +15 -0
- data/test/tmp/config/locales/wobauth.de.yml +42 -0
- data/test/tmp/config/locales/wobauth.en.yml +27 -0
- metadata +636 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 493a4e3df5e29ffb7a98829d5b3ef40ade384f0d
|
4
|
+
data.tar.gz: b9b53c1b9b7ee643151caf805aa9d763eee3e3d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bddbfd66597d8ce2d21fcaa3e1ae30784fd1b8e9b996353c327f48399f7c769fc1b05fb5b5031e45f5b1dff03430e85c3bf979cd437f9e19f6279db8c9bcd393
|
7
|
+
data.tar.gz: f2a3b3b33f8c23d14e73aa505609957368acd59307dedb1cbaf3d77575d488dcec6970b22a41241a87b2ddf0b803f6a6236ea1ceead12c1f31e271a521788da9
|
data/README.md
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
wobauth
|
2
|
+
=======
|
3
|
+
|
4
|
+
Rails engine providing MVCs for Users, Roles, Groups, Authorities and Memberships
|
5
|
+
to support authorization of the main application.
|
6
|
+
|
7
|
+
The User model is mostly coupled with your application, so Wobauth::User is open classed.
|
8
|
+
You should overwrite it depending on your needs. See
|
9
|
+
test/dummy/app/models/wobauth/user.rb for an example.
|
10
|
+
|
11
|
+
You have to build your own concrete authorization logic with cancancan or
|
12
|
+
something similiar. The basic principle
|
13
|
+
is always the same. A subject (user or group) has some rights (the role) on
|
14
|
+
concret objects. The objects may be organizational units, sites, categories
|
15
|
+
or something else whatever your application provides. The object may be nil
|
16
|
+
if your application doesn't need this feature. The role define the rights
|
17
|
+
dependent on your logic, mostly likely read, create, update and destroy.
|
18
|
+
|
19
|
+
Roles are intended to be set from migration or seed and not to be edited by
|
20
|
+
an admin, the logic is mostly hardcoded if based on cancan(can). Users can be
|
21
|
+
members of group. Memberships are separate model here (not only an plain
|
22
|
+
many-to-many association) to allow both manual memberships and
|
23
|
+
automatically created memberships during the login process.
|
24
|
+
|
25
|
+
Versions
|
26
|
+
--------
|
27
|
+
|
28
|
+
wobauth 1.x is for Rails 4, wobauth 2.x will support Rails 5.
|
29
|
+
wobauth 3.x uses bootstrap v4 and font-awesome, starting with Rails 5.1
|
30
|
+
(may work with Rails 5.0, but not tested)
|
31
|
+
|
32
|
+
Requirements
|
33
|
+
------------
|
34
|
+
|
35
|
+
| branch | rails | ruby | bootstrap | icons | wobapphelpers |
|
36
|
+
|------------|-------|--------|-----------|-----------------|---------------|
|
37
|
+
| master | >=5.1 | >= 2.3 | v4 | fontawesome 4.7 | master |
|
38
|
+
| 2-0-stable | 5.0 | >= 2.2 | v3 | glyphicons | 2-0-stable |
|
39
|
+
| 1-0-stable | 4.2 | >= 2.0 | v3 | glyphicons | 1-0-stable |
|
40
|
+
|
41
|
+
|
42
|
+
* simple_form >= 3.3
|
43
|
+
|
44
|
+
Installation
|
45
|
+
------------
|
46
|
+
Add wobauth to your Gemfile:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
gem 'wobauth', git: 'https://github.com/swobspace/wobauth.git', branch: 'master'
|
50
|
+
gem 'wobauth', git: 'https://github.com/swobspace/wobauth.git', branch: '2-0-stable'
|
51
|
+
gem 'wobauth', git: 'https://github.com/swobspace/wobauth.git', branch: '1-0-stable'
|
52
|
+
```
|
53
|
+
Run
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
rails g wobauth:install
|
57
|
+
```
|
58
|
+
to create an example configuration in ''config/initializers/wobauth.rb''
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
rake wobauth:install:migrations
|
62
|
+
```
|
63
|
+
copies wobauth migration files wobauth to your application. Do this before you
|
64
|
+
create your own migration files if possible. If you upgrade from an older wobauth
|
65
|
+
version rerun it. There might be new migration files added.
|
66
|
+
|
67
|
+
Configuration
|
68
|
+
-------------
|
69
|
+
|
70
|
+
### User model
|
71
|
+
|
72
|
+
To customize the user model to your needs, create app/models/wobauth/users.rb in
|
73
|
+
your application:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
# main_app/app/models/wobauth/user.rb
|
77
|
+
|
78
|
+
class Wobauth::User < ActiveRecord::Base
|
79
|
+
include Wobauth::Concerns::Models::User
|
80
|
+
|
81
|
+
devise :database_authenticatable, :registerable,
|
82
|
+
:recoverable, :rememberable, :trackable
|
83
|
+
|
84
|
+
... add your associations and methods ...
|
85
|
+
end
|
86
|
+
```
|
87
|
+
**DO NOT USE :validatable**, since wobauth uses :username as authentication key
|
88
|
+
(devise default :email, will be required if you use :validatable).
|
89
|
+
|
90
|
+
### Views
|
91
|
+
|
92
|
+
Helpers for bootstrap navbar:
|
93
|
+
|
94
|
+
* `navigation_account_links` : navigation partial for login/userprofile/logout
|
95
|
+
* `navigation_admin_links` : navigation partial for user/roles/authorities ...
|
96
|
+
|
97
|
+
### Authorized_for types
|
98
|
+
|
99
|
+
If you have objects for which wobauth should provide authority configuration,
|
100
|
+
set your models in the initializer (created by ```rails g wobauth:install```).
|
101
|
+
In this example we will use Category from your main application:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
Wobauth.setup do |config|
|
105
|
+
#
|
106
|
+
# Configuration for Authorization
|
107
|
+
# 1. Subject: Authorizable
|
108
|
+
# do not change it unless you know exactly what you are doing
|
109
|
+
#
|
110
|
+
# config.authorizable_types = [ "Wobauth::User", "Wobauth::Group" ]
|
111
|
+
#
|
112
|
+
# 2. Object: Authorized_for
|
113
|
+
# depends on your application ...
|
114
|
+
# default: []
|
115
|
+
|
116
|
+
config.authorized_for_types = [ "Category" ]
|
117
|
+
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
You can create and delete authority records within wobauth, but you have to build
|
122
|
+
your own authorization with cancan(can) in your main application.
|
123
|
+
|
124
|
+
### Routes
|
125
|
+
Mount the rails engine:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
# config/routes.rb
|
129
|
+
Rails.application.routes.draw do
|
130
|
+
...
|
131
|
+
mount Wobauth::Engine, at: '/auth'
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
With rails 5.1 there are some problems with engine routing in engines.
|
136
|
+
Add the `mount` statement at the bottom of `Rails.application.routes.draw` and
|
137
|
+
use always a suburl for mount like `/auth`. Don't use `/`.
|
138
|
+
|
139
|
+
Using datatables
|
140
|
+
--------------------------
|
141
|
+
|
142
|
+
Wobauth comes with support for datatables. To select the tables, use the
|
143
|
+
following jQuery selector:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
$('table[role="wobauth_datatable"]')
|
147
|
+
```
|
148
|
+
|
149
|
+
`$('dataTable')` may be removed in a future release.
|
150
|
+
|
151
|
+
Applications using wobauth
|
152
|
+
--------------------------
|
153
|
+
If you are looking for examples using wobauth, have a look at
|
154
|
+
[boskop](https://github.com/swobspace/boskop).
|
155
|
+
|
156
|
+
Another simple example is the [test/dummy](test/dummy) application
|
157
|
+
included in this rails engine.
|
158
|
+
|
159
|
+
Licence
|
160
|
+
-------
|
161
|
+
|
162
|
+
wobauth Copyright (C) 2014-2018 Wolfgang Barth
|
163
|
+
|
164
|
+
MIT license, see [LICENSE](LICENSE)
|
165
|
+
|
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 = 'Wobauth'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
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
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
# require 'rake/testtask'
|
23
|
+
require 'rspec/core/rake_task'
|
24
|
+
|
25
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
26
|
+
t.pattern = Dir.glob('spec/**/*_spec.rb')
|
27
|
+
# t.rspec_opts = '--format documentation'
|
28
|
+
end
|
29
|
+
|
30
|
+
task default: [:spec]
|
@@ -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 vendor/assets/javascripts of plugins, if any, 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.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require wobauth/authorities
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# authorizable
|
3
|
+
#
|
4
|
+
|
5
|
+
# - start with given values
|
6
|
+
auth_types = $('#authority_authorizable_type').find("option:selected").val()
|
7
|
+
$('#authority_authorizable_id optgroup').find('option').addBack().hide()
|
8
|
+
$("#authority_authorizable_id optgroup[label=\"#{auth_types}\"]").find('option').addBack().show()
|
9
|
+
|
10
|
+
# - redisplay on change
|
11
|
+
$(document).on('click', '#authority_authorizable_type', ->
|
12
|
+
type = $(this).find("option:selected").val()
|
13
|
+
$('#authority_authorizable_id optgroup').find('option').addBack().hide()
|
14
|
+
$("#authority_authorizable_id optgroup[label=\"#{type}\"]").find('option').addBack().show()
|
15
|
+
)
|
16
|
+
|
17
|
+
#
|
18
|
+
# authorized_for
|
19
|
+
#
|
20
|
+
|
21
|
+
# - start with given values
|
22
|
+
authfor_type = $('#authority_authorized_for_type').find("option:selected").val()
|
23
|
+
$('#authority_authorized_for_id optgroup').find('option').addBack().hide()
|
24
|
+
$("#authority_authorized_for_id optgroup[label=\"#{authfor_type}\"]").find('option').addBack().show()
|
25
|
+
|
26
|
+
# - redisplay on change
|
27
|
+
$(document).on('click', '#authority_authorized_for_type', ->
|
28
|
+
type = $(this).find("option:selected").val()
|
29
|
+
$('#authority_authorized_for_id optgroup').find('option').addBack().hide()
|
30
|
+
$("#authority_authorized_for_id optgroup[label=\"#{type}\"]").find('option').addBack().show()
|
31
|
+
)
|
@@ -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 vendor/assets/stylesheets of plugins, if any, 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 styles
|
10
|
+
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
11
|
+
* file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_dependency "wobauth/application_controller"
|
2
|
+
|
3
|
+
module Wobauth
|
4
|
+
class AdUsersController < ApplicationController
|
5
|
+
def index
|
6
|
+
if search_params.present?
|
7
|
+
result = SearchAdUserService.new(search_params.to_h).call
|
8
|
+
unless result.success?
|
9
|
+
flash[:error] = result.error_messages.join(", ")
|
10
|
+
end
|
11
|
+
@ad_users = result.ad_users
|
12
|
+
else
|
13
|
+
@ad_users = []
|
14
|
+
end
|
15
|
+
respond_with(@ad_users)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def search_params
|
21
|
+
params.permit(:query, :utf8, :authenticity_token, :bci).slice(:query)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Wobauth
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
# -- use the application default layout
|
4
|
+
layout "application"
|
5
|
+
|
6
|
+
# -- devise
|
7
|
+
before_action :authenticate_user!, :unless => :devise_controller?
|
8
|
+
# -- cancan
|
9
|
+
load_and_authorize_resource unless: :devise_controller?
|
10
|
+
|
11
|
+
# -- breadcrumbs
|
12
|
+
include Wobapphelpers::Breadcrumbs
|
13
|
+
before_action :add_breadcrumb_index, only: [:index]
|
14
|
+
|
15
|
+
# -- flash responder
|
16
|
+
self.responder = Wobapphelpers::Responders
|
17
|
+
respond_to :html, :json
|
18
|
+
|
19
|
+
|
20
|
+
# -- cancan ability for wobauth
|
21
|
+
def current_ability
|
22
|
+
@current_ability ||= Wobauth::AdminAbility.new(current_user)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_dependency "wobauth/application_controller"
|
2
|
+
|
3
|
+
module Wobauth
|
4
|
+
class AuthoritiesController < ApplicationController
|
5
|
+
skip_load_and_authorize_resource
|
6
|
+
load_and_authorize_resource class: Wobauth::Authority
|
7
|
+
|
8
|
+
before_action :set_authority, only: [:show, :edit, :update, :destroy]
|
9
|
+
before_action :add_breadcrumb_show, only: [:show]
|
10
|
+
|
11
|
+
# GET /authorities
|
12
|
+
def index
|
13
|
+
@authorities = Authority.accessible_by(current_ability, :read)
|
14
|
+
respond_with(@authorities)
|
15
|
+
end
|
16
|
+
|
17
|
+
# GET /authorities/1
|
18
|
+
def show
|
19
|
+
respond_with(@authority)
|
20
|
+
end
|
21
|
+
|
22
|
+
# GET /authorities/new
|
23
|
+
def new
|
24
|
+
if @authorizable.present?
|
25
|
+
@authority = @authorizable.authorities.new(authorized_for_params)
|
26
|
+
else
|
27
|
+
@authority = Authority.new(authorized_for_params)
|
28
|
+
end
|
29
|
+
respond_with(@authority)
|
30
|
+
end
|
31
|
+
|
32
|
+
# GET /authorities/1/edit
|
33
|
+
def edit
|
34
|
+
end
|
35
|
+
|
36
|
+
# POST /authorities
|
37
|
+
def create
|
38
|
+
myparams = authorized_for_params.merge(authority_params)
|
39
|
+
if @authorizable.present?
|
40
|
+
@authority = @authorizable.authorities.new(myparams)
|
41
|
+
else
|
42
|
+
@authority = Authority.new(myparams)
|
43
|
+
end
|
44
|
+
|
45
|
+
@authority.save
|
46
|
+
respond_with(@authority, location: location)
|
47
|
+
end
|
48
|
+
|
49
|
+
# PATCH/PUT /authorities/1
|
50
|
+
def update
|
51
|
+
@authority.update(authority_params)
|
52
|
+
respond_with(@authority, location: location)
|
53
|
+
end
|
54
|
+
|
55
|
+
# DELETE /authorities/1
|
56
|
+
def destroy
|
57
|
+
@authority.destroy
|
58
|
+
respond_with(@authority, location: location)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
# Use callbacks to share common setup or constraints between actions.
|
63
|
+
def set_authority
|
64
|
+
@authority = Authority.find(params[:id])
|
65
|
+
end
|
66
|
+
|
67
|
+
# Only allow a trusted parameter "white list" through.
|
68
|
+
def authority_params
|
69
|
+
params.require(:authority).permit(:authorizable_id, :authorizable_type, :role_id, :authorized_for_id, :authorized_for_type, :valid_from, :valid_until)
|
70
|
+
end
|
71
|
+
|
72
|
+
def authorized_for_params
|
73
|
+
{
|
74
|
+
authorized_for_id: @authorized_for&.id,
|
75
|
+
authorized_for_type: @authorized_for&.model_name
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# if @authorizable exist: authorizable/authorizable_id#authority
|
80
|
+
# else authority/authority_id
|
81
|
+
#
|
82
|
+
def location
|
83
|
+
polymorphic_path(
|
84
|
+
(@authorizable || @authorized_for || @authority),
|
85
|
+
anchor: ('authorities' if @authorizable || @authorized_for))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_dependency "wobauth/application_controller"
|
2
|
+
|
3
|
+
module Wobauth
|
4
|
+
class GroupsController < ApplicationController
|
5
|
+
before_action :set_group, only: [:show, :edit, :update, :destroy]
|
6
|
+
before_action :add_breadcrumb_show, only: [:show]
|
7
|
+
|
8
|
+
# GET /groups
|
9
|
+
def index
|
10
|
+
@groups = Group.all
|
11
|
+
respond_with(@groups)
|
12
|
+
end
|
13
|
+
|
14
|
+
# GET /groups/1
|
15
|
+
def show
|
16
|
+
respond_with(@group)
|
17
|
+
end
|
18
|
+
|
19
|
+
# GET /groups/new
|
20
|
+
def new
|
21
|
+
@group = Group.new
|
22
|
+
respond_with(@group)
|
23
|
+
end
|
24
|
+
|
25
|
+
# GET /groups/1/edit
|
26
|
+
def edit
|
27
|
+
end
|
28
|
+
|
29
|
+
# POST /groups
|
30
|
+
def create
|
31
|
+
@group = Group.new(group_params)
|
32
|
+
|
33
|
+
@group.save
|
34
|
+
respond_with(@group)
|
35
|
+
end
|
36
|
+
|
37
|
+
# PATCH/PUT /groups/1
|
38
|
+
def update
|
39
|
+
@group.update(group_params)
|
40
|
+
respond_with(@group)
|
41
|
+
end
|
42
|
+
|
43
|
+
# DELETE /groups/1
|
44
|
+
def destroy
|
45
|
+
@group.destroy
|
46
|
+
respond_with(@group)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
# Use callbacks to share common setup or constraints between actions.
|
51
|
+
def set_group
|
52
|
+
@group = Group.find(params[:id])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Only allow a trusted parameter "white list" through.
|
56
|
+
def group_params
|
57
|
+
params.require(:group).permit(:name, :description)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require_dependency "wobauth/application_controller"
|
2
|
+
|
3
|
+
module Wobauth
|
4
|
+
class MembershipsController < ApplicationController
|
5
|
+
skip_load_and_authorize_resource
|
6
|
+
load_and_authorize_resource class: Wobauth::Authority
|
7
|
+
|
8
|
+
before_action :set_membership, only: [:show, :edit, :update, :destroy]
|
9
|
+
before_action :add_breadcrumb_show, only: [:show]
|
10
|
+
|
11
|
+
# GET /memberships
|
12
|
+
def index
|
13
|
+
@memberships = Membership.accessible_by(current_ability, :read)
|
14
|
+
respond_with(@memberships)
|
15
|
+
end
|
16
|
+
|
17
|
+
# GET /memberships/1
|
18
|
+
def show
|
19
|
+
respond_with(@membership)
|
20
|
+
end
|
21
|
+
|
22
|
+
# GET /memberships/new
|
23
|
+
def new
|
24
|
+
if @membershipable.present?
|
25
|
+
@membership = @membershipable.memberships.new
|
26
|
+
else
|
27
|
+
@membership = Membership.new
|
28
|
+
end
|
29
|
+
respond_with(@membership)
|
30
|
+
end
|
31
|
+
|
32
|
+
# GET /memberships/1/edit
|
33
|
+
def edit
|
34
|
+
end
|
35
|
+
|
36
|
+
# POST /memberships
|
37
|
+
def create
|
38
|
+
if @membershipable.present?
|
39
|
+
@membership = @membershipable.memberships.new(membership_params)
|
40
|
+
else
|
41
|
+
@membership = Membership.new(membership_params)
|
42
|
+
end
|
43
|
+
|
44
|
+
@membership.save
|
45
|
+
respond_with(@membership, location: location)
|
46
|
+
end
|
47
|
+
|
48
|
+
# PATCH/PUT /memberships/1
|
49
|
+
def update
|
50
|
+
@membership.update(membership_params)
|
51
|
+
respond_with(@membership, location: location)
|
52
|
+
end
|
53
|
+
|
54
|
+
# DELETE /memberships/1
|
55
|
+
def destroy
|
56
|
+
@membership.destroy
|
57
|
+
respond_with(@membership, location: location)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
# Use callbacks to share common setup or constraints between actions.
|
62
|
+
def set_membership
|
63
|
+
@membership = Membership.find(params[:id])
|
64
|
+
end
|
65
|
+
|
66
|
+
# Only allow a trusted parameter "white list" through.
|
67
|
+
def membership_params
|
68
|
+
params.require(:membership).permit(:user_id, :group_id)
|
69
|
+
end
|
70
|
+
|
71
|
+
# if @membershipable exist: membershipable/membershipable_id#authority
|
72
|
+
# else authority/authority_id
|
73
|
+
#
|
74
|
+
def location
|
75
|
+
polymorphic_path((@membershipable || @membership), anchor: ('memberships' if @membershipable))
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Wobauth
|
2
|
+
class RegistrationsController < Devise::RegistrationsController
|
3
|
+
|
4
|
+
protected
|
5
|
+
|
6
|
+
def after_sign_up_path_for(resource)
|
7
|
+
main_app.root_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def after_update_path_for(resource)
|
11
|
+
main_app.root_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_inactive_sign_up_path_for(resource)
|
15
|
+
main_app.root_path
|
16
|
+
end
|
17
|
+
|
18
|
+
def sign_up_params
|
19
|
+
params.require(:user).permit(:username, :email, :password, :password_confirmation)
|
20
|
+
end
|
21
|
+
|
22
|
+
def account_update_params
|
23
|
+
params.require(:user).permit(:username, :email, :password, :password_confirmation, :current_password)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_dependency "wobauth/application_controller"
|
2
|
+
|
3
|
+
module Wobauth
|
4
|
+
class RolesController < ApplicationController
|
5
|
+
before_action :set_role, only: [:show, :edit, :update, :destroy]
|
6
|
+
before_action :add_breadcrumb_show, only: [:show]
|
7
|
+
|
8
|
+
# GET /roles
|
9
|
+
def index
|
10
|
+
@roles = Role.all
|
11
|
+
respond_with(@roles)
|
12
|
+
end
|
13
|
+
|
14
|
+
# GET /roles/1
|
15
|
+
def show
|
16
|
+
respond_with(@role)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
# Use callbacks to share common setup or constraints between actions.
|
21
|
+
def set_role
|
22
|
+
@role = Role.find(params[:id])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Only allow a trusted parameter "white list" through.
|
26
|
+
def role_params
|
27
|
+
params.require(:role).permit(:name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|