trainmaster 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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