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
@@ -0,0 +1,14 @@
|
|
1
|
+
module Trainmaster
|
2
|
+
class UserMailer < ApplicationMailer
|
3
|
+
|
4
|
+
def email_verification(user)
|
5
|
+
@user = user
|
6
|
+
mail(to: @user.username, subject: "[trainmaster] Email Confirmation")
|
7
|
+
end
|
8
|
+
|
9
|
+
def password_reset(user)
|
10
|
+
@user = user
|
11
|
+
mail(to: @user.username, subject: "[trainmaster] Password Reset")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Trainmaster
|
2
|
+
class Session < ActiveRecord::Base
|
3
|
+
include Repia::Support::UUIDModel
|
4
|
+
|
5
|
+
# Does not act as paranoid since session objects will be frequently
|
6
|
+
# created.
|
7
|
+
|
8
|
+
belongs_to :user, foreign_key: "user_uuid", primary_key: "uuid"
|
9
|
+
validates :user, presence: true
|
10
|
+
|
11
|
+
##
|
12
|
+
# Creates a session object. The attributes must include user. The secret
|
13
|
+
# to the JWT is generated here and is unique to the session being
|
14
|
+
# created. Since the JWT includes the session id, the secret can be
|
15
|
+
# retrieved.
|
16
|
+
#
|
17
|
+
def initialize(attributes = {})
|
18
|
+
seconds = attributes.delete(:seconds) || (24 * 3600 * 14)
|
19
|
+
super
|
20
|
+
self.uuid = UUIDTools::UUID.timestamp_create().to_s
|
21
|
+
iat = Time.now.to_i
|
22
|
+
payload = {
|
23
|
+
user_uuid: self.user.uuid,
|
24
|
+
session_uuid: self.uuid,
|
25
|
+
role: self.user.role,
|
26
|
+
iat: iat,
|
27
|
+
exp: iat + seconds
|
28
|
+
}
|
29
|
+
self.secret = UUIDTools::UUID.random_create
|
30
|
+
self.token = JWT.encode(payload, self.secret, 'HS256')
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Determines if the session has expired or not.
|
35
|
+
#
|
36
|
+
def expired?
|
37
|
+
begin
|
38
|
+
JWT.decode self.token, nil, false
|
39
|
+
rescue JWT::ExpiredSignature
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns the role of the session user.
|
47
|
+
#
|
48
|
+
def role
|
49
|
+
if !instance_variable_defined?(:@role)
|
50
|
+
@role = user.role
|
51
|
+
end
|
52
|
+
return @role
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Trainmaster
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
include Repia::Support::UUIDModel
|
4
|
+
acts_as_paranoid
|
5
|
+
has_secure_password validations: false
|
6
|
+
|
7
|
+
validates :username, uniqueness: true,
|
8
|
+
format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i,
|
9
|
+
on: [:create, :update] }, allow_nil: true
|
10
|
+
validates :password, confirmation: true
|
11
|
+
validate :valid_user
|
12
|
+
before_save :default_role
|
13
|
+
|
14
|
+
alias_attribute :email, :username
|
15
|
+
|
16
|
+
##
|
17
|
+
# This method validates if the user object is valid. A user is valid if
|
18
|
+
# username and password exist OR oauth integration exists.
|
19
|
+
#
|
20
|
+
def valid_user
|
21
|
+
if (self.username.blank? || self.password_digest.blank?) &&
|
22
|
+
(self.oauth_provider.blank? || self.oauth_uid.blank?)
|
23
|
+
errors.add(:username, " and password OR oauth must be specified")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Create a user from oauth.
|
29
|
+
#
|
30
|
+
def self.from_omniauth_hash(auth_hash)
|
31
|
+
params = {
|
32
|
+
oauth_provider: auth_hash.provider,
|
33
|
+
oauth_uid: auth_hash.uid
|
34
|
+
}
|
35
|
+
where(params).first_or_initialize(attributes={}) do |user|
|
36
|
+
user.oauth_provider = auth_hash.provider
|
37
|
+
user.oauth_uid = auth_hash.uid
|
38
|
+
user.oauth_name = auth_hash.info.name
|
39
|
+
user.oauth_token = auth_hash.credentials.token
|
40
|
+
user.oauth_expires_at = Time.at(auth_hash.credentials.expires_at)
|
41
|
+
user.verified = true
|
42
|
+
user.save!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Initializes the user. User is not verified initially. The user has one
|
48
|
+
# hour to get verified. After that, a PATCH request must be made to
|
49
|
+
# re-issue the verification token.
|
50
|
+
#
|
51
|
+
def initialize(attributes = {})
|
52
|
+
attributes[:api_key] = SecureRandom.hex(32)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Sets the default the role for the user if not set.
|
58
|
+
#
|
59
|
+
def default_role
|
60
|
+
self.role ||= Roles::USER
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# This method will generate a reset token that lasts for an hour.
|
65
|
+
#
|
66
|
+
def issue_token(kind)
|
67
|
+
session = Session.new(user: self, seconds: 3600)
|
68
|
+
session.save
|
69
|
+
if kind == :reset_token
|
70
|
+
self.reset_token = session.token
|
71
|
+
elsif kind == :verification_token
|
72
|
+
self.verification_token = session.token
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= yield %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Trainmaster</title>
|
5
|
+
<%= stylesheet_link_tag "trainmaster/application", media: "all" %>
|
6
|
+
<%= javascript_include_tag "trainmaster/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<p>Dear <%= @user.username %>,</p>
|
2
|
+
|
3
|
+
<p>Please confirm your account with trainmaster by making a PATCH request
|
4
|
+
on the current user with a provided verification token. For example,
|
5
|
+
<pre>http PATCH /users/current token=<%= @user.verification_token %>
|
6
|
+
verified=true</pre> will confirm the account. Here is the verification
|
7
|
+
token:</p>
|
8
|
+
|
9
|
+
<pre><%= @user.verification_token %></pre>
|
10
|
+
|
11
|
+
<p>Thank you for using trainmaster</p>
|
12
|
+
<p><b>trainmaster</b></p>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
Dear <%= @user.username %>,
|
2
|
+
|
3
|
+
Please confirm your account with trainmaster by making a PATCH request
|
4
|
+
on the current user with a provided verification token. For example,
|
5
|
+
|
6
|
+
http PATCH /users/current token=<%= @user.verification_token %> verified=true
|
7
|
+
|
8
|
+
will confirm the account. Here is the verification token:
|
9
|
+
|
10
|
+
<%= @user.verification_token %>
|
11
|
+
|
12
|
+
Thank you for using trainmaster,
|
13
|
+
trainmaster
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<p>Dear <%= @user.username %>,</p>
|
2
|
+
|
3
|
+
<p>You have requested to reset your password. Here are the user UUID and
|
4
|
+
reset token. Make a PATCH request on the UUID with the reset token to set a
|
5
|
+
new password. For instance, <pre>http PATCH /users/current token=<%=
|
6
|
+
@user.reset_token %> password=reallysecret
|
7
|
+
password_confirmation=reallysecret</pre> will set the password to
|
8
|
+
<pre>reallysecret</pre> for the user to whom the reset token was issued.
|
9
|
+
Here is the reset token:</p>
|
10
|
+
|
11
|
+
<pre><%= @user.reset_token %></pre>
|
12
|
+
|
13
|
+
<p>Good luck! :)</p>
|
14
|
+
<p><b>trainmaster</b></p>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Dear <%= @user.username %>,
|
2
|
+
|
3
|
+
You have requested to reset your password. Here are the user UUID and reset
|
4
|
+
token. Make a PATCH request on the UUID with the reset token to set a new
|
5
|
+
password. For instance,
|
6
|
+
|
7
|
+
http PATCH /users/current token=... password=reallysecret password_confirmation=reallysecret
|
8
|
+
|
9
|
+
will set the password to "reallysecret" (without quotes) for the user to
|
10
|
+
whom the reset token was issued.
|
11
|
+
|
12
|
+
Here is the reset token: @user.reset_token
|
13
|
+
|
14
|
+
Good luck! :)
|
15
|
+
trainmaster
|
data/config/routes.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
Trainmaster::Engine.routes.draw do
|
2
|
+
resources :sessions
|
3
|
+
match 'sessions(/:id)' => 'sessions#options', via: [:options]
|
4
|
+
|
5
|
+
resources :users
|
6
|
+
match 'users(/:id)' => 'users#options', via: [:options]
|
7
|
+
|
8
|
+
get 'auth/:provider/callback', to: 'sessions#create'
|
9
|
+
# get 'auth/failure', to: 'session#create'
|
10
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CreateTrainmasterUsers < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :trainmaster_users, id: false, force: :cascade do |t|
|
4
|
+
t.string :uuid, primary_key: true, null: false
|
5
|
+
t.string :username
|
6
|
+
t.string :password_digest
|
7
|
+
t.integer :role
|
8
|
+
t.string :reset_token
|
9
|
+
t.string :verification_token
|
10
|
+
t.boolean :verified, default: false
|
11
|
+
t.string :type
|
12
|
+
t.string :api_key, index: true
|
13
|
+
t.string :oauth_provider
|
14
|
+
t.string :oauth_uid
|
15
|
+
t.string :oauth_name
|
16
|
+
t.string :oauth_token
|
17
|
+
t.string :oauth_expires_at
|
18
|
+
t.datetime :deleted_at, index: true
|
19
|
+
t.timestamps null: false
|
20
|
+
end
|
21
|
+
add_index "trainmaster_users", ["oauth_provider", "oauth_uid"], name: "index_trainmaster_users_on_oauth_provider_and_oauth_uid"
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateTrainmasterSessions < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :trainmaster_sessions, id: false, force: :cascade do |t|
|
4
|
+
t.string :uuid, primary_key: true, null: false
|
5
|
+
t.string :user_uuid, null: false
|
6
|
+
t.string :token, null: false
|
7
|
+
t.string :secret, null: false
|
8
|
+
t.timestamps null: false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/trainmaster.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
module Trainmaster
|
3
|
+
|
4
|
+
##
|
5
|
+
# Use this module to read from and write to cache so prefix is
|
6
|
+
# consistently enforced.
|
7
|
+
#
|
8
|
+
module Cache
|
9
|
+
CACHE_VERSION = "0.0.1"
|
10
|
+
|
11
|
+
def self.cache_key(key)
|
12
|
+
if key.is_a? Hash
|
13
|
+
key["_version"] = CACHE_VERSION
|
14
|
+
return key
|
15
|
+
else
|
16
|
+
return {key: key, _version: CACHE_VERSION}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.get(key)
|
21
|
+
return Rails.cache.fetch(cache_key(key))
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.set(key, value)
|
25
|
+
Rails.cache.write(cache_key(key), value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Trainmaster
|
4
|
+
|
5
|
+
class TestsController < ApplicationController
|
6
|
+
def index
|
7
|
+
render json: {}, status: 200
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class TestsControllerTest < ActionController::TestCase
|
12
|
+
setup do
|
13
|
+
Rails.cache.clear
|
14
|
+
@session = trainmaster_sessions(:one)
|
15
|
+
@token = @session.token
|
16
|
+
@admin_session = trainmaster_sessions(:admin_one)
|
17
|
+
@admin_token = @admin_session.token
|
18
|
+
@api_key = trainmaster_users(:one).api_key
|
19
|
+
@admin_api_key = trainmaster_users(:admin_one).api_key
|
20
|
+
# Rails.application.routes.draw do
|
21
|
+
Trainmaster::Engine.routes.draw do
|
22
|
+
match "tests" => "tests#index", via: [:get]
|
23
|
+
end
|
24
|
+
@routes = Engine.routes
|
25
|
+
end
|
26
|
+
|
27
|
+
teardown do
|
28
|
+
Trainmaster::Engine.routes.draw do
|
29
|
+
resources :sessions
|
30
|
+
match 'sessions(/:id)' => 'sessions#options', via: [:options]
|
31
|
+
|
32
|
+
resources :users
|
33
|
+
match 'users(/:id)' => 'users#options', via: [:options]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
test "require only token" do
|
38
|
+
class ::Trainmaster::TestsController < ApplicationController
|
39
|
+
reset_callbacks :process_action
|
40
|
+
before_action :require_token, only: [:index]
|
41
|
+
end
|
42
|
+
get :index, params: { token: @token }
|
43
|
+
assert_response :success
|
44
|
+
get :index
|
45
|
+
assert_response 401
|
46
|
+
end
|
47
|
+
|
48
|
+
test "require only admin token" do
|
49
|
+
class ::Trainmaster::TestsController < ApplicationController
|
50
|
+
reset_callbacks :process_action
|
51
|
+
before_action :require_admin_token, only: [:index]
|
52
|
+
end
|
53
|
+
get :index, params: { token: @admin_token }
|
54
|
+
assert_response :success
|
55
|
+
Cache.set({kind: :session, token: @token}, @session)
|
56
|
+
get :index, params: { token: @token }
|
57
|
+
assert_response 401
|
58
|
+
end
|
59
|
+
|
60
|
+
test "accept only token" do
|
61
|
+
class ::Trainmaster::TestsController < ApplicationController
|
62
|
+
reset_callbacks :process_action
|
63
|
+
before_action :accept_token, only: [:index]
|
64
|
+
end
|
65
|
+
get :index, params: { token: @token }
|
66
|
+
assert_response :success
|
67
|
+
get :index
|
68
|
+
assert_response :success
|
69
|
+
end
|
70
|
+
|
71
|
+
test "require only api key" do
|
72
|
+
class ::Trainmaster::TestsController < ApplicationController
|
73
|
+
reset_callbacks :process_action
|
74
|
+
before_action :require_api_key, only: [:index]
|
75
|
+
end
|
76
|
+
get :index, params: { api_key: @api_key }
|
77
|
+
assert_response :success
|
78
|
+
get :index, params: { token: @token }
|
79
|
+
assert_response 401
|
80
|
+
get :index
|
81
|
+
assert_response 401
|
82
|
+
end
|
83
|
+
|
84
|
+
test "require only admin api key" do
|
85
|
+
class ::Trainmaster::TestsController < ApplicationController
|
86
|
+
reset_callbacks :process_action
|
87
|
+
before_action :require_admin_api_key, only: [:index]
|
88
|
+
end
|
89
|
+
get :index, params: { api_key: @admin_api_key }
|
90
|
+
assert_response :success
|
91
|
+
get :index, params: { api_key: @api_key }
|
92
|
+
assert_response 401
|
93
|
+
end
|
94
|
+
|
95
|
+
test "accept only api key" do
|
96
|
+
class ::Trainmaster::TestsController < ApplicationController
|
97
|
+
reset_callbacks :process_action
|
98
|
+
before_action :accept_api_key, only: [:index]
|
99
|
+
end
|
100
|
+
get :index, params: { api_key: @api_key }
|
101
|
+
assert_response :success
|
102
|
+
get :index
|
103
|
+
assert_response :success
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|