trainmaster 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +286 -0
- data/Rakefile +38 -0
- data/app/controllers/trainmaster/application_controller.rb +9 -0
- data/app/controllers/trainmaster/sessions_controller.rb +141 -0
- data/app/controllers/trainmaster/users_controller.rb +199 -0
- data/app/helpers/trainmaster/application_helper.rb +313 -0
- data/app/helpers/trainmaster/sessions_helper.rb +4 -0
- data/app/helpers/trainmaster/users_helper.rb +4 -0
- data/app/jobs/trainmaster/sessions_cleanup_job.rb +13 -0
- data/app/mailers/application_mailer.rb +4 -0
- data/app/mailers/trainmaster/user_mailer.rb +14 -0
- data/app/models/trainmaster/session.rb +56 -0
- data/app/models/trainmaster/user.rb +77 -0
- data/app/views/layouts/mailer.html.erb +5 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/layouts/trainmaster/application.html.erb +14 -0
- data/app/views/trainmaster/user_mailer/email_verification.html.erb +12 -0
- data/app/views/trainmaster/user_mailer/email_verification.text.erb +13 -0
- data/app/views/trainmaster/user_mailer/password_reset.html.erb +14 -0
- data/app/views/trainmaster/user_mailer/password_reset.text.erb +15 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20161120020344_create_trainmaster_users.rb +23 -0
- data/db/migrate/20161120020722_create_trainmaster_sessions.rb +11 -0
- data/lib/tasks/trainmaster_tasks.rake +4 -0
- data/lib/trainmaster.rb +10 -0
- data/lib/trainmaster/cache.rb +28 -0
- data/lib/trainmaster/engine.rb +9 -0
- data/lib/trainmaster/roles.rb +12 -0
- data/lib/trainmaster/version.rb +3 -0
- data/test/controllers/trainmaster/application_controller_test.rb +106 -0
- data/test/controllers/trainmaster/sessions_controller_test.rb +275 -0
- data/test/controllers/trainmaster/users_controller_test.rb +335 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +34 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +44 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/trainmaster/sessions.yml +36 -0
- data/test/fixtures/trainmaster/users.yml +27 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/jobs/trainmaster/sessions_cleanup_job_test.rb +9 -0
- data/test/mailers/previews/trainmaster/user_mailer_preview.rb +6 -0
- data/test/mailers/trainmaster/user_mailer_test.rb +9 -0
- data/test/models/trainmaster/session_test.rb +26 -0
- data/test/models/trainmaster/user_test.rb +52 -0
- data/test/test_helper.rb +33 -0
- data/test/trainmaster.rb +12 -0
- metadata +327 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7510a9194b744aa809f3e63ab7df28e5d115cc2f
|
4
|
+
data.tar.gz: a7a7bba317744d1311eb97d0df63fcfdac44defb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bbb59e5e424cff10717d6f9a59a8f53026efffdf450690641b3f58f6b9a1c91c1fa951ae9207fbf138242abece14a2dda934dfd24da57edd772233161c6cb078
|
7
|
+
data.tar.gz: 2688b0cf496fc8087696395f8d4dd12cc52bdc27494e1237f539ec243c4874c3de05445eb38c96a6f20313cdf4500b1cf680729c2c16d71639cdbdd13433d33f
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 David An
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
# trainmaster
|
2
|
+
|
3
|
+
[](https://travis-ci.org/davidan1981/trainmaster)
|
4
|
+
[](https://coveralls.io/github/davidan1981/trainmaster?branch=master)
|
5
|
+
[](https://codeclimate.com/github/davidan1981/trainmaster)
|
6
|
+
[](https://badge.fury.io/rb/trainmaster)
|
7
|
+
|
8
|
+
trainmaster is a Rails engine that provides
|
9
|
+
a [JWT](https://jwt.io/)-based session management platform for API
|
10
|
+
development. This plugin is suitable for developing RESTful APIs that do
|
11
|
+
not require an enterprise identity service. No cookies or non-unique IDs
|
12
|
+
involved in this project.
|
13
|
+
|
14
|
+
It is a continuation of
|
15
|
+
[rails-identity](https://github.com/davidan1981/rails-identity) which has
|
16
|
+
been deprecated due to backwards compatibility issues.
|
17
|
+
|
18
|
+
This documentation uses [httpie](https://github.zom/) (rather than curl)
|
19
|
+
to demonstrate making HTTP requests from the command line.
|
20
|
+
|
21
|
+
## Features
|
22
|
+
|
23
|
+
* Mountable Rails engine
|
24
|
+
* JWT-based session management API (REST)
|
25
|
+
* API key based authentication
|
26
|
+
* Email verification workflow
|
27
|
+
* Password reset workflow
|
28
|
+
* Caching
|
29
|
+
* OAuth authentication (beta)
|
30
|
+
|
31
|
+
## Install
|
32
|
+
|
33
|
+
Install the gem, or
|
34
|
+
go to your app's directory and add this line to your `Gemfile`:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
gem 'trainmaster'
|
38
|
+
```
|
39
|
+
|
40
|
+
Then, add the following line in `application.rb`:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'trainmaster'
|
44
|
+
```
|
45
|
+
|
46
|
+
And the following in `route.rb`:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'trainmaster'
|
50
|
+
|
51
|
+
Rails.application.routes.draw do
|
52
|
+
mount Trainmaster::Engine, at: "/"
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Note that you may designate a different target prefix other than the root.
|
57
|
+
Then, run `bundle install` and do `rake routes` to verify the routes.
|
58
|
+
|
59
|
+
Next, install migrations from trainmaster and perform migrations:
|
60
|
+
|
61
|
+
$ bundle exec rake trainmaster:install:migrations
|
62
|
+
$ bundle exec rake db:migrate RAILS_ENV=development
|
63
|
+
|
64
|
+
FYI, to see all `rake` tasks, do the following:
|
65
|
+
|
66
|
+
$ bundle exec rake --tasks
|
67
|
+
|
68
|
+
### Other Plugins
|
69
|
+
|
70
|
+
trainmaster uses ActiveJob to perform tasks asynchronously, which
|
71
|
+
requires a back-end module. For example, you can use
|
72
|
+
[DelayedJob](https://github.com/collectiveidea/delayed_job) by adding the
|
73
|
+
following in Gemfile.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
gem 'delayed_job_active_record'
|
77
|
+
gem 'daemons'
|
78
|
+
```
|
79
|
+
|
80
|
+
Also, email service must be specified in your app for sending out
|
81
|
+
email verification token and password reset token. Note that the
|
82
|
+
default email template is not sufficient for real use.
|
83
|
+
You must define your own mailer action views to cater emails for
|
84
|
+
your need.
|
85
|
+
|
86
|
+
To use OAuth, you must configure two endpoints. First, specify
|
87
|
+
`oauth_landing_page_url` to the URL that will assign token (from query
|
88
|
+
string) to a temporary storage such as cookie.
|
89
|
+
|
90
|
+
config.oauth_landing_page_url = '/oauth_success'
|
91
|
+
|
92
|
+
Once OAuth callback is successful, the controller will response a redirect
|
93
|
+
(302) to the URL specified above with `token=<actual token>` as a query
|
94
|
+
string.
|
95
|
+
|
96
|
+
Second, set a route for oauth failure.
|
97
|
+
|
98
|
+
get 'auth/failure', redirect_to('/oauth_failure')
|
99
|
+
|
100
|
+
This page should simply display failed authentication.
|
101
|
+
|
102
|
+
|
103
|
+
### Other Changes
|
104
|
+
|
105
|
+
`Trainmaster::User` model is a STI model. It means your app can inherit
|
106
|
+
from `Trainmaster::User` with additional attributes. All data will be
|
107
|
+
stored in `trainmaster_users` table. This is particularly useful if you
|
108
|
+
want to extend the model to meet your needs.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class User < Trainmaster::User
|
112
|
+
# more validations, attributes, methods, ...
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
### Running Your App
|
117
|
+
|
118
|
+
Now you're ready. Run the server to test:
|
119
|
+
|
120
|
+
$ bundle exec rails server
|
121
|
+
|
122
|
+
To allow DelayedJob tasks to run, do
|
123
|
+
|
124
|
+
$ RAILS_ENV=development bin/delayed_job start
|
125
|
+
|
126
|
+
## Usage
|
127
|
+
|
128
|
+
### Create User
|
129
|
+
|
130
|
+
Make a POST request on `/users` with `email`, `password`, and
|
131
|
+
`password_confirmation` in the JSON payload.
|
132
|
+
|
133
|
+
$ http POST localhost:3000/users email=foo@example.com password="supersecret" password_confirmation="supersecret"
|
134
|
+
|
135
|
+
The response should be 201 if successful.
|
136
|
+
|
137
|
+
HTTP/1.1 201 Created
|
138
|
+
{
|
139
|
+
"created_at": "2016-04-05T02:02:11.410Z",
|
140
|
+
"deleted_at": null,
|
141
|
+
"metadata": null,
|
142
|
+
"role": 10,
|
143
|
+
"updated_at": "2016-04-05T02:02:11.410Z",
|
144
|
+
"username": "foo@example.com",
|
145
|
+
"uuid": "68ddbb3a-fad2-11e5-8fc3-6c4008a6fa2a",
|
146
|
+
"verified": false
|
147
|
+
}
|
148
|
+
|
149
|
+
This request will send an email verification token to the user's email.
|
150
|
+
The app should craft the linked page to use the verification token to
|
151
|
+
start a session and set `verified` to true by the following:
|
152
|
+
|
153
|
+
http PATCH localhost:3000/users/current verified=true token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOm51bGwsInNlc3Npb25fdXVpZCI6IjU5YTQwODRjLTAwNWMtMTFlNi1hN2ExLTZjNDAwOGE2ZmEyYSIsInJvbGUiOm51bGwsImlhdCI6MTQ2MDQzMDczMiwiZXhwIjoxNDYwNDM0MzMyfQ.rdi5JT5NzI9iuXjWfhXjYhc0xF-aoVAaAPWepgSUaH0
|
154
|
+
|
155
|
+
Note that `current` can be used when UUID is unknown but the token is
|
156
|
+
specified. Also note that, if user's `verified` is `false`, some endpoints
|
157
|
+
will reject the request.
|
158
|
+
|
159
|
+
### Create Session
|
160
|
+
|
161
|
+
A proper way to create a session is to use username and password:
|
162
|
+
|
163
|
+
$ http POST localhost:3000/sessions username=foo@example.com password=supersecret
|
164
|
+
|
165
|
+
HTTP/1.1 201 Created
|
166
|
+
{
|
167
|
+
"created_at": "2016-04-05T02:04:22.465Z",
|
168
|
+
"metadata": null,
|
169
|
+
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiI2OGRkYmIzYS1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJzZXNzaW9uX3V1aWQiOiJiNmZhZGJhNC1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJyb2xlIjoxMCwiaWF0IjoxNDU5ODIxODYyLCJleHAiOjE0NjEwMzE0NjJ9.B9Ld00JvHUZT37THrwFrHzUwxIx6s3UFPbVCCwYzRrQ",
|
170
|
+
"updated_at": "2016-04-05T02:04:22.465Z",
|
171
|
+
"user_uuid": "68ddbb3a-fad2-11e5-8fc3-6c4008a6fa2a",
|
172
|
+
"uuid": "b6fadba4-fad2-11e5-8fc3-6c4008a6fa2a"
|
173
|
+
}
|
174
|
+
|
175
|
+
Notice this is essentially a login process for single-page apps. The client
|
176
|
+
app should store the value of `token` in either `localStore` or `cookie`.
|
177
|
+
(To allow cross-domain, you may want to use `cookie`.)
|
178
|
+
|
179
|
+
### Delete Session
|
180
|
+
|
181
|
+
A session can be deleted via a DELETE method. This is essentially a logout
|
182
|
+
process.
|
183
|
+
|
184
|
+
$ http DELETE localhost:3000/sessions/b6fadba4-fad2-11e5-8fc3-6c4008a6fa2a token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiI2OGRkYmIzYS1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJzZXNzaW9uX3V1aWQiOiJiNmZhZGJhNC1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJyb2xlIjoxMCwiaWF0IjoxNDU5ODIxODYyLCJleHAiOjE0NjEwMzE0NjJ9.B9Ld00JvHUZT37THrwFrHzUwxIx6s3UFPbVCCwYzRrQ
|
185
|
+
|
186
|
+
HTTP/1.1 204 No Content
|
187
|
+
|
188
|
+
Make sure to remove the token from its storage. The old tokens will no
|
189
|
+
longer work.
|
190
|
+
|
191
|
+
### Password Reset
|
192
|
+
|
193
|
+
Since trainmaster is a RESTful service itself, password reset is done via
|
194
|
+
a PATCH method on the user resource. But you must specify either the old
|
195
|
+
password or a reset token. To use the old password:
|
196
|
+
|
197
|
+
$ http PATCH localhost:3000/users/68ddbb3a-fad2-11e5-8fc3-6c4008a6fa2a old_password="supersecret" password="reallysecret" password_confirmation="reallysecret" token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiI2OGRkYmIzYS1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJzZXNzaW9uX3V1aWQiOiJiNmZhZGJhNC1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJyb2xlIjoxMCwiaWF0IjoxNDU5ODIxODYyLCJleHAiOjE0NjEwMzE0NjJ9.B9Ld00JvHUZT37THrwFrHzUwxIx6s3UFPbVCCwYzRrQ
|
198
|
+
|
199
|
+
To use a reset token, you must issue one first:
|
200
|
+
|
201
|
+
$ http PATCH localhost:3000/users/current username=foo@example.com issue_reset_token=true
|
202
|
+
|
203
|
+
HTTP/1.1 204 No Content
|
204
|
+
|
205
|
+
User token will be sent to the user's email. In a real application, the email
|
206
|
+
would include a link to a _page_ with JavaScript code automatically making a
|
207
|
+
PATCH request to `/users/current?token=<reset_token>`.
|
208
|
+
|
209
|
+
Note that the response includes a JWT token that looks similar to a normal
|
210
|
+
session token. Well a surprise! It _is_ a session token but with a shorter life span (1
|
211
|
+
hour). So use it instead on the password reset request:
|
212
|
+
|
213
|
+
http PATCH localhost:3000/users/current password="reallysecret" password_confirmation="reallysecret" token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiI2OGRkYmIzYS1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJzZXNzaW9uX3V1aWQiOiIzYjI5ZGI4OC1mYjlhLTExZTUtODNhOC02YzQwMDhhNmZhMmEiLCJyb2xlIjoxMCwiaWF0IjoxNDU5OTA3NTU0LCJleHAiOjE0NTk5MTExNTR9.g4iosqm8dOVUL5ErtCggsNAOs4WQV2u-heAUPf145jg
|
214
|
+
|
215
|
+
HTTP/1.1 200 OK
|
216
|
+
{
|
217
|
+
"created_at": "2016-04-05T02:02:11.410Z",
|
218
|
+
"deleted_at": null,
|
219
|
+
"metadata": null,
|
220
|
+
"reset_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOiI2OGRkYmIzYS1mYWQyLTExZTUtOGZjMy02YzQwMDhhNmZhMmEiLCJzZXNzaW9uX3V1aWQiOiIzYjI5ZGI4OC1mYjlhLTExZTUtODNhOC02YzQwMDhhNmZhMmEiLCJyb2xlIjoxMCwiaWF0IjoxNDU5OTA3NTU0LCJleHAiOjE0NTk5MTExNTR9.g4iosqm8dOVUL5ErtCggsNAOs4WQV2u-heAUPf145jg",
|
221
|
+
"role": 10,
|
222
|
+
"updated_at": "2016-04-06T01:55:45.163Z",
|
223
|
+
"username": "foo@example.com",
|
224
|
+
"uuid": "68ddbb3a-fad2-11e5-8fc3-6c4008a6fa2a",
|
225
|
+
"verification_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX3V1aWQiOm51bGwsInNlc3Npb25fdXVpZCI6IjU5YTQwODRjLTAwNWMtMTFlNi1hN2ExLTZjNDAwOGE2ZmEyYSIsInJvbGUiOm51bGwsImlhdCI6MTQ2MDQzMDczMiwiZXhwIjoxNDYwNDM0MzMyfQ.rdi5JT5NzI9iuXjWfhXjYhc0xF-aoVAaAPWepgSUaH0",
|
226
|
+
"verified": true
|
227
|
+
}
|
228
|
+
|
229
|
+
The token used with the request _must_ match the reset token previously
|
230
|
+
issued for the user.
|
231
|
+
|
232
|
+
### Authentication and Authorization
|
233
|
+
|
234
|
+
There are two ways to do general authentication: token or API key.
|
235
|
+
|
236
|
+
To authorize a request to an action, use provided callbacks. trainmaster
|
237
|
+
provides three controller callbacks for each approach:
|
238
|
+
|
239
|
+
* Token
|
240
|
+
* `require_token`
|
241
|
+
* `require_admin_token`
|
242
|
+
* `accept_token` - If a token is given, trainmaster will validate it.
|
243
|
+
* API key
|
244
|
+
* `require_api_key`
|
245
|
+
* `require_admin_api_key`
|
246
|
+
* `accept_api_key` - If an API key is given, trainmaster will validate it.
|
247
|
+
* Both
|
248
|
+
* `require_auth` - A token or an API key must be given.
|
249
|
+
* `require_admin_auth` - A token or an API key of an admin must be given.
|
250
|
+
* `accept_auth` - If either a token or an API key is given, trainmaster will validate it
|
251
|
+
|
252
|
+
To determine if the authenticated user has access to a specific resource
|
253
|
+
object, use `authorized?`. An example of a resource authorization callback
|
254
|
+
looks like the following:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
def authorize_user_to_obj(obj)
|
258
|
+
unless authorized?(obj)
|
259
|
+
raise Repia::Errors::Unauthorized
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
### Other Notes
|
265
|
+
|
266
|
+
#### Instance Variables
|
267
|
+
|
268
|
+
`ApplicationHelper` module will define the following instance variables:
|
269
|
+
|
270
|
+
* `@auth_user` - the authenticated user object
|
271
|
+
* `@auth_session` - the authenticated session
|
272
|
+
* `@token` - the token that authenticated the current session
|
273
|
+
* `@user` - the context user, only available if `get_user` is called
|
274
|
+
|
275
|
+
Try not to overload these variables. You may use these variables to enforce
|
276
|
+
further access control. Note that `@auth_session` and `@token` will be
|
277
|
+
populated only if a token is used to authenticate.
|
278
|
+
|
279
|
+
#### Roles
|
280
|
+
|
281
|
+
For convenience, trainmaster pre-defined four roles:
|
282
|
+
|
283
|
+
* Owner (1000) - the owner of the app
|
284
|
+
* Admin (100) - the admin(s) of the app
|
285
|
+
* User (10) - the user(s) of the app
|
286
|
+
* Public (0) - the rest of the world
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
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 = 'Trainmaster'
|
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("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
t.warning = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task default: :test
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require_dependency "trainmaster/application_controller"
|
2
|
+
|
3
|
+
module Trainmaster
|
4
|
+
|
5
|
+
##
|
6
|
+
# This class is sessions controller that performs CRD on session objects.
|
7
|
+
# Note that a token includes its session ID. Use "current" to look up a
|
8
|
+
# session in the current context.
|
9
|
+
#
|
10
|
+
class SessionsController < ApplicationController
|
11
|
+
|
12
|
+
prepend_before_action :require_auth, except: [:create, :options]
|
13
|
+
before_action :get_session, only: [:show, :destroy]
|
14
|
+
before_action :get_user, only: [:index]
|
15
|
+
|
16
|
+
##
|
17
|
+
# Lists all sessions that belong to the specified or authenticated user.
|
18
|
+
#
|
19
|
+
def index
|
20
|
+
@sessions = Session.where(user: @user)
|
21
|
+
expired = []
|
22
|
+
active = []
|
23
|
+
@sessions.each do |session|
|
24
|
+
if session.expired?
|
25
|
+
expired << session.uuid
|
26
|
+
else
|
27
|
+
active << session
|
28
|
+
end
|
29
|
+
end
|
30
|
+
SessionsCleanupJob.perform_later(*expired)
|
31
|
+
render json: active, except: [:secret]
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# This action is essentially the login action. Note that get_user is not
|
36
|
+
# triggered for this action because we will look at username first. That
|
37
|
+
# would be the "normal" way to login. The alternative would be with the
|
38
|
+
# token based authentication. If the latter doesn't make sense, just use
|
39
|
+
# the username and password approach.
|
40
|
+
#
|
41
|
+
# A ApplicationController::UNAUTHORIZED_ERROR is thrown if user is not
|
42
|
+
# verified.
|
43
|
+
#
|
44
|
+
def create
|
45
|
+
|
46
|
+
# See if OAuth is used first. When authenticated successfully, either
|
47
|
+
# the existing user will be found or a new user will be created.
|
48
|
+
# Failure will be redirected to this action but will not match this
|
49
|
+
# branch.
|
50
|
+
if (omniauth_hash = request.env["omniauth.auth"])
|
51
|
+
@user = User.from_omniauth_hash(omniauth_hash)
|
52
|
+
|
53
|
+
# Then see if the request already has authentication. Note that if the
|
54
|
+
# user does not have access to the specified session owner, 401 will
|
55
|
+
# be thrown.
|
56
|
+
elsif accept_auth
|
57
|
+
@user = @auth_user
|
58
|
+
|
59
|
+
# Otherwise, it's a normal login process. Use username and password to
|
60
|
+
# authenticate. The user must exist, the password must be vaild, and
|
61
|
+
# the email must have been verified.
|
62
|
+
else
|
63
|
+
@user = User.find_by_username(session_params[:username])
|
64
|
+
if (@user.nil? || !@user.authenticate(session_params[:password]) ||
|
65
|
+
!@user.verified)
|
66
|
+
raise ApplicationController::UNAUTHORIZED_ERROR
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Finally, create session regardless of the method and store it.
|
71
|
+
@session = Session.new(user: @user)
|
72
|
+
if @session.save
|
73
|
+
if omniauth_hash
|
74
|
+
# redirect_to the app page that accepts new session token
|
75
|
+
url = Rails.application.config.oauth_landing_page_url
|
76
|
+
url = "#{url}?token=#{@session.token}"
|
77
|
+
render inline: "", status: 302, location: url
|
78
|
+
else
|
79
|
+
render json: @session, except: [:secret], status: 201
|
80
|
+
end
|
81
|
+
else
|
82
|
+
# :nocov:
|
83
|
+
render_errors 400, @session.full_error_messages
|
84
|
+
# :nocov:
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Shows a session information.
|
90
|
+
#
|
91
|
+
def show
|
92
|
+
render json: @session, except: [:secret]
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Deletes a session.
|
97
|
+
#
|
98
|
+
def destroy
|
99
|
+
if @session.destroy
|
100
|
+
render body: "", status: 204
|
101
|
+
else
|
102
|
+
# :nocov:
|
103
|
+
render_error 400, @session.errors.full_messages
|
104
|
+
# :nocov:
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
##
|
111
|
+
# Get the specified or current session.
|
112
|
+
#
|
113
|
+
# A Repia::Errors::NotFound is raised if the session does not
|
114
|
+
# exist (or deleted due to expiration).
|
115
|
+
#
|
116
|
+
# A ApplicationController::UNAUTHORIZED_ERROR is raised if the
|
117
|
+
# authenticated user does not have authorization for the specified
|
118
|
+
# session.
|
119
|
+
#
|
120
|
+
def get_session
|
121
|
+
session_id = params[:id]
|
122
|
+
if session_id == "current"
|
123
|
+
if @auth_session.nil?
|
124
|
+
raise Repia::Errors::NotFound
|
125
|
+
end
|
126
|
+
session_id = @auth_session.id
|
127
|
+
end
|
128
|
+
@session = find_object(Session, session_id)
|
129
|
+
authorize_for!(@session)
|
130
|
+
if @session.expired?
|
131
|
+
@session.destroy
|
132
|
+
raise Repia::Errors::NotFound
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def session_params
|
137
|
+
params.permit(:username, :password)
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|