ioquatix-account_engine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/README +0 -0
  2. data/lib/account_engine/configuration.rb +101 -0
  3. data/lib/account_engine/controller.rb +246 -0
  4. data/lib/account_engine/helper.rb +104 -0
  5. data/lib/account_engine/password.rb +432 -0
  6. data/lib/account_engine/support.rb +12 -0
  7. data/lib/account_engine/user_account/class_methods.rb +63 -0
  8. data/lib/account_engine/user_account.rb +184 -0
  9. data/lib/account_engine.rb +63 -0
  10. data/rails/app/controllers/account_controller.rb +162 -0
  11. data/rails/app/controllers/permissions_controller.rb +90 -0
  12. data/rails/app/controllers/roles_controller.rb +133 -0
  13. data/rails/app/controllers/users_controller.rb +144 -0
  14. data/rails/app/helpers/account_helper.rb +3 -0
  15. data/rails/app/helpers/permissions_helper.rb +3 -0
  16. data/rails/app/helpers/roles_helper.rb +3 -0
  17. data/rails/app/helpers/users_helper.rb +3 -0
  18. data/rails/app/models/permission.rb +129 -0
  19. data/rails/app/models/role.rb +60 -0
  20. data/rails/app/models/user.rb +5 -0
  21. data/rails/app/models/user_notify.rb +75 -0
  22. data/rails/app/views/account/_form.rhtml +8 -0
  23. data/rails/app/views/account/change_password.rhtml +17 -0
  24. data/rails/app/views/account/edit.rhtml +5 -0
  25. data/rails/app/views/account/forgot_password.rhtml +12 -0
  26. data/rails/app/views/account/home.rhtml +3 -0
  27. data/rails/app/views/account/login.rhtml +27 -0
  28. data/rails/app/views/account/logout.rhtml +8 -0
  29. data/rails/app/views/account/signup.rhtml +28 -0
  30. data/rails/app/views/permissions/_form.rhtml +14 -0
  31. data/rails/app/views/permissions/_list.rhtml +38 -0
  32. data/rails/app/views/permissions/edit.rhtml +5 -0
  33. data/rails/app/views/permissions/index.rhtml +3 -0
  34. data/rails/app/views/permissions/new.rhtml +5 -0
  35. data/rails/app/views/roles/_form.rhtml +8 -0
  36. data/rails/app/views/roles/_permissions.rhtml +25 -0
  37. data/rails/app/views/roles/edit.rhtml +5 -0
  38. data/rails/app/views/roles/index.rhtml +34 -0
  39. data/rails/app/views/roles/new.rhtml +5 -0
  40. data/rails/app/views/roles/show.rhtml +20 -0
  41. data/rails/app/views/user_notify/change_password.rhtml +10 -0
  42. data/rails/app/views/user_notify/delete.rhtml +5 -0
  43. data/rails/app/views/user_notify/forgot_password.rhtml +11 -0
  44. data/rails/app/views/user_notify/pending_delete.rhtml +9 -0
  45. data/rails/app/views/user_notify/signup.rhtml +12 -0
  46. data/rails/app/views/users/_form.rhtml +12 -0
  47. data/rails/app/views/users/edit.rhtml +5 -0
  48. data/rails/app/views/users/index.rhtml +38 -0
  49. data/rails/app/views/users/new.rhtml +5 -0
  50. data/rails/app/views/users/roles.rhtml +42 -0
  51. data/rails/app/views/users/show.rhtml +36 -0
  52. data/rails/assets/images/default/omnipotent.png +0 -0
  53. data/rails/assets/images/default/system.png +0 -0
  54. data/rails/assets/images/permissions/create.png +0 -0
  55. data/rails/assets/images/permissions/sync.png +0 -0
  56. data/rails/assets/images/roles/add_permission.png +0 -0
  57. data/rails/assets/images/roles/create.png +0 -0
  58. data/rails/assets/images/roles/edit.png +0 -0
  59. data/rails/assets/images/roles/remove_permission.png +0 -0
  60. data/rails/assets/images/roles/user.png +0 -0
  61. data/rails/assets/images/table_background.png +0 -0
  62. data/rails/assets/images/users/create.png +0 -0
  63. data/rails/assets/images/users/destroy.png +0 -0
  64. data/rails/assets/images/users/edit.png +0 -0
  65. data/rails/assets/images/users/show.png +0 -0
  66. data/rails/assets/javascripts/account_engine.js +166 -0
  67. data/rails/assets/stylesheets/account_engine.css +7 -0
  68. data/rails/assets/stylesheets/check_password.css +10 -0
  69. data/rails/assets/stylesheets/simple.css +168 -0
  70. data/rails/db/migrate/001_initial_schema.rb +49 -0
  71. data/rails/init.rb +21 -0
  72. data/rails/routes.rb +5 -0
  73. data/rails/tasks/account_engine.rake +123 -0
  74. metadata +165 -0
@@ -0,0 +1,38 @@
1
+ <h1>Listing Users</h1>
2
+
3
+ <table id="user_list" class="listing">
4
+ <thead>
5
+ <tr>
6
+ <th>Login</th>
7
+ <th> </th>
8
+ <th>Email</th>
9
+ <th>Created</th>
10
+ <th>Logged In</th>
11
+ <th>Roles</th>
12
+ <th> </th>
13
+ </tr>
14
+ </thead>
15
+ <tfoot>
16
+ <tr>
17
+ <td colspan="7">
18
+ <%= link_if_authorized icon_tag(:create) + ' New User', new_user_path %>
19
+ </td>
20
+ </tr>
21
+ </tfoot>
22
+ <tbody>
23
+ <% for user in @users %>
24
+ <tr>
25
+ <td><%= link_if_authorized user.fullname, :action => 'show', :id => user.id %></td>
26
+ <td><%= link_if_authorized icon_tag(:edit), {:action => 'edit', :id => user} %></td>
27
+ <td><%= mail_to user.email, user.email %></td>
28
+ <td><%= user.created_at.strftime("%H:%I %Z, %d %b %Y") if user.created_at %></td>
29
+ <td><%= (user.logged_in_at.strftime("%H:%I %Z, %d %b %Y") if user.logged_in_at) || '-' %></td>
30
+ <td><%= user.roles.collect { |r| link_if_authorized r.name, :controller => 'roles', :action => 'show', :id => r.id }.join(", ") %></td>
31
+ <td><%= link_if_authorized icon_tag('edit', :controller => 'roles'), :action => 'roles', :id => user.id %></td>
32
+ </tr>
33
+ <% end %>
34
+ </tbody>
35
+ </table>
36
+
37
+ <%= will_paginate @users %>
38
+
@@ -0,0 +1,5 @@
1
+ <h1>New User</h1>
2
+
3
+ <% table_form_for :user, :url => {:action => :create} do |f| %>
4
+ <%= f.render_form %>
5
+ <% end %>
@@ -0,0 +1,42 @@
1
+ <h1>Roles for User: <%= @user.fullname %></h1>
2
+
3
+ <table class="listing">
4
+ <thead>
5
+ <tr>
6
+ <th>Role</th>
7
+ <th>Granted</th>
8
+ <th>Revoke</th>
9
+ <th>Description</th>
10
+ </tr>
11
+ </thead>
12
+ <tfoot>
13
+ <tr>
14
+ <td colspan="4">
15
+ <%= link_to icon_tag('create', :controller => 'roles') + " New Role", :controller => 'roles', :action => 'new' %>
16
+ </td>
17
+ </tr>
18
+ </tfoot>
19
+ <% @roles.each do |r| %>
20
+ <tr>
21
+ <td><%= link_if_authorized r.name, :controller => 'roles', :action => 'show', :id => r.id %></td>
22
+ <% if @user.roles.include? r %>
23
+ <td>
24
+ <b>Granted</b>
25
+ </td>
26
+ <td>
27
+ <%= link_to "Revoke", {:action => 'remove_role', :id => @user.id, :role => r.id}, :post => true %>
28
+ </td>
29
+ <% else %>
30
+ <td>
31
+ <%= link_to "Grant", {:action => 'add_role', :id => @user.id, :role => r.id}, :post => true %>
32
+ </td>
33
+ <td>
34
+ </td>
35
+ <% end %>
36
+ <td>
37
+ <%= r.description %>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ </table>
42
+
@@ -0,0 +1,36 @@
1
+ <h1>User <%= @user.login %></h1>
2
+
3
+ <h2>Account Details</h2>
4
+
5
+ <dl>
6
+ <dt>User ID</dt>
7
+ <dd><%= @user.id %></dd>
8
+ <dt>Login</dt>
9
+ <dd><%= @user.login %></dd>
10
+ <dt>Email</dt>
11
+ <dd><%= @user.email %></dd>
12
+ </dl>
13
+
14
+ <dl>
15
+ <dt>Account created</dt>
16
+ <dd><%= @user.created_at %></dd>
17
+ <dt>Updated at</dt>
18
+ <dd><%= @user.updated_at %></dd>
19
+ <dt>Last logged in at</dt>
20
+ <dd><%= @user.logged_in_at %></dd>
21
+ </dl>
22
+
23
+ <h2>Account Status</h2>
24
+
25
+ <dl>
26
+ <dt>Verified?</dt>
27
+ <dd><%= @user.verified? ? "Yes" : "No" %></dd>
28
+ <dt>Deleted?</dt>
29
+ <dd><%= @user.deleted? ? "Yes" : "No" %></dd>
30
+ <% if @user.security_token != nil %>
31
+ <dt>Security Token</dt>
32
+ <dd><%= @user.security_token %></dd>
33
+ <dt>Token Expiry</dt>
34
+ <dd><%= @user.token_expiry %></dd>
35
+ <% end %>
36
+ </dl>
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,166 @@
1
+
2
+ function updatePasswordStatus (okay) {
3
+ var passwordChangeButton = $("passwordChangeButton");
4
+ var errorImage = $("passwordErrorStatus");
5
+
6
+ if (okay) {
7
+ if (errorImage) {
8
+ errorImage.removeClassName("okay");
9
+ errorImage.addClassName("error");
10
+ }
11
+
12
+ passwordChangeButton.disabled = 1;
13
+ } else {
14
+ if (errorImage) {
15
+ errorImage.removeClassName("error");
16
+ errorImage.addClassName("okay");
17
+ }
18
+
19
+ passwordChangeButton.disabled = null;
20
+ }
21
+ }
22
+
23
+ function checkPasswords () {
24
+ var password = $F("password");
25
+ var passwordCopy = $F("passwordCopy");
26
+
27
+ if (password == "") {
28
+ updatePasswordStatus(false);
29
+ }
30
+
31
+ updatePasswordStatus(password != passwordCopy)
32
+ }
33
+
34
+ // http://www.movable-type.co.uk/scripts/sha1.html
35
+ function sha1Hash(msg)
36
+ {
37
+ // constants [§4.2.1]
38
+ var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
39
+
40
+
41
+ // PREPROCESSING
42
+
43
+ msg += String.fromCharCode(0x80); // add trailing '1' bit to string [§5.1.1]
44
+
45
+ // convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
46
+ var l = Math.ceil(msg.length/4) + 2; // long enough to contain msg plus 2-word length
47
+ var N = Math.ceil(l/16); // in N 16-int blocks
48
+ var M = new Array(N);
49
+ for (var i=0; i<N; i++) {
50
+ M[i] = new Array(16);
51
+ for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding
52
+ M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |
53
+ (msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));
54
+ }
55
+ }
56
+ // add length (in bits) into final pair of 32-bit integers (big-endian) [5.1.1]
57
+ // note: most significant word would be ((len-1)*8 >>> 32, but since JS converts
58
+ // bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
59
+ M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14])
60
+ M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
61
+
62
+ // set initial hash value [§5.3.1]
63
+ var H0 = 0x67452301;
64
+ var H1 = 0xefcdab89;
65
+ var H2 = 0x98badcfe;
66
+ var H3 = 0x10325476;
67
+ var H4 = 0xc3d2e1f0;
68
+
69
+ // HASH COMPUTATION [§6.1.2]
70
+
71
+ var W = new Array(80); var a, b, c, d, e;
72
+ for (var i=0; i<N; i++) {
73
+
74
+ // 1 - prepare message schedule 'W'
75
+ for (var t=0; t<16; t++) W[t] = M[i][t];
76
+ for (var t=16; t<80; t++) W[t] = ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
77
+
78
+ // 2 - initialise five working variables a, b, c, d, e with previous hash value
79
+ a = H0; b = H1; c = H2; d = H3; e = H4;
80
+
81
+ // 3 - main loop
82
+ for (var t=0; t<80; t++) {
83
+ var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants
84
+ var T = (ROTL(a,5) + f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff;
85
+ e = d;
86
+ d = c;
87
+ c = ROTL(b, 30);
88
+ b = a;
89
+ a = T;
90
+ }
91
+
92
+ // 4 - compute the new intermediate hash value
93
+ H0 = (H0+a) & 0xffffffff; // note 'addition modulo 2^32'
94
+ H1 = (H1+b) & 0xffffffff;
95
+ H2 = (H2+c) & 0xffffffff;
96
+ H3 = (H3+d) & 0xffffffff;
97
+ H4 = (H4+e) & 0xffffffff;
98
+ }
99
+
100
+ return H0.toHexStr() + H1.toHexStr() + H2.toHexStr() + H3.toHexStr() + H4.toHexStr();
101
+ }
102
+
103
+ //
104
+ // function 'f' [§4.1.1]
105
+ //
106
+ function f(s, x, y, z)
107
+ {
108
+ switch (s) {
109
+ case 0: return (x & y) ^ (~x & z); // Ch()
110
+ case 1: return x ^ y ^ z; // Parity()
111
+ case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj()
112
+ case 3: return x ^ y ^ z; // Parity()
113
+ }
114
+ }
115
+
116
+ //
117
+ // rotate left (circular left shift) value x by n positions [§3.2.5]
118
+ //
119
+ function ROTL(x, n)
120
+ {
121
+ return (x<<n) | (x>>>(32-n));
122
+ }
123
+
124
+ //
125
+ // extend Number class with a tailored hex-string method
126
+ // (note toString(16) is implementation-dependant, and
127
+ // in IE returns signed numbers when used on full words)
128
+ //
129
+ Number.prototype.toHexStr = function()
130
+ {
131
+ var s="", v;
132
+ for (var i=7; i>=0; i--) { v = (this>>>(i*4)) & 0xf; s += v.toString(16); }
133
+ return s;
134
+ }
135
+
136
+ def self.hashed(str)
137
+ # check if a salt has been set...
138
+ if AccountEngine.salt == nil
139
+ raise "You must define a :salt value in the configuration for the AccountEngine module."
140
+ end
141
+
142
+ return Digest::SHA1.hexdigest("#{AccountEngine.salt}-#{str}}")[0..39]
143
+ end
144
+
145
+ var siteSalt = "...";
146
+ var userSalt = "...";
147
+ var challenge = "...";
148
+
149
+ function hashed(s) {
150
+ return sha1Hash(siteSalt + s)
151
+ }
152
+
153
+ // Challenge-response JS..
154
+ function computeResponse(password, challenge) {
155
+ hashedPassword = hashed(userSalt + password);
156
+
157
+ hashedChallenge = hashed(challenge + hashedPassword);
158
+
159
+ return hashedChallenge;
160
+ }
161
+
162
+ function processForm() {
163
+ currentPassword = $F("password");
164
+
165
+ $F("password") = computeResponse(currentPassword);
166
+ }
@@ -0,0 +1,7 @@
1
+ .granted {
2
+ background-color: #cfc;
3
+ }
4
+
5
+ .unassigned {
6
+ background-color: #fcc;
7
+ }
@@ -0,0 +1,10 @@
1
+ #passwordErrorStatus {
2
+ }
3
+
4
+ #passwordErrorStatus.okay {
5
+ border: 1px dashed green;
6
+ }
7
+
8
+ #passwordErrorStatus.error {
9
+ border: 1px dashed red;
10
+ }
@@ -0,0 +1,168 @@
1
+ .granted {
2
+ background-color: #cfc;
3
+ }
4
+
5
+ .unassigned {
6
+ background-color: #fcc;
7
+ }
8
+
9
+ h1 {
10
+ border-bottom: 3px solid #bbb;
11
+ text-shadow: 3px 1px #ccc;
12
+ }
13
+
14
+ form table {
15
+ /*border-collapse: collapse;*/
16
+ width: 80%;
17
+ margin: auto;
18
+
19
+ font-size: 80%;
20
+ background-color: #eef;
21
+
22
+ padding: 10px;
23
+
24
+ border: 1px solid #779;
25
+ }
26
+
27
+ form table tr > :first-child {
28
+ text-align: right;
29
+ width: 30%;
30
+ }
31
+
32
+ form table tr.form_errors td {
33
+ text-align: left;
34
+ }
35
+
36
+ form table td[colspan="2"] {
37
+ border-top: 1px solid #666;
38
+ background-color: #fafaff;
39
+ padding: 5px;
40
+ }
41
+
42
+ textarea, input, table.detail_select {
43
+ padding: 2px;
44
+ margin: 2px;
45
+ }
46
+
47
+ textarea {
48
+ width: 80%;
49
+ height: 60px;
50
+ }
51
+
52
+ table.listing {
53
+ font-size: 75%;
54
+
55
+ border-collapse: collapse;
56
+ width: 100%;
57
+
58
+ border: 1px solid black;
59
+ }
60
+
61
+ table.listing td {
62
+ padding: 5px;
63
+ }
64
+
65
+ table.listing tbody .description {
66
+ width: 25%;
67
+ font-size: 90%;
68
+ }
69
+
70
+ table.listing thead {
71
+ font-weight: bold;
72
+ background: url(/plugin_assets/account_engine/images/table_background.png);
73
+ }
74
+
75
+ table.listing .when {
76
+ width: 80px;
77
+ text-align: center;
78
+ }
79
+
80
+ table.listing .total, table.listing .subtotal, table.listing .unitprice, table.listing .quantity, table.listing .tax {
81
+ text-align: right;
82
+ width: 9%;
83
+ border-left: 1px dashed #ccc;
84
+ }
85
+
86
+ table.listing tfoot {
87
+ background: url(/plugin_assets/account_engine/images/table_background.png);
88
+ }
89
+
90
+ table.listing tfoot td {
91
+ text-align: right;
92
+ font-weight: bold;
93
+ }
94
+
95
+ table.listing tr .s {
96
+ cbackground-color: #aac;
97
+ ccolor: white;
98
+ }
99
+
100
+ table.detail_select {
101
+ background-color: white;
102
+ }
103
+
104
+ .icon {
105
+ vertical-align: -20%;
106
+ }
107
+
108
+ img {
109
+ border: none;
110
+ }
111
+
112
+ a {
113
+ color: #00f;
114
+ text-decoration: none;
115
+ }
116
+
117
+ a:hover {
118
+ color: #99f;
119
+ }
120
+
121
+ #menu {
122
+ padding: 2px;
123
+ border: 1px dashed #333;
124
+ background-color: #abc;
125
+ padding-left: 10px;
126
+ }
127
+
128
+ #menu a {
129
+ font-size: 15px;
130
+ margin-right: 5px;
131
+ padding: 2px 6px 2px 6px;
132
+
133
+ color: #22f;
134
+ text-decoration: none;
135
+
136
+ text-shadow: 2px 2px 2px #666;
137
+ }
138
+
139
+ #menu a:hover {
140
+ border-top: 4px solid black;
141
+ border-bottom: 4px solid black;
142
+ }
143
+
144
+ #menu a:active {
145
+ background-color: #cde;
146
+ }
147
+
148
+ #messages {
149
+ margin: 10px;
150
+ }
151
+
152
+ h2 {
153
+ font-size: 14px;
154
+ padding: 2px;
155
+ background-color: #ececec;
156
+ }
157
+
158
+ .property_list {
159
+ border: 1px dashed #ccf;
160
+ padding: 2px;
161
+ margin: 10px;
162
+ }
163
+
164
+ .property_list li {
165
+ display: inline-block;
166
+ padding: 2px;
167
+ background-color: #efefef;
168
+ }
@@ -0,0 +1,49 @@
1
+ class InitialSchema < ActiveRecord::Migration
2
+ def self.up
3
+ create_table AccountEngine.users_table do |t|
4
+ t.column "login", :string, :limit => 80, :default => "", :null => false
5
+ t.column "salted_password", :string, :limit => 40, :default => ""
6
+ t.column "salt", :string, :limit => 40, :default => ""
7
+ t.column "verified", :integer, :default => 0
8
+ t.column "security_token", :string, :limit => 40
9
+ t.column "token_expiry", :datetime
10
+ t.column "created_at", :datetime
11
+ t.column "updated_at", :datetime
12
+ t.column "logged_in_at", :datetime
13
+ t.column "deleted", :integer, :default => 0
14
+ t.column "email", :string, :limit => 60
15
+ end
16
+
17
+ create_table AccountEngine.permissions_table, :force => true do |t|
18
+ t.column "controller", :string, :default => "", :null => false
19
+ t.column "action", :string, :default => "", :null => false
20
+ t.column "system", :boolean, :default => false, :null => false
21
+ t.column "description", :string
22
+ end
23
+
24
+ create_table AccountEngine.permissions_roles_table, :id => false, :force => true do |t|
25
+ t.column "permission_id", :integer, :default => 0, :null => false
26
+ t.column "role_id", :integer, :default => 0, :null => false
27
+ end
28
+
29
+ create_table AccountEngine.users_roles_table, :id => false, :force => true do |t|
30
+ t.column "user_id", :integer, :default => 0, :null => false
31
+ t.column "role_id", :integer, :default => 0, :null => false
32
+ end
33
+
34
+ create_table AccountEngine.roles_table, :force => true do |t|
35
+ t.column "name", :string, :default => "", :null => false
36
+ t.column "description", :string
37
+ t.column "system", :boolean, :default => false, :null => false
38
+ t.column "omnipotent", :boolean, :default => false, :null => false
39
+ end
40
+ end
41
+
42
+ def self.down
43
+ drop_table AccountEngine.permissions_table
44
+ drop_table AccountEngine.permissions_roles_table
45
+ drop_table AccountEngine.users_roles_table
46
+ drop_table AccountEngine.roles_table
47
+ drop_table AccountEngine.users_table
48
+ end
49
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Copyright (c) 2006 Sammi Williams <sammi@oriontransfer.co.nz>
2
+ #
3
+ # The GNU General Public License (GPL)
4
+ # Version 2, June 1991
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
+
20
+ # load up all the required files we need...
21
+ require 'account_engine'
data/rails/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+
2
+ resource :account, :controller => "account", :collection => { :login => :get, :logout => :get, :home => :get }
3
+ resources :permissions, :collection => { :resync => :get }
4
+ resources :roles, :member => { :add_permission => :post, :remove_permission => :post }
5
+ resources :users