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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +286 -0
  4. data/Rakefile +38 -0
  5. data/app/controllers/trainmaster/application_controller.rb +9 -0
  6. data/app/controllers/trainmaster/sessions_controller.rb +141 -0
  7. data/app/controllers/trainmaster/users_controller.rb +199 -0
  8. data/app/helpers/trainmaster/application_helper.rb +313 -0
  9. data/app/helpers/trainmaster/sessions_helper.rb +4 -0
  10. data/app/helpers/trainmaster/users_helper.rb +4 -0
  11. data/app/jobs/trainmaster/sessions_cleanup_job.rb +13 -0
  12. data/app/mailers/application_mailer.rb +4 -0
  13. data/app/mailers/trainmaster/user_mailer.rb +14 -0
  14. data/app/models/trainmaster/session.rb +56 -0
  15. data/app/models/trainmaster/user.rb +77 -0
  16. data/app/views/layouts/mailer.html.erb +5 -0
  17. data/app/views/layouts/mailer.text.erb +1 -0
  18. data/app/views/layouts/trainmaster/application.html.erb +14 -0
  19. data/app/views/trainmaster/user_mailer/email_verification.html.erb +12 -0
  20. data/app/views/trainmaster/user_mailer/email_verification.text.erb +13 -0
  21. data/app/views/trainmaster/user_mailer/password_reset.html.erb +14 -0
  22. data/app/views/trainmaster/user_mailer/password_reset.text.erb +15 -0
  23. data/config/routes.rb +10 -0
  24. data/db/migrate/20161120020344_create_trainmaster_users.rb +23 -0
  25. data/db/migrate/20161120020722_create_trainmaster_sessions.rb +11 -0
  26. data/lib/tasks/trainmaster_tasks.rake +4 -0
  27. data/lib/trainmaster.rb +10 -0
  28. data/lib/trainmaster/cache.rb +28 -0
  29. data/lib/trainmaster/engine.rb +9 -0
  30. data/lib/trainmaster/roles.rb +12 -0
  31. data/lib/trainmaster/version.rb +3 -0
  32. data/test/controllers/trainmaster/application_controller_test.rb +106 -0
  33. data/test/controllers/trainmaster/sessions_controller_test.rb +275 -0
  34. data/test/controllers/trainmaster/users_controller_test.rb +335 -0
  35. data/test/dummy/README.rdoc +28 -0
  36. data/test/dummy/Rakefile +6 -0
  37. data/test/dummy/app/assets/javascripts/application.js +13 -0
  38. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  39. data/test/dummy/app/controllers/application_controller.rb +5 -0
  40. data/test/dummy/app/helpers/application_helper.rb +2 -0
  41. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  42. data/test/dummy/bin/bundle +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/bin/setup +29 -0
  46. data/test/dummy/config.ru +4 -0
  47. data/test/dummy/config/application.rb +34 -0
  48. data/test/dummy/config/boot.rb +5 -0
  49. data/test/dummy/config/database.yml +25 -0
  50. data/test/dummy/config/environment.rb +5 -0
  51. data/test/dummy/config/environments/development.rb +41 -0
  52. data/test/dummy/config/environments/production.rb +79 -0
  53. data/test/dummy/config/environments/test.rb +44 -0
  54. data/test/dummy/config/initializers/assets.rb +11 -0
  55. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  56. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  57. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  58. data/test/dummy/config/initializers/inflections.rb +16 -0
  59. data/test/dummy/config/initializers/mime_types.rb +4 -0
  60. data/test/dummy/config/initializers/session_store.rb +3 -0
  61. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  62. data/test/dummy/config/locales/en.yml +23 -0
  63. data/test/dummy/config/routes.rb +4 -0
  64. data/test/dummy/config/secrets.yml +22 -0
  65. data/test/dummy/public/404.html +67 -0
  66. data/test/dummy/public/422.html +67 -0
  67. data/test/dummy/public/500.html +66 -0
  68. data/test/dummy/public/favicon.ico +0 -0
  69. data/test/fixtures/trainmaster/sessions.yml +36 -0
  70. data/test/fixtures/trainmaster/users.yml +27 -0
  71. data/test/integration/navigation_test.rb +10 -0
  72. data/test/jobs/trainmaster/sessions_cleanup_job_test.rb +9 -0
  73. data/test/mailers/previews/trainmaster/user_mailer_preview.rb +6 -0
  74. data/test/mailers/trainmaster/user_mailer_test.rb +9 -0
  75. data/test/models/trainmaster/session_test.rb +26 -0
  76. data/test/models/trainmaster/user_test.rb +52 -0
  77. data/test/test_helper.rb +33 -0
  78. data/test/trainmaster.rb +12 -0
  79. metadata +327 -0
@@ -0,0 +1,4 @@
1
+ module Trainmaster
2
+ module SessionsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Trainmaster
2
+ module UsersHelper
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ module Trainmaster
2
+ class SessionsCleanupJob < ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(*args)
6
+ # Do something later
7
+ args.each do |uuid|
8
+ session = Session.find_by_uuid(uuid)
9
+ session.destroy()
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: Trainmaster::MAILER_EMAIL
3
+ layout 'mailer'
4
+ end
@@ -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,5 @@
1
+ <html>
2
+ <body>
3
+ <%= yield %>
4
+ </body>
5
+ </html>
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :trainmaster do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,10 @@
1
+ require "trainmaster/engine"
2
+ require "trainmaster/cache"
3
+ require "trainmaster/roles"
4
+
5
+ module Trainmaster
6
+
7
+ # App MUST monkey patch this constant
8
+ MAILER_EMAIL = "no-reply@trainmaster.com"
9
+
10
+ end
@@ -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,9 @@
1
+ Gem.loaded_specs['trainmaster'].dependencies.each do |d|
2
+ require d.name
3
+ end
4
+
5
+ module Trainmaster
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Trainmaster
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+
2
+ module Trainmaster
3
+
4
+ # Fixed set of roles.
5
+ module Roles
6
+ PUBLIC = 0
7
+ USER = 10
8
+ ADMIN = 100
9
+ OWNER = 1000
10
+ end
11
+
12
+ end
@@ -0,0 +1,3 @@
1
+ module Trainmaster
2
+ VERSION = "0.1.0"
3
+ 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