trainmaster 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/davidan1981/trainmaster.svg?branch=master)](https://travis-ci.org/davidan1981/trainmaster)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/github/davidan1981/trainmaster/badge.svg?branch=master)](https://coveralls.io/github/davidan1981/trainmaster?branch=master)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/davidan1981/trainmaster/badges/gpa.svg)](https://codeclimate.com/github/davidan1981/trainmaster)
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/trainmaster.svg)](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
|